PowerAppsで業務シフト管理アプリ 改良版

# 概要
- 作業者及び日付ごとの作業内容を選択してシフト管理ができるアプリの解説です。
- [以前作ったアプリ](https://qiita.com/Rambosan/items/d34693423be932ee21f3)の改良版です。
- いくつか気に入らない部分や反省点があり改良しました。

# 改良点
- ギャラリーを4つ使用→3つ使用に
    - AddColumnsで直接データを取得できたのに何を思ったかギャラリーを介してしまったため改良。
- シフトデータの取得を一回に
    - 作業者ごとにデータソースから取得→1週間分をまとめて取得(要件的に500件以内なので)
- 保存ボタンを1つに
    - 入れ子ギャラリーの参照が空になる問題がクリアになったので。
 
# アプリの仕様
前回と同じです。
左側に作業者リスト、上に一週間分の日付、その交わるセルに業務内容があります。
ドロップダウンで業務内容を変更して保存可能です。
データはSharePointリストに保管です。
![Animation2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293667/68efe887-3d98-bacc-978e-732126d4a9e9.gif)

# SharePoint

前回と同じです。
データベースはSharePointリストです。
3つのテーブルを使います。
作業者名、業務内容はマスターテーブルを作成しシフト管理テーブルから参照します。
※お約束ですが列名は一旦英字で内部名を作成してから日本語にします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293667/f4f2489a-beb9-09fb-dad2-67670358e126.png)

実際の業務シフトテーブル
業務内容と作業者はデータ型を参照にしマスターから取得してあります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293667/9db11380-67df-1430-0ae4-f8fd7785c483.png)

# PowerApps

## 画面の概要
ギャラリーを3つ使用します。
カレンダーから表示期間を選択、ドロップダウンで業務内容を選択し、保存ボタンで保存です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293667/b1c15e16-9444-701d-7ddc-74c22214de5b.png)




## スクリーンの設定

OnVisibleやOnHiddenで画面で使用する変数を設定します。
_defaultDateと_defaultToggle変数は、OnChangeを発火させるために値を2度セットしています。(次で解説)

OnVisible：

```JavaScript:OnVisible
UpdateContext({
    _resetGallery:true,//ドロップダウンリストをリセットする変数
    _defaultDate:Blank(),//既定の日付：OnChange発火用
    _defaultToggle:true,//既定のトグル値：OnChange発火用
    _maxWeekday:If(Toggle_OnlyWeekday.Value = true,5,7)//平日のみ、土日の切り替え用
});
UpdateContext({
    _defaultDate:Today(),//既定の日付
    _defaultToggle:false//既定のトグル値
});
```

OnHidden：

```JavaScript:OnHidden
UpdateContext({_resetGallery:false})
```


## カレンダー表示部分
カレンダー部分の作成方法です。
前回とは少し異なります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293667/a400ec67-6040-e9f5-db1e-ffde8ad51d13.png)

### ①日付の選択コントロール

OnChangeで、コレクションに選択日から一週間分のシフトデータと、カレンダー用の日付が入るようにします。

これらのコレクションは、画面のOnVisibleやレコード保存後にも更新する必要があります。
今回はOnChangeを任意のタイミングで発火させる小技を使って、コードの記述をこの一箇所で済ませます。

DefaultDate：```_defaultDate```
OnChange：

```JavaScript:OnChange
If(!IsBlankOrError(Self.SelectedDate) && Self.SelectedDate <> _prevDate,
    //シフトデータを格納するコレクション
    ClearCollect(
        _weeklyTasks,
        Filter(
            SortByColumns(業務シフト管理テーブル ,"WorkingDate" ,Ascending),
            ThisRecord.作業日 >= Self.SelectedDate,
            ThisRecord.作業日 < DateAdd(Self.SelectedDate,11,Days)
        )
    );
    //カレンダー用のコレクション
    ClearCollect(
        _calenderLabelDates,
        FirstN(
            Filter(
                ForAll(
                    Sequence(11,0),
                    {Date:DateAdd(Self.SelectedDate,Value,Days)}
                ),
                Weekday(ThisRecord.Date,StartOfWeek.Monday) <= _maxWeekday
            ),
            7
        ) 
    )
);

//前回の日付を保持
UpdateContext({_prevDate:Self.SelectedDate});
```




- 先程のスクリーンのOnVisibleで_defaultDateを一旦Blankにして直後にToday()を設定しています。
- これによりOnVisible時に値が変更され、OnChangeイベントが実行されます。OnChangeが2回実行されますが、Blankのときはコードが実行されないよう、!IsBlankOrErrorで判定して分岐します。
→<font color="red">'21/5/18追記：</font>```OnChangeの実行条件に && Self.SelectedDate <> _prevDate```を追加しました。
UpdateContextとそれをトリガーにするOnChangeイベントの実行順序の問題で、最初に画面を開いたときを除き、OnChangeの実行判定```IsBlankOrError(Self.SelectedDate)```が効かず、OnChangeが2回実行されます。その対策用のコードです。　後述します。

- これで任意のタイミングでOnChangeを発火させコードの使いまわしが可能になります。※現状FireEventのような関数がないため。
- コレクション_weeklyTasksには、指定日から1週間分でフィルターしてデータを格納します。
- コレクション_calenderLabelDatesには、指定日から1週間分の日付をセットします。<br>
※Sequence(11・・は、土曜から平日のみ7日分表示したい場合、11日分が最大となるためです。


### ②トグル(平日のみ切り替え)
切り替えコントロールを配置します。
FalseText、TrueText：```"平日のみ"```
OnChange：```UpdateContext({_maxWeekday:If(Self.Value = true,5,7)});```
Default：```_defaultToggle```

### ③カレンダー用ギャラリー
横方向のギャラリーを追加します。
日付選択のOnChangeで作ったコレクションをセットします。
Items：```_calenderLabelDates```

ラベルを一つ配置し、
Text：```Substitute(Text(ThisItem.Date,"[$-ja]mm/dd(dddd)"),"曜日","")```


## シフト表示ギャラリー部分
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293667/23294a0a-172d-5cb5-54c4-4b99fdc30feb.png)

### ①親ギャラリーの設定
単純です。縦方向のギャラリーを配置します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293667/313b01af-1262-b8d3-8432-d7486aedfbe9.png)

**ギャラリー**
Items：作業者マスターテーブル

**ギャラリー内のコントロール**
テキストラベルを2つ配置し以下を設定。

一つ目のText：```Thisitem.作業者名```
二つ目のText：```CountRows(Gallery_TasksByWorker.AllItems)```

二つめのラベルは非表示でOKです。後で一括保存ボタンを動作させるために必要になります。

### ②入れ子ギャラリーの設定
親ギャラリーの中に横方向のギャラリーを配置します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293667/9fa0a1b8-4006-3820-58d9-faecc12eeffb.png)

ギャラリーのItemsは以下です。
先程のカレンダーギャラリーのレコードにAddColumnsで**レコードID**と**業務内容**の列を加えます。
値は_weeklyTasksコレクションから日付と作業者名で検索して取得します。

```JavaScript:Items
AddColumns(
    _calenderLabelDates As Dates,
    "RecordId",LookUp(_weeklyTasks,ThisRecord.作業日 = Dates.Date && ThisRecord.作業者.Value = ThisItem.作業者名).ID,
    "WorkType",LookUp(_weeklyTasks,ThisRecord.作業日 = Dates.Date && ThisRecord.作業者.Value = ThisItem.作業者名).業務内容
)
```

※ThisRecordはLookUp関数のレコードを参照しており、ThisItemはギャラリーのアイテムを参照しています。

次に、ギャラリーの中には以下のコントロールを配置します。
１．ドロップダウン：作業内容表示・編集用
２．チェックボックス：編集・更新判定用(変更するとTrueに)
３．ラベル：未保存表示用
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293667/720bab2f-ae06-6bef-e067-743e636cdf3b.png)

**１．ドロップダウン**
作業内容を表示します。
Items：```作業内容マスター```
Value：```業務内容```
Default：```ThisItem.WorkType.Value```
AllowEmptySelection：```True```
Reset：```_ResetGallery```
Resetに設定した変数は外部からコントロールをリセットするために必要です。変数をtrueにするとリセットできます。

**２．チェックボックス**
編集したセルかどうかを判定するために必要になります。
レコードの値とドロップダウンの選択値を比較して、異なる場合にチェックが入る(true)ようにします。
ユーザーから編集できないように設定します。

Defaults：```ThisItem.WorkType.Value <> Dropdown_WorkTypes.Selected.名前```
DisplayMode：```DisplayMode.Disabled```

**３．ラベル**
未保存データを表示するラベルです。
Text：```未保存```
Visible：```Checkbox_Unsaved.Value```(チェックボックスの値です。)

## 保存、更新ボタン
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293667/6c352ede-6e9f-bb74-2117-3753be846fe3.png)

### 保存ボタン
クリックしたとき、ギャラリーの中身を参照し未保存のチェックが入っているレコードのみ保存します。
その後、_defaultDate変数を変更して日付選択のOnChangeを発火させ、レコードを再読み込みさせます。

OnSelect：

```JavaScript:OnSelect
ForAll(
    Gallery_Parent_Workers.AllItems As A,
    ForAll(Filter(A.Gallery_TasksByWorker.AllItems,ThisRecord.Checkbox_Unsaved.Value = true) As B,
        With({
                currentRecord:If(IsBlankOrError(B.RecordId),Defaults(業務シフト管理テーブル),LookUp(業務シフト管理テーブル,ID = B.RecordId)),
                currentWork:B.Dropdown_WorkTypes.Selected
            },
            Patch(業務シフト管理テーブル,currentRecord,
                {
                    作業者:{Id:A.ID,Value:A.作業者名},
                    作業日:B.Date,
                    業務内容:{Id:currentWork.ID,Value:currentWork.名前}
                }
            )
        )
    )
);
With(
    {prevDate:DatePicker_StartDate.SelectedDate},
    UpdateContext({_defaultDate:Blank()});
    UpdateContext({_defaultDate:prevDate});
)

```

- 大体は前回と同じです。ForAllを入れ子にして子ギャラリーの中身を参照していきます。
- 普通にやると、子ギャラリーであるGallery_TasksByWorker.AllItemsは空となりこのコードは動きません。(バグか仕様か)
    - 正しく動かすには、親ギャラリーに設定したラベル：CountRows(Gallery_TasksByWorker.AllItems)が必要です。
    - 親ギャラリーから子ギャラリーへの参照があることでForAllで回したときに参照先が正しく修正される？
    - 参考：https://powerusers.microsoft.com/t5/Building-Power-Apps/ForAll-on-Nested-Gallery/m-p/206310#M66144
- 一時変数のCurrentRecordは、後のPatch関数の第二引数となる部分で、RecordIdにIDがあるかどうかで新規・変更を判定してレコードを代入します。
- Patch関数で業務シフト管理テーブルのレコードを更新します。参照型の列は{Id:参照先ID, Value:表示値}で設定します。
- 最後に日付選択コントロールのdefaultを更新してOnChangeを発火させます。
    - defaultを更新した後Resetを実行していませんが、仕様なのかdefaultを変更するだけでいいようです。

### 更新ボタン
日付選択のOnChange発火は保存ボタンと同じです。
With関数で一時変数prevDateに現在の値を保持しておき、Blank→prevDateと変更します。

```JavaScript:OnSelect
Refresh(業務シフト管理テーブル);
With(
    {prevDate:DatePicker_StartDate.SelectedDate},
    UpdateContext({_defaultDate:Blank()});
    UpdateContext({_defaultDate:prevDate});
)
```


# PowerAppsのイベント実行順序の問題

発生する状況と問題点

①まず画面のOnVisibleで_defaultDateを2回書き換えています。

```JavaScript:OnVisible
UpdateContext({_defaultDate:Blank()});
UpdateContext({_defaultDate:Today()});
```

②DatePickerのDefaultとOnChangeは以下のように設定されています。
Default：```_defaultDate```
OnChange：```If(!IsBlankOrError(Self.SelectedDate),/*レコードを取得するコード*/)```

③これにより、画面を開いたとき期待される実行順序は以下です。
・OnVisible実行→_defaultDate:Blank()→OnChange実行→if条件がfalseに。
・_defaultDate:Today()→OnChange実行→if条件がtrueに→レコードを取得。

**④実際の動作順序は以下の通り、OnChange内if条件が2回ともTrueとなります。**
・OnVisible実行→_defaultDate:Blank()→_defaultDate:Today()が実行。(この間OnChangeは実行されない)
・OnChange実行→if条件がtrueに(_defaultDateはToday)→レコードを取得、OnChange実行→if条件がtrueに(_defaultDateはToday)→レコードを取得

つまり、イベント内の数式をトリガーに別のイベントが実行される場合、即時実行されずキューに格納。その他の数式が全て評価された後、最後にキューが実行される、ようです。
※ただし初めて_defaultDateにBlank()を設定するとき＝DatePickerに初めて値を設定するときはOnChangeは動かないため、最初に画面を開いたときはUpdateContextを2回実行しても1回しかOnChangeが動きません。


この挙動について検証、解説されている方がいました。
https://koruneko.hatenablog.com/entry/2021/05/05/053259

> このことから、まず対象コントロールの動作プロパティないの式が評価されてから、その式によって生じた他コントロールの動作が順次動作されていくのではないか？と私は考えました。