きっかけ:静かなオフィス、荒ぶる全社会議の日
普段のオフィスは、正直がらんどう。
固定席もないし、来客も少ない。
「今日はどこ座ろうかな」くらいの、のどかな世界。
ところが全社会議の日。
リモート勤務のメンバーが一斉に出社し、
開場と同時に始まる椅子取り合戦。
-
電源が近い席は即消滅
-
画面が見やすい席も即消滅
-
出遅れた人は「空いてるけど微妙」な席へ…
誰が悪いわけでもないけど、
なんとなく殺伐とするこの空気、どうにかしたい。
「だったら予約制にすればいいのでは?」
→ でも専用ツールを入れるほどでもない
→ じゃあ Power Platformで作ってみるか
という流れで、座席予約アプリを作ることにしました。
めざせマルス!(いやいやそこまでは目指しません、あしからず。)
ゴール設定(あくまで控えめに)
最初から立派なものは目指さず、ゴールはこれだけ。
-
スマホ・PCから座席を予約できる
-
誰がどこを使うか“なんとなく”見える
-
管理者が死なない(運用が楽)
優雅なオフィス改革とか言いつつ、
実態は「当日の無言の圧を減らしたい」だけです。
全体構成:Power Platform縛り
使ったのはこれだけ。
-
SharePoint List:データ置き場
-
Power Apps(Canvas):予約画面
-
Power Automate:通知用(おまけ)
Dataverseも使わず、
「Microsoft 365契約があればだいたいある構成」です。
データ設計(まずは素直に)
座席マスタ(Seats)
| 列名 | 型 | 内容 |
|---|---|---|
| Title | 1行テキスト | 座席名(A-1など) |
| Area | 選択肢 | エリア |
| HasPower | Yes/No | 電源あり? |
予約テーブル(Reservations)
| 列名 | 型 | 内容 |
|---|---|---|
| Seat | 参照 | Seats |
| ReservedBy | ユーザー | 予約者 |
| ReserveDate | 日付 | 利用日 |
この時点では
「時間帯」は割り切って入れませんでした。
午前午後で揉める未来が見えたので…。
Power Apps:画面は3つだけ
① 座席一覧画面
ギャラリーに Seats を並べます。
Seats
予約済みかどうかは、
その日の予約が存在するかで判定。
If(
CountRows(
Filter(
Reservations,
Seat.Id = ThisItem.ID &&
ReserveDate = Today()
)
) > 0,
RGBA(200,200,200,1),
RGBA(255,255,255,1)
)
グレー=埋まってる
これだけでだいぶ平和になります。
② 予約ボタン
空いている席だけ予約可能に。
If(
CountRows(
Filter(
Reservations,
Seat.Id = ThisItem.ID &&
ReserveDate = Today()
)
) = 0,
Patch(
Reservations,
Defaults(Reservations),
{
Seat: ThisItem,
ReservedBy: User(),
ReserveDate: Today()
}
)
)
押せたら予約完了。
エラーハンドリング? 最初はありませんでした。
③ 自分の予約確認画面
Filter(
Reservations,
ReservedBy.Email = User().Email &&
ReserveDate = Today()
)
「今日はここ座ります」が一目で分かるだけで、
朝の精神的コストが激減しました。
Power Automate(おまけ)
予約完了時に Teams に通知。
-
トリガー:SharePoint「アイテム作成時」
-
アクション:Teams に投稿
正直、これはなくても成立します。
でも「予約した感」が出てちょっと楽しい。
実際に使ってみて良かった点
-
朝の無言の圧が消えた
-
席を探してウロウロする人が減った
-
「あ、そこ◯◯さんなんだ」が分かる安心感
殺伐さはかなり軽減されました。
オフィス、ちょっとだけ優雅。
反省点・ハマりどころ(重要)
① 同時押し問題
ほぼ同時に2人が予約 → 両方成功するケースあり。
さすがに放置できず、疑似排他制御を入れた
最初に作った版では、
ほぼ同時に2人が「予約」ボタンを押すと、両方成功してしまうという問題がありました。
デモなら笑って済むけど、
実運用ではさすがにまずい。
とはいえ、
-
Dataverseのトランザクション
-
Azure Functions
-
本格的なロック機構
…を持ち出すほどでもない。
そこで今回は
Power Apps × SharePointでできる範囲の疑似排他制御
という割り切った方法を使いました。
考え方:予約前に「仮押さえ」フラグを立てる
やったことはシンプルです。
-
座席マスタに「ロック用の列」を追加
-
予約処理の最初でロックを取る
-
ロックが取れなければ予約中止
Seats に追加した列
| 列名 | 型 | 用途 |
|---|---|---|
| IsLocking | Yes/No | 予約処理中かどうか |
| LockTime | 日時 | ロック開始時刻 |
「誰が取ったか」までは持たず、
短時間だけロックする前提にしました。
実装①:ロック取得(最初の関門)
予約ボタンを押したら、まずこれを実行します。
If(
ThisItem.IsLocking,
Notify("他の人が予約処理中です。少し待ってください。", NotificationType.Error),
Patch(
Seats,
ThisItem,
{
IsLocking: true,
LockTime: Now()
}
)
)
ここでポイントなのは、
-
ロック取得だけを先にやる
-
予約登録はまだしない
という点。
これで「ほぼ同時押し」でも
後から来た人は弾かれるようになります。
実装②:予約登録(再チェック付き)
ロック取得後、あらためて予約済みチェック。
If(
CountRows(
Filter(
Reservations,
Seat.Id = ThisItem.ID &&
ReserveDate = Today()
)
) = 0,
Patch(
Reservations,
Defaults(Reservations),
{
Seat: ThisItem,
ReservedBy: User(),
ReserveDate: Today()
}
),
Notify("すでに予約されています。", NotificationType.Error)
)
ロックを取っていても油断しない、
というのが地味に大事でした。
実装③:ロック解除(忘れると事故る)
予約が成功しても失敗しても、
最後に必ずロック解除。
Patch(
Seats,
ThisItem,
{
IsLocking: false,
LockTime: Blank()
}
)
これを忘れると
永久に予約できない座席が誕生します。
実際、一度やらかしました。
念のため:ロックの自然死も入れておく
アプリが落ちた、通信が切れた、などを考慮して
「一定時間たったロックは無効」としました。
If(
ThisItem.IsLocking && DateDiff(ThisItem.LockTime, Now(), Seconds) > 30,
Patch(
Seats,
ThisItem,
{
IsLocking: false,
LockTime: Blank()
}
)
)
30秒は完全に感覚値ですが、
人が押して考えて戻る時間としては十分でした。
② 日付の扱い
Today() 基準だと、
-
夜中に翌日の予約ができない
-
タイムゾーンでズレる
という地味な罠。
後から「予約日選択」を追加しました。
③ 座席レイアウト欲が出る
途中から
「オフィス図面を背景に置きたい」
「色分けしたい」
と欲が暴走。
まずは一覧で十分だったな、という反省。
まとめ:完璧じゃないけど、今すぐできる
このアプリ、
-
設計はシンプル
-
コードも短い
-
失敗しても業務が止まらない
という、Power Platformの練習題材としてちょうどいい感じでした。
「オフィスの小さなストレス」を
大げさなDXにせず、
自分たちで雑に直す。
そんなノリでも、
意外と現場は救われます。
同じように椅子取り合戦で心をすり減らしている方の
参考になれば幸いです。