# 指示書BE — エラー一覧でエラー内容を選ぶ（画面）（2026-10-27）

**担当の目安**: Cursor でよい。ただし§5（3画面で共通の下書き）と§6（保存前の検証）は取りこぼしやすいので、
Grok / Composer で進めるなら、§9 の画面確認を省略しないこと。

**前提**: 指示書BD（API）の実装が develop に入っていること。
- `GET /api/error-lists`
- `ErrorReportItem` の `error_list_id` / `error_list` / `error_list_selectable`
- `PATCH /api/errors/{error_id}/status` の `error_list_id`

契約の正は `d-watch-api.yaml`。決定の記録は D-185（案は §11）。

---

## 0. ユーザーの要望と決定（2026-10-27）

- エラー情報を統計的に取るため、エラー一覧（`/errors`）でエラー内容を選択リストから選べるようにする
  - 選択肢は `error_lists`（決定稿 131件）
  - 分類（大分類。日本語は「清掃ロボットエラー」など5種）で絞れる
- 見た目の参考は https://copipe-de-ui.net/examples/data-list-2 。次の要素で、条件はすべて同時に効く（AND）:
  - キーワード入力
  - 分類の select
  - 件数「n件 / 全体」
  - 0件のときの文言
  - クリアボタン
- 保存するのは id（`error_list_id`）
- 選べる行の範囲:
  - 清掃開始失敗（`NOT_STARTED`）の行でも選べる
  - 制御盤（`D-S_CONTROL_FAILURE`）とセキュリティ（`SECURITY_ON`）の行では選べない
  - ★画面は `error_type` を見ない。`error_list_selectable` だけを見る★
- 「その他」（`note_required: true` の選択肢。いまは id 131）を選んだら、備考の入力を必須にする
- 未選択は空欄。★選んだあとに「未選択」へ戻す操作は作らない★（API が受け付けない）

## 1. 共通ルール（G1〜G4）

- **G1**: 文言を足したら `lib/i18n.ts` に JP と EN の両方を入れる
- **G2**: 触った箇所の古いコメントを直す
  - 例: `ErrorsScreen.tsx:45` の「新しい共通部品は作らない」。今回の選択用の窓は、この画面専用の部品として作る。共通部品にはしない
- **G3**: 素通りするテストを書かない
- **G4**: 完了時に `npx tsc --noEmit`・`npm run lint`・`npx vitest run` の件数と、§9 の画面確認の結果を報告する

## 2. 禁止・罠

- ★エラー内容の文字列（分類名・詳細）を `t()` に通さない★
  - 辞書に同じ綴りのキーがあり、誤訳される。例: 「該当するエラーはありません」「清掃中」「清掃キャンセル」「設定」（`lib/i18n.ts`）
  - JP / EN の切り替えは、`useTheme()` の `lang` を見て `_ja` / `_en` の列を出し分ける
- ★その他（id 131）のセルに詳細「該当するエラーはありません」を出さない★
  - 一覧が0件のときの文言（`ErrorsScreen.tsx:534`）と同じ文字列で紛らわしい
  - 英語の詳細は "There are no errors." で、意味も逆になる
  - `note_required` の選択肢は、セルに大分類名（「その他(該当なし)」/ "OTHER ERROR"）を出す
- ~~★列を足さない★~~ ★**2026-10-27 のユーザー指示で取り消し**★（エラー内容は「詳細」の列として備考の左に出す。§12）
  - ~~表の最小幅は 1130px（`ErrorsScreen.module.css:254`）。列を足すと、D-180 項10 で直した横スクロールが戻る~~
    → ★横スクロールを出さない条件（1440×900 で表の最小幅 ≦ 約1130px）は今も守る★。§12 の列幅で ≒1129px
  - ~~「種類」のセルの2行目に入れる。`COLUMN_COUNT = 8` と `<colgroup>` は変えない~~ → `COLUMN_COUNT = 9`
- ★セルの中にパネルを浮かせない★
  - 表は `.tableWrap { overflow-x: auto }` と `.card { overflow: hidden }` の中にあるので、浮かせたパネルは端で切れる
  - 選ぶ操作は窓（モーダル）で行う。作法は `components/features/detail-log/AreaModal.tsx`（`role="dialog"`・`aria-modal`・Escape で閉じる）
- アラート（`/alerts`）と D-Sec アラート（`/ds-alerts`）の画面から `error_list_id` を送る経路を作らない（§5）
- `git add -A` 禁止
- `docs/decisions.md` に他人の未コミット差分があれば、上書きせずに止まる

## 3. 表示（通常モード・編集モード共通）

「種類」のセルを2行にする。

- 1行目: `error_type`（今のまま。生値）
- 2行目: エラー内容。小さめ・薄めの文字、1行、はみ出しは「…」、全文は `title`

2行目に出す文字（★判定は1つの純粋関数にまとめ、テストする★。例 `lib/errors/error-content-label.ts`）:

| 行の状態 | 出す文字 | `title` |
|---|---|---|
| `error_list_selectable` が false | `-` | なし |
| `error_list_id` が null | 「未選択」（`t()` を通す。薄い色） | なし |
| `error_list_id` はあるが `error_list` が null | 「不明({id})」（`t()` を通し、`{id}` を置き換える） | なし |
| `error_list.note_required` が true | 大分類名（`error_category_main_ja` / `_en`） | 「大分類 / 小分類」 |
| それ以外 | 詳細（`error_detail_ja` / `_en`） | 「大分類 / 小分類 / 詳細」 |

- 編集モードで下書きがあるときは、下書きの id を `GET /error-lists` の選択肢から引いて同じ規則で出す
- 下書きの id が選択肢に無いことは起きない（窓で選んだ id だけが入る）

## 4. 編集モードの操作

- 選べる行（`error_list_selectable` が true）では、2行目を★固定幅のボタン★にする
  - ボタンの文字は §3 の表どおり。未選択なら「選択」
  - 押すと選択用の窓が開く
  - 保存中（`saving`）は押せない
- 選べない行は `-` のまま（ボタンを置かない）
- 選択肢（`GET /api/error-lists`）の取り方:
  - 編集モードに初めて入ったときに1回だけ取り、画面の state に持つ
  - 取得中は、ボタンを押せなくする
  - 失敗したら、編集中の案内（`.editNotice`）に「エラー内容の選択肢を読み込めませんでした」を出し、ボタンを押せなくする
  - 対応状況と備考の編集はそのまま使える
  - 取得の取り消しと、古い応答を捨てる処理は、この画面の既存の書き方（D-183）に合わせる

### 4-1. 選択用の窓（この画面専用の部品。例 `components/features/errors/ErrorContentPicker.tsx`）

**窓の作り**
- `role="dialog"`・`aria-modal="true"`・`aria-labelledby`（見出し「エラー内容を選ぶ」）
- 開いたらキーワード入力にフォーカスを移す。閉じたら、押したボタンにフォーカスを戻す
- Escape・背景のクリック・「閉じる」ボタンで、何も変えずに閉じる
  - 「閉じる」は、画面の「キャンセル」とは役目が違うので文言を分ける

**上段**
- 分類の select: 「すべての分類」と、選択肢の大分類を id 順に初めて出た順で並べる（`_ja` / `_en`。`t()` に通さない）
- キーワード入力: 部分一致・大文字小文字を区別しない
  - 対象は、今の言語の詳細・小分類・大分類と、もう一方の言語の詳細
- 件数: 「{n}件 / {total}件」
- クリアボタン: キーワードと分類を初期状態に戻す
- 上段の要素は固定幅にする（文言の長さで動かない）

**下段の一覧**
- 縦スクロールする固定の高さにする
- 見出しは「大分類 / 小分類」
  - 小分類の名前（データ・状態 など）は複数の大分類にまたがるので、小分類だけを見出しにしない
- 項目は `<button>`。文字は詳細（`_ja` / `_en`）
- `note_required` の項目には「備考必須」の小さな印を付ける
- 今の値（下書き優先）の項目は強調し、`aria-current="true"` を付ける
- 項目を押すと、下書きに id を入れて窓を閉じる
  - 行の値と同じ id を選んだら、下書きからその項目を消す（§5）
- 0件のときは「該当するエラー内容がありません」
  - 画面の「該当するエラーはありません」とは別の文言にする
- 並びは id 順（API の順のまま）

**表示の作法**
- テーマのトークン（ダーク／ライト）に合わせる
- 狭い画面では幅いっぱいにする
- 日本語フォントのスタックを外さない（`app/globals.css`）

## 5. 下書き（★3画面で共通の仕組みを壊さない★）

`lib/prechecks/edit-drafts.ts` は、エラー一覧・アラート・D-Sec アラートの3画面が使っている。期待値表は P / D / S / H。

- 共通側の `ResolutionDraft` と `patchRequestFromDraft()` の型と挙動は変えない
  - 返すのは `PrecheckStatusUpdateRequest` のまま
  - アラートと D-Sec が `error_list_id` を送らないことを、既存のテストが守っている
- エラー一覧用に `lib/errors/edit-drafts.ts` を作る
  - `ErrorResolutionDraft = ResolutionDraft & { error_list_id?: number }`
  - 正規化: 行の `error_list_id` と同じなら消す。項目が0になったらエントリごと消す
  - 本文を作る関数は `ErrorStatusUpdateRequest` を返す
    - 対応状況は必須（今の規則のまま）
    - 備考は変えたときだけ入れる
    - `error_list_id` も変えたときだけ入れる
  - ★エラー内容だけを変えた行も「変えた行」に数える★。保存（N件）の N に入り、送られること
  - 共通側の `saveDrafts()` は、型引数で下書きと本文の型を受け取れるように広げてよい
    - 3画面の挙動は変えない
    - 広げずに、エラー一覧用の関数を `lib/errors/edit-drafts.ts` に作ってもよい
- 期待値表 E を `lib/errors/__tests__/edit-drafts.test.ts` に作る。最低限、次の行を入れる:
  - エラー内容だけ変更
  - 行と同じ id に戻す（下書きが消える）
  - 対応状況と同時に変更
  - 備考と同時に変更
  - 下書きなし
- 変えた行の目印（`rowChanged`）は、エラー内容の変更でも付くこと

## 6. 保存前の検証（その他は備考必須）

- 「保存」を押したら、送る前に行ごとに確かめる。条件は次のすべて:
  - その行の下書きが★エラー内容か備考を変えている★（対応状況だけの変更には掛けない。サーバーの規則と同じ。指示書BD §3-3 の手順4）
  - その行の★適用後★のエラー内容（下書き優先）が `note_required`
  - ★適用後★の備考（下書き優先）を前後の空白を除くと空
- 当たった行は★送らない★
  - 下書きを残す
  - 行の直後のエラー（`rowErrors`）に「その他を選んだときは備考を入力してください」を出す
  - 「{n}件を保存できませんでした」の件数に入れる
  - ほかの行は送る（1行の失敗で止めない。今の規則のまま）
- 既にその他が入っている行で備考を消した場合も対象に入る（サーバーが 422「備考を入力してください」を返すため）
- ★既にその他で備考が空の行（アラート画面で備考を消した等）で、対応状況だけを変えた場合は送る★（サーバーも通す）
- 編集中の見た目:
  - `note_required` の行は、備考欄の placeholder を「備考を入力（必須）」にする
  - 空のあいだは枠を警告色にする
- サーバーの 422（どの理由でも）は、今のとおり行の直後に文言を出す。最後の砦として残る

## 7. 触るファイル（目安）

- `components/features/errors/ErrorsScreen.tsx` / `ErrorsScreen.module.css`
- 新規
  - `components/features/errors/ErrorContentPicker.tsx`（＋ CSS）
  - `lib/errors/edit-drafts.ts`
  - `lib/errors/error-content-label.ts`（§3）
  - それぞれのテスト
- `lib/prechecks/edit-drafts.ts`: 型引数を広げる場合だけ
- `lib/i18n.ts`
- `docs/code-map.md`（画面の表と「エラー一覧」の節）、`docs/spec.md`（該当の機能の行があれば）

## 8. テスト

- §3 の表のすべての行（JP / EN）
- §5 の期待値表 E と、既存の P / D / S / H が通ること
- §6:
  - その他＋備考なし → 送らない
  - その他＋空白だけ → 送らない
  - その他＋備考あり → 送る
  - 既にその他の行で備考を消す → 送らない
  - 既にその他で備考が空の行で、対応状況だけ変える → 送る
  - 当たった行があっても、ほかの行は送る
- キーワードと分類の絞り込みの純粋関数（AND・大文字小文字・0件・クリア）

## 9. 画面で確かめる（fixtures）

手順:
1. `USE_FIXTURES=true npm run build`
2. `USE_FIXTURES=true npx next start -p 3100`
3. アプリ内ブラウザで開く（Edge は使わない）

★:3000 のサーバーは止めない★。

fixtures の行:

| 行 | 期待 |
|---|---|
| FX-ERR-01 | 「非常停止スイッチが作動しました」 |
| FX-ERR-05 | 「不明(9999)」 |
| FX-ERR-11（清掃開始失敗・その他） | 「その他(該当なし)」 |
| `D-S_CONTROL_FAILURE` / `SECURITY_ON` の行 | `-` |
| ほか | 「未選択」 |

確かめること:
- 通常モードと編集モードの表示、JP / EN、ダーク／ライト
- 画面幅 1440px で横スクロールが出ないこと（いちばん長い英語の詳細を選んだ行でも）
- 窓の操作
  - キーボードだけで、開く → 絞る → 選ぶ → 閉じる（フォーカスが戻る）ができること
  - Escape で閉じること
- 保存の流れ
  - 有効な id を選んで保存すると、反映して通常モードに戻ること
  - その他を選んで備考を空のまま保存すると、送られずに行エラーが出ること
  - 備考を入れて保存し直すと、成功すること
- アラート・D-Sec アラートの画面の編集が今までどおり動くこと

ローカルDB（`dwatch_local`）で実DBモードを確かめるなら:
- 先に `scripts/local-db/` の手順で `error_lists` の表と `error_list_id` の列を入れる
- シードを入れ直す

## 10. 今回やらないこと

- 統計の画面・CSV 出力
- エラー内容での絞り込み
- 未選択に戻す操作
- アラート・D-Sec アラートの画面への追加

## 11. D-185（案）

- エラー一覧の「種類」のセルの2行目にエラー内容を出す（列は足さない）
- 編集モードでは、ボタン → 窓（キーワード・分類・件数・クリア）で選ぶ
- その他は、セルに大分類名を出す。保存前に備考が必須かを確かめる
- 文字列は `t()` に通さず、言語で列を出し分ける
- 却下案:
  - セルの中に浮かせるパネル: 表の端で切れる
  - 2段の `<select>` やグループ分けしたネイティブの select: キーワードで絞れない。分類で絞っても「清掃ロボット状態エラー」だけで51件残る
  - 新しい列: 横スクロールが戻る

## 12. 変更（2026-10-27 ユーザー指示）

- ★エラー内容は「種類」のセルの2行目ではなく、独立した列「詳細」（EN "Details"）として★**備考のすぐ左**★に出す★
  - 列の並び: 検知時刻・施設名・シリアル・種類・レベル・紐づく予定・対応状況・★詳細★・備考（`COLUMN_COUNT = 9`）
  - 「種類」のセルは `error_type` だけに戻す
  - 詳細のセルに出す文字・`title` は §3 の表のまま（`errorContentView()`。`t()` に通さない）。「-」「未選択」「不明(id)」は薄い色
  - 編集モードのボタン（§4）は詳細のセルに置く。★列幅いっぱい・左寄せ★。押したときの動き・フォーカスの戻し方は変えない
- ★長い文字は折り返す（「…」で切らない・1行に詰めない）★ — 通常モードも編集モードのボタンも同じ。
  英語や識別子のような長い綴りは `overflow-wrap: anywhere` で列の中で切る
  - §3 の「1行、はみ出しは「…」」と §4 の「固定幅のボタン」はこの指示で置き換え
- 列幅（`ErrorsScreen.module.css` の `.table`。値は言語によらず同じ。文字幅は 2026-10-27 にブラウザで測った値）:

  | 列 | 幅 | 旧 |
  |---|---|---|
  | 検知時刻 | 120px | 120px |
  | 施設名 | 表の 9%（最小幅のとき ≒102px） | 残りの半分（最小 150px） |
  | シリアル | 140px（「8111R5628060033」≒111px ＋ padding 24） | 160px |
  | 種類 | 表の 9%（最小幅のとき ≒102px） | 残りの半分（最小 150px） |
  | レベル | 90px | 90px |
  | 紐づく予定 | 110px（「事前チェック」/「（30分前）」の2行） | 150px |
  | 対応状況 | 150px（D-165。変えない） | 150px |
  | ★詳細★ | ★残りの幅すべて（最小幅のとき 196px 以上）★ | （なし） |
  | 備考 | 120px | 160px |

  - ★表の最小幅 = (固定列の合計 730 + 詳細の最小 196) ÷ (1 − 0.18) ≒ 1129px★（旧 1130px）。
    1440×900（縦スクロールバーありで表の入れ物 ≒1139px）で横スクロールが出ない（D-180 項10 を保つ）
  - 施設名・種類を割合にしたのは、広い画面で詳細だけでなく両列も少し広がるようにするため
    （却下: 3列で等分 → 狭い画面で詳細が ≒133px／詳細を 200px 固定 → 広い画面で種類の列だけが空いて広がる）
  - 施設名は最小幅のとき中が ≒78px なので、長い店名は2〜3行に折り返す
