はじめに
運用が始まって半年ほど経った業務アプリで、「対応中2」というステータスを見かけたことはないでしょうか。ステータス管理の画面は、作った直後より運用が進んでからボロが出ます。よくあるのは次のような症状です。
- 「対応中」と「対応中2」のように、意味の重なったステータスが増えていく
- 差戻し済みの申請なのに承認ボタンが押せてしまい、押すとエラーになる(またはそのまま通ってしまう)
- いつ誰が差し戻したのか、理由は何だったのかが画面のどこにも残っていない
症状はバラバラに見えますが、原因はどれも同じです。画面を作る前に「どの状態から、誰が、何をできるか」を決めていないことです。個々の症状をその場しのぎで直しても、決めごとがない限り同じ問題が別の場所で再発します。
この記事では、ステータス管理UIの設計を遷移可否マトリクスを起点に整理し、押せない操作の見せ方、権限による出し分け、履歴に残す項目、そしてステータスを増やす前の判断基準までを順に扱います。対象は画面側の設計に絞ります。データベースのステータスカラムの持ち方や、ステートマシンライブラリの使い方には踏み込みません。動くフル実装は承認フロー(ステータス遷移管理)の事例ページにあり、この記事の内容はすべてそのデモで確認できます。
遷移可否マトリクスの作り方
最初に作るのはコードではなく表です。行に現在の状態、列に遷移先の状態を並べ、遷移できるセルに操作名を書き込みます。題材は業務アプリで最も出番の多い承認フロー(下書き→申請中→承認済み/差戻しの4状態)を使います。
| 現在の状態\遷移先 | 下書き | 申請中 | 承認済み | 差戻し |
|---|---|---|---|---|
| 下書き | - | 申請する | × | × |
| 申請中 | 取り下げる | - | 承認する | 差し戻す |
| 承認済み | × | × | - | × |
| 差戻し | × | 再申請する | × | - |
この表の価値は、操作名が入るセルよりも「×」を明示的に決めるところにあります。表を埋めていくと、「承認済みからはどこへも遷移できない(終端の状態がある)」「差戻しから直接承認済みへは行けない(必ず再申請を挟む)」といった決めごとが漏れなく見えてきます。冒頭の症状2(差戻し済みなのに承認ボタンが押せる)は、この「×」を誰も決めていなかったことが原因です。ExcelでもホワイトボードでもかまわないのでUIを作る前にこの表を埋めておくと、実装もレビューもこの表を正として進められます。
マトリクスをそのまま実装する遷移マップ
マトリクスができたら、それをそのままJavaScriptのオブジェクトに書き写します。行がキーに、操作名が入ったセルがエントリになります。
// 遷移マップ: 現在の状態 → 許可される操作と遷移先
const transitions = {
draft: {
submit: { label: '申請する', to: 'pending' }
},
pending: {
approve: { label: '承認する', to: 'approved' },
reject: { label: '差し戻す', to: 'rejected' },
withdraw: { label: '取り下げる', to: 'draft' }
},
rejected: {
resubmit: { label: '再申請する', to: 'pending' }
},
approved: {} // 終端: 何も操作できない
};
画面側は、現在の状態をキーにこのマップを引いてボタンを生成し、クリックされたらtoの状態へ書き換えて再描画するだけです。「申請中なら承認ボタンを出す」「承認済みなら全部消す」といったif分岐を画面のあちこちに書かないのがポイントです。分岐を散在させると、状態を1つ追加したときにボタン表示・遷移処理・バッジ更新をそれぞれ直して回ることになり、修正漏れがそのまま冒頭の症状2になります。マップに集約しておけば追加は1エントリで済み、マトリクスとコードが1対1で対応しているのでレビューでも突き合わせが簡単です。
下のデモで、遷移マップだけからボタンが生成される様子を確認できます。ボタンを押して遷移させると、その状態から実行できる操作だけに表示が切り替わります。
ボタンを押すと状態が遷移し、遷移マップから次に実行できる操作だけが生成されます。承認済みまで進めると、操作が何も表示されないこと(終端)を確認できます。
(ここに遷移の記録が表示されます)
チェックを入れると、「取り下げる」の遷移先が下書きから新しい終端ステータス「取り下げ済み」に変わります。変更点はマップの1エントリと表示名・配色の追加だけで、ボタン生成のコードには手を入れていません。この追加コストの見え方は、後半の「ステータスを増やす前に問う3つの質問」で使います。
視点切替(申請者と承認者)や差戻しコメント、操作履歴まで含めたフル実装は承認フロー(ステータス遷移管理)にコピペで動く形で置いてあります。なお、一覧画面で複数行のステータスをまとめて変更する場合も、選択行それぞれにこの遷移マップで可否を判定する形になります。一括変更のUIはテーブル(選択行の一括ステータス変更)を参照してください。
押せない操作の見せ方(隠す・disabled・押させてエラー)
マトリクスで「できない操作」が決まったら、次はそれを画面でどう見せるかです。選択肢は3つあります。判断軸は1つで、「なぜ操作できないのかが、その画面の利用者に伝わるか」です。
| 方式 | 向いている場面 | 注意点 |
|---|---|---|
| 隠す(表示しない) | その利用者にとって操作自体が存在しない場合。承認者に「申請する」を見せる意味はない | 「あるはずのボタンがない」と迷わせる可能性がある。役割で操作が根本的に分かれるときに使う |
| disabledで見せる | 操作は存在するが、今は条件を満たしていない場合(入力不備・前工程の未完了など) | 押せない理由を近くに書かないと問い合わせになる。disabledのボタンはフォーカスが当たらないため、理由の表示をツールチップだけに頼らない |
| 押させてエラー表示 | 押せない条件が複雑で、理由をその場で具体的に説明したい場合 | 期待させてから断る形になるため、多用すると操作のたびにストレスになる |
下のデモは、「申請中」の案件に対する承認ボタン(承認者だけが実行できる操作)を、同じ条件のまま3方式で並べたものです。視点を切り替えて、それぞれの見え方を比べてください。
A. 隠す
(承認ボタンは表示されません)
B. disabledで見せる
承認は承認者のみ実行できます
C. 押させてエラー
承認する権限がありません
承認を実行しました(デモ)
3方式は排他ではなく、1つの画面の中で使い分けるものです。承認フロー(ステータス遷移管理)のデモでは「隠す」を採用しました。申請者と承認者で操作が根本的に分かれており、承認者の画面に灰色の「申請する」ボタンが並んでいても迷わせるだけだからです。一方、同じ承認画面でも「必須項目が未入力だから申請できない」のような一時的な条件であれば、disabledと理由の表示を組み合わせる方が親切です。「その操作がいずれ自分に関係するか」で隠すかdisabledかを選び、条件が複雑で説明が長くなるときだけエラー表示に頼る、という順で考えると迷いません。
権限による出し分けとサーバー側の検証
「誰ができるか」も遷移マップに含めてしまうのが実装上は簡単です。各操作に実行できる役割を書き足し、ボタン生成時に現在の視点でフィルタします。
pending: {
approve: { label: '承認する', to: 'approved', role: 'approver' },
reject: { label: '差し戻す', to: 'rejected', role: 'approver' },
withdraw: { label: '取り下げる', to: 'draft', role: 'applicant' }
}
// ボタン生成時に現在の役割でフィルタする
const actions = Object.values(transitions[status])
.filter(function (a) { return a.role === currentRole; });
ここで1つだけ原則があります。フロント側の出し分けはあくまで使い勝手のためのもので、権限と遷移可否の確定判断は必ずサーバー側でも行うことです。ボタンを隠してもdisabledにしても、開発者ツールを使えば操作は復元できますし、APIを直接叩くこともできます。承認APIの側で「このユーザーは承認者か」「対象は本当に申請中か」を検証していなければ、画面でどれだけ丁寧に出し分けても権限管理としては機能していません。金額計算をフロントの表示値だけで確定させてはいけないのと同じ構図です(請求金額の合計と消費税の丸め処理で扱った話と同型です)。画面側の遷移マップは「押せない操作を押させないUX」のため、サーバー側の検証は「不正な遷移を通さない防御」のためと、役割を分けて両方作ります。
履歴に残す項目
冒頭の症状3(いつ誰が差し戻したのか分からない)への答えが操作履歴です。設計の中心は見た目より「何を残すか」で、最低限そろえるのは次の4点です。
| 項目 | 内容 | 残っていないと起きること |
|---|---|---|
| 日時 | いつ操作されたか | 「先週の申請どうなった?」に答えられない |
| 操作者 | 誰が操作したか | 確認したい相手が分からず、関係者全員に聞いて回ることになる |
| 遷移の前後 | どの状態から、どの状態へ変わったか | 「差戻し」とだけ残っていても、どの段階の差戻しか追えない |
| コメント | なぜその操作をしたか | 差戻し理由がチャットや口頭にしか残らず、再申請時に確認できない |
とくに抜けやすいのがコメントです。差戻し理由がチャットにしか残っていない現場は珍しくありません。対策はシンプルで、差戻しのような理由が必要な操作では、遷移の操作と同じ場所でコメントを必須入力にすることです。承認フロー(ステータス遷移管理)のデモでも、差戻し時はコメントを入力しないと確定できない作りにしています。後から書いてもらう運用は、まず定着しません。
見せ方は、承認フローのように件数が限られるなら詳細画面の下部に並べる簡易リストで十分です。作業ログのように件数が多く時系列の流れ自体を見せたい場合は、タイムライン(作業ログ型)の形式が向いています。なお、履歴はサーバー側に記録することが前提です。画面のメモリ上にしか持っていない履歴は、リロードした時点で消えます。
ステータスを増やす前に問う3つの質問
最後に、冒頭の症状1(「対応中2」が生まれる)への向き合い方です。運用中のシステムでは「この案件は保留とも違うから、新しいステータスが欲しい」という要望が必ず来ます。そのまま受けてステータスを足し続けると、マトリクスの行が増えるたびに埋めるセルは2乗で増え、誰も全体を把握できなくなります。追加する前に、次の3つを確認してください。
- 遷移ルールが変わるか。既存のどれかの状態と同じ遷移しかしないなら、それはステータスではなく、タグや担当者欄などの別の項目で表せます。遷移が変わらない「対応中(急ぎ)」はステータスにしない
- その状態から終端に到達できるか。マトリクスに行を足したとき、承認済みのような完了状態への経路がない行き止まりの状態を作っていないか確認する
- 既存ステータスの細分化ではないか。「対応中2」の多くは、対応中という状態に担当者や工程の情報を重ねたいだけで、「対応中+工程」の2項目に分ければステータスは増やさずに済む
3つの質問を通ったら、堂々と追加します。遷移マップで実装していれば追加コストは小さく、たとえば「取り下げ済み」を独立した終端ステータスにする変更は、マトリクスの1行とマップの1エントリ、表示名と配色の追加だけで済みます(この記事の最初のデモのチェックボックスで、実際にその変更を体験できます)。「取り下げたものは下書きと違って再申請させない」のように遷移ルールが変わるなら独立したステータスにする価値があり、変わらないなら既存の状態に戻せば足りる。この判断も結局は質問1に帰着します。バッジの配色を増やす際の色の選び方はステータスバッジ(業務配色パターン)に体系化してあります。
まとめ
ステータス管理UIの設計は、画面のレイアウトからではなく遷移可否マトリクスから始めます。「×」まで含めて表を埋め、それをそのまま遷移マップとして実装すれば、ボタンの出し分けと遷移処理は1か所に集約されます。押せない操作は「なぜ操作できないかが伝わるか」で隠す・disabled・エラー表示を使い分け、権限の確定判断はフロントに頼らずサーバー側でも検証する。履歴は日時・操作者・遷移の前後・コメントの4点を操作と同時に記録し、ステータスの追加要望には3つの質問で応じる。ここまで決めてあれば、「対応中2」が生まれる前に設計の側で受け止められます。この記事の内容を1つにまとめた動く実装は承認フロー(ステータス遷移管理)にあります。コピペしてそのまま業務アプリの土台にしてください。
関連するUI事例
ステータス管理と関わりの深いUI事例です。いずれもコピペで動くサンプルを掲載しています。