PowerAppsで業務シフト管理アプリを作成

#概要
- 現場作業員向けに業務管理アプリを作りたいとの要望があったため作ってみました。
- Microsoft365でのシフト管理は、既に[Teamsに統合されたShift](https://support.microsoft.com/ja-jp/office/%E3%82%B7%E3%83%95%E3%83%88%E3%81%AE%E4%BD%BF%E7%94%A8%E3%82%92%E9%96%8B%E5%A7%8B%E3%81%99%E3%82%8B-5f3e30d8-1821-4904-be26-c3cd25a497d6)と呼ばれる優れたサービスがあります。
- ただしこれは全員がPCやM365アカウントを保有していることが前提であり、製造現場などPCのない人が多くいる現場では使えません。<br>

→[業務シフト管理アプリの改良版はこちらです。](https://qiita.com/Rambosan/items/cacac6a1067bd17945a5)


#アプリの仕様
左側に作業者リスト、上に一週間分の日付、その交わるセルに業務内容があります。
ドロップダウンで業務内容を変更して保存可能です。
調べたら既に作成されている方がいましたが、作り方が異なったため記事にしてみました。
![Animation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293667/d602db70-7ec6-60d8-a209-4d624b9da63e.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)

#アプリ作成方法

##シフト入力画面
メインとなる作業者ごと、日付ごとの業務を表示・編集する画面です。
以下のようにギャラリーを4つ使います。
ポイントとしては、作業者と日付が交わるセルでLookUpを使って一つずつデータを取るのではなく、入れ子ギャラリーで作業者ごとのデータを取ってきておき、そこに対してLookUpすることでデータ取得を効率化します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293667/86b653ae-16d4-6b3e-f18c-7135a797445f.png)

###①カレンダー部分
カレンダーの表示は横方向のギャラリーコントロールです。
日付の選択コントロールと、切り替えコントロールを配置し、その値を元にカレンダーを表示します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293667/a84b05f7-96f4-5cb4-7c50-9a968d3aab9e.png)

まず切り替えコントロールのOnChangeには```UpdateContext({_FilterdWeekday:If(Self.Value = true,6,8)})```を設定します。
また、画面のOnVisibleにも```UpdateContext({_FilterdWeekday:If(Toggle_OnlyWeekday.Value = true,6,8)})```を入れておきます。

次にギャラリーのItemsは以下のように設定します。
・カレンダーコントロールで表示した日付から7日間がコレクションとして入るようにします。
・「平日のみ」にチェックが入っているときは、月曜始まりのWeekdayが6未満(月～金)のデータにフィルターし、7個分データを取り出します。

```JavaScript:Items
FirstN(
    Filter(
        ForAll(
            Sequence(11,0),
            {Date:DateAdd(DatePicker_StartDate.SelectedDate,Value,Days)}
        ),
        Weekday(ThisRecord.Date,StartOfWeek.Monday) < _FilterdWeekday
    ),
    7
)
```

そして、ギャラリー内にラベルを一つ配置しTextプロパティで日付を表示します。

```JavaScript:Text
Substitute(Text(ThisItem.Date,"[$-ja]mm/dd(dddd)"),"曜日","")
```

###②親ギャラリー部分
ここは単純です。縦方向のギャラリーを配置します。
Items：作業者マスターテーブル
ラベルのText：Thisitem.作業者名

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293667/0445f7cf-b982-9223-2fd5-68acc15290b0.png)

###③入れ子ギャラリー(データの箱用)
横方向のギャラリーを入れ子で配置します。
データの箱にしか使いませんのでこのギャラリーは非表示でOKです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293667/cda5676a-0646-1006-be2a-c3f1c3990b35.png)

ギャラリーのItemsは以下です。
まず作業日で降順にし「作業者が行の値と一致しているか」、と「カレンダーで指定した日付以降か」の複数条件でフィルターします。
ギャラリーコントロールはスクロールしない場合先頭100件しか取得されませんが、作業者ごとに1週間分しか必要ないため問題はこれでOKですｗ

```JavaScript:Items
Filter(
    SortByColumns(業務シフト管理テーブル ,"WorkingDate" ,Ascending),
    作業者.Value = ThisItem.作業者名,
    ThisRecord.作業日 >= DatePicker_StartDate.SelectedDate
)

```
SortByColumnsのWorkingDateは内部名です。Sort関数は内部名でしか指定できないようです。


####補足
この後の「作業内容」となる「作業者と日付が交わるセル」の値は、LookUp関数により「日付=日付 && 作業者=作業者」の複数条件で検索し最初の一レコードを取得することで表示します。
ただし、これをデータソース対して直接やると多数のAPIコールが発生してしまいます。
そこで、この入れ子ギャラリー③でまず作業者ごとのデータをまとめて取得しておき、ギャラリー④ではここに対してLookUpします。
今回の場合は作業者が3人なので、3回分のAPIコールで済みます。

~~現状、Collection[i]のようにインデックスでアクセスできる変数がないのでギャラリーを介するしかありません。~~
→すみません、親ギャラリーのItemsにAddColumnsで列を追加してデータ格納すればOKでした。
ただしギャラリーを介さない場合、編集途中でレコード更新したり作業者ごとの保存ボタンを押すとデータ更新されドロップダウンの選択がリセットされます。


###④入れ子ギャラリー(データ表示・編集用)
こちらも横方向のギャラリーを配置します。

ギャラリーのItemsは以下です。
先程のカレンダーギャラリーのレコードに加え、AddColumnsで**レコードID**と**業務内容**の列を加えます。
ここのLookUpですが、既に取得済みの入れ子ギャラリー③に対してLookUpします。
既に作業者ごとに絞り込んであるため、ここでは日付の比較だけでOKなのです。

```JavaScript:Items
AddColumns(
    FirstN(
        Filter(
            ForAll(
                Sequence(11,0),
                {Date:DateAdd(DatePicker_StartDate.SelectedDate,Value,Days)}
            ),
            Weekday(ThisRecord.Date,StartOfWeek.Monday) < _FilterdWeekday
        ),
        7
    ) As Dates,
    "RecordId",LookUp(Gallery_WorkersTaskBase.AllItems,作業日 = Dates.Date).ID,
    "WorkType",LookUp(Gallery_WorkersTaskBase.AllItems,作業日 = Dates.Date).業務内容
)
```

####補足
関数がカレンダー部分と重複するため冗長です。
気になる場合は、日付選択のOnChangeでコレクションに代入するやり方か、実験的な機能、[強化されたコンポーネントプロパティ](https://powerapps.microsoft.com/ja-jp/blog/enhanced-component-properties/)を使用してカレンダー生成関数を自作します。


次に、ギャラリーの中には以下のコントロールを配置します。
１．ドロップダウン：作業内容表示・編集用
２．ラベル：ID表示用
３．チェックボックス：編集・更新判定用(変更するとTrueに)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293667/f577e83a-a998-bf17-f3e2-49d5e0941b63.png)

**１．ドロップダウン**
作業内容を表示します。
Items：```作業内容マスター```
Default：```ThisItem.WorkType.Value```
AllowEmptySelection：```True```
Reset:_ResetGallery
Resetに設定した変数は外部からコントロールをリセットするために必要です。変数をtrueにするとリセットできます。
<font color="Red">'21/4/29追記：ドロップダウンはAllowEmptySelectionをTrueにしてあげないと、空白を表示できませ。</font>


**２．ID表示ラベル**
IDを表示し、「IDが空白の場合は新規」、「IDがある場合は編集」といったように区別できるようにします。
Text：```ThisItem.RecordId```

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

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

###保存ボタン
仕様上、保存は作業者ごとに保存する形です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/293667/940708c0-315d-563e-e948-fd838a6e5d32.png)

Visible、編集したレコード(チェックが入っているセル)がある場合だけ表示されるようにします。

```JavaScript:Visible
If(CountRows(Filter(Gallery_WorkerTasks.AllItems,ThisRecord.Checkbox_UnSave.Value = true))>0,true,false)
```

OnSelect、やや長めです。

```JavaScript:OnSelect
ForAll(Filter(Gallery_TasksByWorker.AllItems,Checkbox_Unsaved.Value = true),
    With({
            CurrentRecord:
                //新規・編集の判定
                If(IsBlankOrError(ThisRecord.RecordId),
                    Defaults(業務シフト管理テーブル),
                    With({thisID:Value(ThisRecord.RecordId)},
                        LookUp(業務シフト管理テーブル,ThisRecord.ID = thisID)
                    )
                ),
            _selectedWork:Dropdown_WorkTypes.Selected
        },
        Patch(業務シフト管理テーブル ,CurrentRecord,
            {
                作業者:{Id:ThisItem.ID,Value:ThisItem.作業者名},
                作業日:Date,
                業務内容:{Id:_selectedWork.ID,Value:_selectedWork.作業内容}
            }
        );
    )
)
```

- まずFilterで、ギャラリーの中からチェックが入っているレコード(編集したもの)を絞り込み、ForAllで回します。
- 一時変数のCurrentRecordは、後のPatch関数の第二引数となる部分で、Label_TaskIdにIDがあるかどうかで新規・変更を判定してレコードを代入します。
- Patch関数でレコードを更新します。
- 参照型のフィールドは{Id:参照先ID, Value:表示値}のスキーマで指定。
- 選択肢型のフィールドは、{Value:選択値}で指定します。

※@Odata.typeなんたら～は入れなくてよくなった？？

###画面の設定
業務内容を選択するドロップダウンリストですが、画面を移動してもリセットされないため変数によってリセットされるようにします。

ScreenのOnHidden、OnVisibleに以下を設定します。
OnHidden：```UpdateContext({_ResetGallery:true})```、OnVisible：```UpdateContext({_ResetGallery:false})```を設定して、
※普通にReset(コントロール)としても外部からは実行できないので、コントロールのResetプロパティに変数を設定して、その値を切り替えることでリセットします。


##確認
保存ボタンを押したときに、レコードが更新されれば成功です。



##その他

**１．作業内容がBlankに設定できない問題。**
作業内容ですが、一旦設定すると参照型のためBlankにできません。

ドロップダウンリストの上ではブランクにできるのですが、Patchで業務内容:{Id:Blank(),Value:Blank()}みたいにしても駄目でした。
現状、参照型の場合PowerAppsからBlankにはできないようです。対応方法としては、①業務内容マスターに空白アイテムを入れる。②参照型にせず選択肢でやる。が考えられます。

**２．一括保存ボタンが動かない問題(入れ子ギャラリーに対しての
ForAllが動かない問題)**

~~保存ボタンですが、ForAllを入れ子にして一ボタンで全レコード保存する方法も試しましたがうまくいきませんでした。~~<br>

普通にやると以下のコードでは保存されません。
入れ子ギャラリーであるGallery_TasksByWorker.AllItemsが空となり実行されません。
試しにClearCollect(DebugList,First(Gallery_ParentWorkers).Gallery_TasksByWorker.AllItems)としてコレクションに入れてみても、列名しか取得できていません。バグでしょうか？

```JavaScript:OnSelect

ForAll(
    Gallery_ParentWorkers.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.名前}
                }
            )
        )
    )
)
```
回避方法ですが、親ギャラリーからGallery_TasksByWorker.AllItemsに対する参照を一つでも行っていれば動くようになります。
例えば、親ギャラリーにラベルを配置しText：CountRows(Gallery_TasksByWorker.AllItems)を追加すれば動くようになります。
※今回の例だと、作業者ごとの保存ボタンでAllItemsを参照しているのでOKです。

参考：
https://powerusers.microsoft.com/t5/Building-Power-Apps/ForAll-on-Nested-Gallery/m-p/206310#M66144

#あとがき
- ピボット形式で入力するならExcelでやってExcelで見れば、という考えもありますが、PowerAppsでやって現場で見れればカッコいいですよね。