Power Appsでリソースや他人の空き時間を確認して予約する

## 緒言
Xでたくさん反響をいただいた[下記投稿](https://x.com/meccha__eeyan/status/1833171707449978893)のアプリの作り方。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3670355/85d5f390-1743-ef08-0fbf-421a39b67011.png)
機能としては選択したリソース（他人でもOK）の空き時間可視化とワンクリック確保。

:::note info
そもそも[@Takashi_Masumoriさんの記事](https://qiita.com/Takashi_Masumori/items/bc3c4588a18ad852a246)と、その記事のアプリを[ダウンロード](https://blogs.windows.com/japan/2021/07/05/2powerappsforteams/)して勉強させていただいたおかげで作成できました。ありがとうございます！
:::

## 作り方
### データ接続
本アプリで使用しているデータ接続は下図の2つのみ。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3670355/4b38a500-ad4a-3b3e-e55c-ab82aff251d6.png)


### 定数の設定
下図のように4つの定数を`Formulas`プロパティで設定しておく。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3670355/3309e488-04ad-236a-ab9d-dca3359da2e3.png)
`_ResourceList`：リソースアカウント格納
`_MyProfile`：アプリ利用者のアカウント情報
`_MyCalenderID`：アプリ利用者のOutlookカレンダー「予定表」のID
`_BusinessHour`：稼働時間タイムテーブル（8～18時とする）

:::note warn
`CalenderGetTablesV2`で`name="予定表"`の条件で`LookUp`すればアプリ利用者のOutlookカレンダーIDを取得可能だが、`"予定表"`の部分は海外メンバーの場合は当然表記が異なるので要注意
:::

### 主なコントロール
本アプリのポイントとなるコントロールは下図の3つ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3670355/c04a67a6-289b-d6e4-0434-c9daa77d2843.png)

**リソース選択ギャラリー**
前述の`_ResourceList`を`Items`プロパティに設定し、チェックボックスでリソースを選択。

**隠しボタン** [^1]
空き時間情報を更新するコードを`OnSelect`に記述。`Width`と`Height`をゼロにして、他のコントロールから`Select`関数で空き時間情報を更新したい時に呼び出す。

**空き時間可視化ギャラリー**
隠しボタンで取得した空き時間情報を基にタイムテーブル可視化。ワンクリックでリソース予約できるボタンも配置。リソース選択ギャラリーで選択されたレコードを格納したコレクションをデータソースにするギャラリーの中に、前述の`_BusinessHour`をデータソースに持つギャラリーが入れ子になっている。


[^1]: [ヨウセイさんのイベント](https://www.youtube.com/watch?v=iHIcKBoRuo0&t=4285s)で教えていただいたテクニックです。

### リソース選択ギャラリーの設定
`Items`プロパティは前述の`_ResourceList`を設定。
ギャラリー内にチェックボックスを配置し、チェックされるとコレクション`ColSelectedResouces`にチェックしたリソース情報が格納されるよう、チェックボックスの`OnCheck`プロパティに下記を設定。

```
Collect(ColSelectedResources,{
    ID:ThisItem.Id,
    DisplayName:ThisItem.DisplayName,
    Resource:ThisItem,
    MeetingTime:Office365Outlook.FindMeetingTimesV2({
        RequiredAttendees:ThisItem.Mail,
        MeetingDuration:_baseMinute,
        IsOrganizerOptional:true,
        Start:_selectedDate,
        End:DateAdd(_selectedDate,1,TimeUnit.Days),
        MaxCandidates:40
    }).meetingTimeSuggestions
})
```

ポイントになる`FindMeetingTimesV2`[^2]は指定した出席者のOutlookカレンダー空き時間情報に基づいて会議可能時間を返すアクション。`IsOrganizerOptional`オプションがデフォルトでは`false`になっているところを`true`にするのがポイントで、これをしておけば自分自身の予定情報が加味されず純粋に相手方の空き情報が返されるようになる。

[^2]:詳細は[Learn](https://learn.microsoft.com/ja-jp/connectors/office365/#%E4%BC%9A%E8%AD%B0%E3%81%AE%E6%99%82%E9%96%93%E3%82%92%E6%A4%9C%E7%B4%A2-(v2))参照

チェックボックスの`OnUnCheck`プロパティには下記を設定し、チェックがはずされるとコレクションから除外されるようにしておく。

```
Remove(ColSelectedResources, LookUp(ColSelectedResources, ID=ThisItem.Id))
```

### 隠しボタンの設定[^1]
`Width`と`Height`をゼロに設定し画面上は見えないように。`OnSelect`を必要な時に呼び出すユーザー定義関数的な使い方。
`FindMeetingTimesV2`の`MeetingDuration`や`Start`、`End`等の空き情報取得条件が変化した時に再取得するよう下記を`OnSelect`に記述する。

```
Clear(ColSelectedResources);
ForAll(
    Filter(GalleryResources.AllItems,Checkbox1.Value=true),
    // GalleryResourcesはリソース選択ギャラリーのこと
    Collect(ColSelectedResources,{
        ID:ThisRecord.Id,
        DisplayName:ThisRecord.DisplayName,
        Resource:ThisRecord,
        MeetingTime:Office365Outlook.FindMeetingTimesV2({
            RequiredAttendees:ThisRecord.Mail,
            MeetingDuration:_baseMinute,
            IsOrganizerOptional:true,
            Start:_selectedDate,
            End:DateAdd(_selectedDate,1,TimeUnit.Days),
            MaxCandidates:40
        }).meetingTimeSuggestions
    })
)
```

あとは必要なタイミングに`Select`関数でこのボタンアクションを呼び出す。

### 空き時間可視化ギャラリーの設定
本アプリ最大の山場。使用するコントロールの構成は下図の通り。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3670355/dcf9e304-3acd-e191-4031-d2b8c5baa8a5.png)
**GallerySelectedResources**は**水平**ギャラリーで`Items`プロパティは前述の`ColSelectedResources`で、**LabelDisplayName**に各リソースの`DisplayName`が表示されるようにしておく。
**GalleryTimeTable**は**垂直**ギャラリーで`Items`プロパティは前述の`_BusinessHour`を設定し、各リソースごとの空き時間可視化のためのタイムテーブルとなる。`_BusinessHour`の中身は下図のような**30分間隔**のテーブルとしている。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3670355/d8d1877a-b5e1-76e4-93bb-705ca1bba99a.png)
**RectangleSeparator**(目盛線)や**LabelTime**(時間ラベル)は1時間単位で表示されるように`Visible`プロパティを`Mod(ThisItem.Value,1)=0`としている。
予定有りを灰色網掛けするための**RectangleBusy**のプロパティ設定は下記のようにした。
```
Y: 0
Height: =Parent.TemplateHeight*_baseMinute/30
Visible: |-
    =IsBlank(
        LookUp(
            LookUp(ColSelectedResources,DisplayName = LabelDisplayName.Text).MeetingTime,
            TimeValue(
                DateAdd(
                    DateTimeValue(meetingTimeSlot.start.dateTime),
                    -TimeZoneOffset(),
                    TimeUnit.Minutes
                )
            ) = ThisItem.Time
        )
    )
```
`LookUp`が2回出てきて複雑だが、要するに30分ごとに`FindMeetingTimesV2`の空き時間を検索して空きが無い(`IsBlank`)状態だったら予定有りということで`Visible`が`true`になるという考え方。`LookUp`が2回出てきているのは**Gallery In Gallery**の内側から**親Gallery**のレコードを取ってくるため。。。何かスマートなテクニックがあったような気がする。。。

最後に、クリックして空き時間予約[^3]するボタン**ButtonReserve**のプロパティは下記。
```
X: =RectangleBusy.X
Y: =RectangleBusy.Y
Height: =RectangleBusy.Height
Width: =RectangleBusy.Width
Visible: =!RectangleBusy.Visible
OnSelect: |-
    =With(
        {
            _dateTime: DateTimeValue(
                Text(_selectedDate,"yyyy-mm-dd") & " " &
                Text(ThisItem.Time,"hh:mm")
            )
        },
        Office365Outlook.V4CalendarPostItem(
            _MyCalenderID,
            "MTG",
            _dateTime,
            DateAdd(_dateTime,_baseMinute,TimeUnit.Minutes),
            "(UTC+09:00) Osaka, Sapporo, Tokyo",
            {
                requiredAttendees: LookUp(
                    ColSelectedResources,
                    DisplayName = LabelDisplayName.Text
                ).Resource.Mail
            }
        )
    )
```
[^3]: 詳細は[V4CalendarPostItem](https://learn.microsoft.com/ja-jp/connectors/office365/#%E3%82%A4%E3%83%99%E3%83%B3%E3%83%88%E3%81%AE%E4%BD%9C%E6%88%90-(v4))

## 結言
SharePointリスト等のデータソースすら不要で慣れれば2～3時間で作成できる割に使い勝手が良くて個人的にも気に入ってます！リソース予約に重点を置いて書いてますが、リソースアカウントだけでなく人のアカウントでも同様のことができるのでアイデア次第でいろいろできそうで良いなと思ってます。