文化祭やサークルのシフトを自動で割り振るWebアプリ「シフっと」の、前回の記事からの更新分を書きます。
アプリ全体の話は1本目、割り当てソルバーの話は2本目に書きました。今回はその後の変更のうち、書き残しておきたいものを3つに絞ります。
今回やったこと
| 変更 | 概要 |
|---|---|
| 回答履歴に変更内容を表示 | 「何が変わったか」を○△×の差分で見られる |
| 役割の追加・名前変更・削除 | イベント作成後に役割を編集できる |
| シフト表からの共有ボタン | LINEとXで、アプリを人に紹介できる |
ほかに、開発者ページ、お問い合わせ、OGPなど、サイトとしての体裁も整えました。ここでは触れません。
回答履歴に、変更内容を表示する
幹事は、参加者の回答を代わりに編集できます。編集するたびに履歴が残り、「この時点に戻す」で復元できます。
ただ、履歴に出ていたのは「回答を編集」のような要約だけでした。復元していいのかを決めるには、何が変わったのかを見たいところです。「この時点に戻す」を押すのが怖い、という状態でした。
履歴には変更前しか入っていない
履歴は、変更前のスナップショットだけを持っています。
type AvailabilitySnapshot = {
displayName: string;
maxSlots: number | null;
disabledRoleIds: string[];
answers: { slotId: string; value: AvailabilityValue }[];
};
変更後の状態は持っていないので、差分を出すには「変更後」を別の場所から持ってくる必要があります。履歴は新しい順に並んでいるので、次のように考えました。
- いちばん新しい履歴の「変更後」は、現在の状態
- それより古い履歴の「変更後」は、1つ新しい履歴の「変更前」
つまり、現在の状態から出発して、新しい方から順にたどればよいことになります。
let after = 現在の状態;
for (const h of participantHistory) { // 新しい順
if (!h.snapshotBefore) continue;
out.set(h.id, diffSnapshots(h.snapshotBefore, after, ...));
after = h.snapshotBefore; // 1つ古い履歴の「変更後」になる
}
履歴の型を変えて「変更後」も持たせる手もありました。ただ、すでに保存されている履歴が読めなくなるので、やめています。
差分の出し方
名前、最大枠数、できない役割、枠ごとの回答を比べます。表示はこうなります。
・最大枠数: 未設定 → 4
・できない役割に追加: 調理
・10/12 10:00〜11:00: ○ → ×
枠の回答は、いま存在する枠だけを比べています。履歴に残っていても、すでに削除された枠は表示のしようがないからです。
重くしない
差分は、履歴を開いている人の分だけ計算します。全員分を毎回計算すると、参加者が多いイベントで重くなるからです。
変更が4件以上あるときは「詳細(N件)」に折りたたみ、3件までは最初から展開します。これは、履歴を何件も並べたときに、画面が差分だらけになったので入れました。
役割を、あとから編集できるようにする
これまで、役割はイベント作成時に決めたきりで、あとから変えられませんでした。「受付」を「受付・案内」にしたい、あとから「呼び込み」を足したい、という声が、使ってみた人から出ていました。
削除は連鎖する
追加と名前変更は単純です。難しいのは削除でした。役割は、次のような場所から参照されています。
- 枠ごとの必要人数
- 割り当て済みのシフト
- 参加者ごとの「できない役割」
これらを全部そろえて消さないと、存在しない役割を指すデータが残ります。
bundle.roles = bundle.roles.filter((r) => r.id !== roleId);
bundle.roles.forEach((r, i) => (r.sortOrder = i));
bundle.requirements = bundle.requirements.filter((q) => q.roleId !== roleId);
bundle.assignments = bundle.assignments.filter((a) => a.roleId !== roleId);
for (const p of bundle.participants) {
p.disabledRoleIds = p.disabledRoleIds.filter((id) => id !== roleId);
}
イベントを1行のJSONで持っているので(1本目に書いた構成です)、これは1回の書き込みで済みます。正規化していたら、トランザクションを考える場面でした。
古い画面からの送信を弾く
役割を消せるようになると、別の問題が出ます。幹事が役割を削除したあとに、古い画面を開いたままの人が送信してくる、ということが起こります。
- 参加者の回答に、削除済みの役割が「できない」として入っている
- 幹事の別タブから、削除済みの役割の必要人数や手動割り当てが送られてくる
対策は、サーバー側で存在確認をすることです。
- 回答の
disabledRoleIdsは、存在する役割だけに絞って保存する(エラーにはしない) - 必要人数の更新と手動割り当ては、存在しない枠・役割ならエラーにする
前者を黙って絞るのは、参加者に原因が分からないエラーを見せたくなかったからです。履歴の復元でも同じ値が入りうるので、同じ処理を通しています。
制限
- 確定済みのイベントでは、役割を変更できません(確定を取り消せば変更できます)
- 同じ名前の役割は作れません
- 最後の1つは削除できません
- 追加した役割の必要人数は、全作業枠で同じ値になります。枠ごとに変えたいときは、あとから必要人数の画面で直します
シフト表の下に、共有ボタンを置く
参加者がシフト表を見終わったところに、「LINEで送る」「Xで投稿」のボタンを足しました。参加者の多くは、アプリを知らないまま幹事に言われたURLを開いた人です。そこで「便利だった」と思ってもらえれば、次の幹事になるかもしれません。
気をつけたのは、共有するURLです。
// 共有するのはサイトのURL。シフト表のURLは参加者限定なので載せない
const shareText = encodeURIComponent(`${文言} ${SITE_URL}${localePath(lang, "/")}`);
現在のページのURLをそのまま共有すると、イベントのシフト表が外に出てしまいます。このアプリは、URLを知っている人だけが見られる作りなので、共有するのはトップページだけにしています。
まだできていないこと
- 自動テストがまだありません。2本目の記事にも書きましたが、前回から増えていません。役割の削除のような「連鎖して消す」処理こそ、先に書くべきでした
- 役割の削除で割り当てが消えるとき、確認ダイアログの文言が十分か、自信がありません
- 履歴の差分は、削除された枠の分を出せません
触ってもらえると助かります
ログイン不要で、無料です。
「ここが分かりにくい」「こういう機能がほしい」といった指摘は、コメントでお待ちしています。