担当: Cursor。監督(Claude Code)がレビューする。 前提: develop の 38e8f5d 以降(施設ロボットマスタの並べ替え=先行例が入っている版)。 先行例: 38e8f5d — lib/facilities/query.ts(FACILITY_SORT_KEYS・parseFacilityQuery() の sort/order・sortFacilities())、 components/features/facilities/FacilitiesScreen.tsx(nextSort()・sortableTh()・changeSort())、d-watch-api.yaml の /facilities の sort/order。 ★作業の前に必ず読むこと★。この指示書は「同じ形を2画面に広げる」もので、★先行例と違うところ(§2-1 の表)★に特に注意すること。
| 見出し | sort の値 |
1回目(▲ asc) |
2回目(▼ desc) |
下に固定する行 | 見出しのそろえ(今のまま) |
|---|---|---|---|---|---|
| 機種 | model_name |
文字の昇順 | 文字の降順 | model_name が null |
左 |
| シリアル | serial_number |
文字の昇順 | 文字の降順 | serial_number が null |
左 |
| 日付 | scheduled_at |
古い順 | ★無し(2状態)★ | 無し | 左 |
| 出発 | departed |
✗ が上 | ✓ が上 | 結果待ち | 中央 |
| 清掃結果 | task_status |
悪い順(§2-2 の順位) | 良い順 | 結果待ち | 左 |
| 完了率 | completion_rate |
低い順 | 高い順 | 完了率 null(出発失敗)→ その下に結果待ち | 右 |
| 判定 | grade |
× → △ → ○ → ◎ | ◎ → ○ → △ → × | 結果待ち | 中央 |
aria-sort="descending")。他の列で並べているあいだは日付は ⇅(★§12 で薄い △ に変更★)asc でも desc でも一番下★(向きで反転させない)。固定する行どうしは既定順CleanLogTable.tsx の見出しの textAlign を変えない(実物で確かめること。上の表と違ったら実物が正)| 見出し | sort の値 |
1回目(▲ asc) |
2回目(▼ desc) |
下に固定する行 |
|---|---|---|---|---|
| 検知時刻 | detected_at |
古い順 | ★無し(2状態)★ | 無し |
| シリアル | serial_number |
文字の昇順 | 文字の降順 | 無し(--- も値として並べる) |
| 種類 | error_type |
文字の昇順 | 文字の降順 | 無し |
| レベル | error_level |
文字の昇順(CRITICAL → ERROR → WARNING) | 文字の降順 | 無し |
| 対応状況 | resolution_status |
未解消 → 対応中 → 解消(DB の status 0→1→2) |
解消 → 対応中 → 未解消 | 無し |
| 備考 | note |
文字の昇順 | 文字の降順 | note が null・空文字・空白だけ |
| 今の状態 | 押した列 | 次の状態 |
|---|---|---|
| 既定 / 別の列で並べ中 | 普通の列 | その列の asc(▲) |
その列の asc |
同じ列 | その列の desc(▼) |
その列の desc |
同じ列 | 既定(sort を送らない) |
| 既定 / 別の列で並べ中 | 2状態の列(日付・検知時刻) | その列の asc(▲ 古い順) |
2状態の列の asc |
同じ列 | 既定 |
asc★(ユーザー指示「悪いほうが上」。完了率・判定・出発・清掃結果・対応状況は「asc = 悪いほう」になるよう §2 で定義してある)。 ★施設ロボットマスタは今のまま(1回目 desc = ○ が上)★ — 切り替えの部品を共通にしても、施設マスタの動きは変えないasc = ▲、desc = ▼、並べていない列 = 薄い ⇅(施設マスタと同じ)。★§12 で変更: 並べていない列は薄い △、印の色は白(ダーク)★setPage(1)★(useEffect で後追いしない)。queryString の useMemo の依存に sort を入れるdesc は主キーの比較だけを反転する★。 ★asc の結果を reverse() して desc を作らないこと★(同じ値の中まで逆になる。lib/facilities/query.ts の sortFacilities() の注記と同じ)compareCleanLogItems()(lib/db/clean-log-mapping.ts:394)compareErrorReportKeys()(lib/error-reports/query.ts:134)< / >(コードポイント順)★。localeCompare を使わない(lib/masters/query.ts:62-66 の方針。実行環境で順が変わる)。大文字小文字は区別する★★先行例(施設マスタ)と違うところ — ここを写し間違えると既定順が壊れる★★
| 施設マスタ(先行例) | 今回(両画面) | |||
|---|---|---|---|---|
sort を省略したとき |
"id" に正規化(既定値あり) |
★null★(型は `CleanLogSortKey \ |
null/ErrorReportSortKey \ |
null)。★既定値を"scheduled_at"/"detected_at"にしないこと★(orderの既定がasc` なので古い順になってしまう) |
order を省略したとき |
"desc" |
★"asc"★(1回目は悪いほうが上のため) |
||
sort が既定(null)のとき |
— | order は検証する(不正なら 422)が、並びには使わない |
||
2状態の列の desc(sort=scheduled_at&order=desc / sort=detected_at&order=desc) |
— | ★パースの段階で sort: null に畳む★(既定順と同じ意味。422 にしない。画面は送らない) |
||
sort が null のときの並べ替え関数 |
— | ★並べ替えず、入力の写し([...items])をそのまま返す★(§7 罠1。fixtures 版の既定順を変えない) |
||
| 型の名前 | FacilitySortOrder |
★TableSortOrder★(lib/table-sort.ts。Prisma の生成物に同名の SortOrder があるので使わない) |
departed false かつ result_pending false)= 0、✓ = 1。asc = ✗ が上。結果待ち(result_pending true)は下に固定asc = × が上。結果待ちは下に固定(grade は "×" のままなので ★result_pending を先に見る★)task_status)。asc = 順位の小さい順。結果待ちは下に固定:| 順位 | 行 | |---|---| | 1 | 出発失敗(task_status が null で結果待ちでない) | | 2 | 5 清掃異常終了 | | 3 | 2 清掃中断 | | 4 | 3 と 6 清掃キャンセル(★同じ順位★。表示名が同じため) | | 5 | 表に無い値(「不明(n)」。7 以上・負の値など) | | 6 | 0 清掃開始前 | | 7 | 1 清掃中 | | 8 | 4 清掃正常終了 |★この順位表は lib/config/index.ts の CLEAN_TASK_STATUS_LABELS の隣に定数で置く★(CLEAN_TASK_STATUS_SORT_RANK など)。 コードに数字を直書きしない。判定(◎○△×)には使わない(表示の並びだけ)completion_rate の生値。null(出発失敗)は下に固定、結果待ちはさらにその下scheduled_at(予定外の行は started_at が入っている。今の既定順と同じ値)。Date.parse の差で比べるstatus(0 未解消 / 1 対応中 / 2 解消)の数値。asc = 未解消が上sortCleanLogs() の純粋関数のテスト。行は CleanLogItem としてテストの中で作る。置き場所 lib/clean-logs/__tests__/sort.test.ts)| 行 | id |
row_key |
scheduled_at(JST) |
機種 | シリアル | 出発 | task_status |
完了率 | 判定 |
|---|---|---|---|---|---|---|---|---|---|
| R1 | 101 | tt-101 |
11-03 02:00 | CC1 | S-003 | ✓ | 4 | 95.0 | ◎ |
| R2 | 102 | tt-102 |
11-03 02:00 | CC1 | S-001 | ✓ | 2 | 45.0 | △ |
| R3 | 103 | tt-103 |
11-02 02:00 | MT1 | S-002 | ✗(出発失敗) | null | null | × |
| R4 | 104 | tt-104 |
11-03 03:00 | null | null | 結果待ち(result_pending true・departed false) |
null | null | ×(画面「-」) |
| R5 | ★null★ | rp-9001 |
11-02 21:00 | CC1 | S-004 | ✓ | 6 | 9.9 | × |
| R6 | 105 | tt-105 |
11-01 02:00 | CC1 | S-005 | ✓ | 7 | 89.5 | ○ |
| R7 | 106 | tt-106 |
11-01 02:00 | CC1 | S-006 | ✓ | 3 | 90.0 | ◎ |
| R8 | 107 | tt-107 |
11-02 01:00 | CC1 | S-007 | ✓ | 5 | 70.0 | ○ |
unscheduled true・id null・report_id あり)。ほかは予定のある行(unscheduled false)compareCleanLogItems()): R4, R1, R2, R5, R3, R8, R6, R7| # | 指定 | 期待する順 |
|---|---|---|
| S1-1 | sort null(入力は既定順 R4, R1, R2, R5, R3, R8, R6, R7) |
同じ順で返る |
| S1-1b | sort null(入力をわざと R8, R1, R7, R2, R6, R3, R5, R4 にする) |
★その順のまま返る(並べ直さない)★ |
| S1-2 | scheduled_at asc |
R6, R7, R8, R3, R5, R1, R2, R4(同じ時刻の R6・R7 と R1・R2 は既定順のまま) |
| S1-4 | completion_rate asc |
R5, R2, R8, R6, R7, R1, R3, R4 |
| S1-5 | completion_rate desc |
R1, R7, R6, R8, R2, R5, R3, R4 |
| S1-6 | grade asc |
R5, R3, R2, R8, R6, R1, R7, R4 |
| S1-7 | grade desc |
R1, R7, R8, R6, R2, R5, R3, R4 |
| S1-8 | departed asc |
R3, R1, R2, R5, R8, R6, R7, R4 |
| S1-9 | departed desc |
R1, R2, R5, R8, R6, R7, R3, R4 |
| S1-10 | task_status asc |
R3, R8, R2, R5, R7, R6, R1, R4(キャンセルの R5・R7 は既定順のまま) |
| S1-11 | task_status desc |
R1, R6, R5, R7, R2, R8, R3, R4(★R5・R7 は逆にならない★) |
| S1-12 | model_name asc |
R1, R2, R5, R8, R6, R7, R3, R4 |
| S1-13 | model_name desc |
R3, R1, R2, R5, R8, R6, R7, R4 |
| S1-14 | serial_number asc |
R2, R3, R1, R5, R6, R7, R8, R4 |
| S1-15 | serial_number desc |
R8, R7, R6, R5, R1, R3, R2, R4 |
| S1-16 | grade asc の結果を slice(3, 6) |
R8, R6, R1(並べ替えたあとの件数は 8) |
| S1-17 | 入力の配列 | 書き換えない(並べ替えの前後で元の配列の順が同じ) |
scheduled_at desc はパースで null に畳むので §表S2 の S2-7 で見る)| # | 何を | 期待 |
|---|---|---|
| S2-1 | parseCleanLogQuery()・sort 省略 |
sort: null・order: "asc" |
| S2-2 | sort= / sort=%20 |
省略と同じ |
| S2-3 | sort=grade(order 省略) |
sort: "grade"・order: "asc" |
| S2-4 | sort=facility_name |
422(★並べ替えない列★) |
| S2-5 | sort=precheck_1 / sort=foo / sort=Grade |
422(大文字小文字は区別) |
| S2-6 | order=up(sort 省略でも) |
422(施設マスタと同じく検証する) |
| S2-7 | sort=scheduled_at&order=desc |
422 にならず、★sort: null★ に畳まれる |
| S2-8 | 422 の本文 | 入力値をエコーしない。details のキーはパラメータ名、中身は受け付ける値の一覧(lib/facilities/query.ts と同じ形) |
| S2-9 | /clean-logs/export(app/api/clean-logs/export のテスト。db.cleanLogs のモックが表S1 の8行を返す)に sort=grade&order=asc |
CSV の行のシリアル列が S-004, S-002, S-001, S-007, S-005, S-003, S-006, 空 の順。不正な sort で 422 |
| S2-10 | /clean-logs(ルートのテスト)に sort を付けて per_page・page 2 |
2ページ目にも並べ替えが効き、meta.total は絞り込み後の件数(並べ替えで変わらない) |
filterSortErrorReportKeys() の純粋関数のテスト。include_resolved true)★行は ErrorReportKeySource(DB の行の形。clean_timetable_id は BigInt(501) / BigInt(CLEAN_TIMETABLE_ID_UNSCHEDULED) / null)で作り、 ★errorReportKeyFromRow() を通してから★ filterSortErrorReportKeys() に渡す(0 と NULL がどちらも予定外になることを、キーを手で作らずに確かめるため)。 置き場所は lib/error-reports/__tests__/query.test.ts の新しい describe。既存の表1 の定数(E1〜E9 など)とぶつからない名前にする(例: BH_E1)。
| 行 | error_id |
検知時刻(JST) | シリアル | 種類 | レベル | 対応状況(status) |
備考(comment) |
clean_timetable_id |
|---|---|---|---|---|---|---|---|---|
| E-1 | E-1 |
11-03 02:00 | 8111R-0002 | Offline | ERROR | 0 未解消 | null | 501 |
| E-2 | E-2 |
11-03 02:00 | --- | SECURITY_ON | CRITICAL | 1 対応中 | 回線確認中 | 0 |
| E-3 | E-3 |
11-02 23:00 | 8111R-0001 | BrushError | WARNING | 0 未解消 | ""(空文字) |
NULL |
| E-4 | E-4 |
11-02 22:00 | DS-005 | D-S_CONTROL_FAILURE | CRITICAL | 2 解消 | 復帰を確認 | 502 |
| E-5 | E-5 |
11-01 10:00 | 8111R-0001 | Offline | ERROR | 1 対応中 | null | 503 |
| E-10 | E-10 |
11-02 22:00 | 8111R-0003 | LowBattery | WARNING | 0 未解消 | 現場に連絡済み | 504 |
既定順(compareErrorReportKeys()): E-1, E-2, E-3, E-10, E-4, E-5(同じ 11-02 22:00 は "E-10" < "E-4")
| # | 指定 | 期待する順 |
|---|---|---|
| E1-1 | sort null |
E-1, E-2, E-3, E-10, E-4, E-5 |
| E1-2 | detected_at asc |
E-5, E-10, E-4, E-3, E-1, E-2 |
| E1-3 | serial_number asc |
E-2, E-3, E-5, E-1, E-10, E-4(--- が先頭。同じシリアルの E-3・E-5 は既定順) |
| E1-4 | serial_number desc |
E-4, E-10, E-1, E-3, E-5, E-2(★E-3・E-5 は逆にならない★) |
| E1-5 | error_type asc |
E-3, E-4, E-10, E-1, E-5, E-2 |
| E1-6 | error_type desc |
E-2, E-1, E-5, E-10, E-4, E-3 |
| E1-7 | error_level asc |
E-2, E-4, E-1, E-5, E-3, E-10 |
| E1-8 | error_level desc |
E-3, E-10, E-1, E-5, E-2, E-4 |
| E1-9 | resolution_status asc |
E-1, E-3, E-10, E-2, E-5, E-4 |
| E1-10 | resolution_status desc |
E-4, E-2, E-5, E-1, E-3, E-10 |
| E1-11 | note asc |
E-2, E-4, E-10, E-1, E-3, E-5(回 U+56DE < 復 U+5FA9 < 現 U+73FE。空の3行は既定順で下) |
| E1-12 | note desc |
E-10, E-4, E-2, E-1, E-3, E-5(空の3行は下のまま) |
| E1-13 | schedule scheduled |
E-1, E-10, E-4, E-5 |
| E1-14 | schedule unscheduled |
E-2, E-3(clean_timetable_id が 0 と NULL の両方) |
| E1-15 | schedule unscheduled + serial_number desc |
E-3, E-2 |
| E1-16 | include_resolved false・resolution_status asc |
E-1, E-3, E-10, E-2, E-5(E-4 は解消なので出ない) |
| E1-17 | include_resolved false・schedule scheduled |
E-1, E-10, E-5(3件) |
parseErrorReportQuery())| # | クエリ | 期待 |
|---|---|---|
| E2-1 | sort・order・schedule 省略 |
sort: null・order: "asc"・schedule: null(今と同じ並び・絞り込みなし) |
| E2-2 | 空文字・空白だけ | 省略と同じ |
| E2-3 | sort=facility_name / sort=schedule_link / sort=error_list_id / sort=foo |
422 |
| E2-4 | order=up |
422 |
| E2-5 | schedule=all / schedule=main / schedule=Scheduled |
422(受け付けるのは scheduled と unscheduled だけ。「すべて」は送らない) |
| E2-6 | 422 の本文 | S2-8 と同じ作法 |
| E2-7 | sort=detected_at&order=desc |
422 にならず、★sort: null★ に畳まれる |
| E2-8 | sort=error_level(order 省略) |
order: "asc" |
schedule: scheduled(予定あり)/ unscheduled(予定外)。省略 = すべてscheduledTimetableId()(lib/db/timetable-link.ts:20)の1本★。clean_timetable_id が 0 か NULL なら予定外(CLAUDE.md §4.7)。 ★=== null / { not: null } / === 0 を直書きしない★unscheduled = 「紐づく予定」が「予定なし」(schedule_link: "none")の行、scheduled = それ以外★filterSortErrorReportKeys())で行う。meta.total は絞り込み後の件数
errorReportListWhere() は期間で既に OR を使っており、もう1つの OR を足すと条件が上書きされる事故が起きやすい。 1段目は今も条件に合う全件のキーを読んでいるので、アプリで振り分けても読む量は増えない<fieldset> の中)に <select> を足す。aria-label・title は「紐づく予定」。 選択肢は ★「予定:すべて」(値は空=送らない)「予定あり」「予定外」★(★文言はユーザー指示。変えない★。先頭だけ欄の名前を入れるのは、ほかのプルダウンと並んだときに何の欄か分かるように)
<select> と同じく styles.select + 固定幅のクラス。いちばん長い表示(EN の "Schedule: All" か "Unscheduled")と ▼ が収まる幅。JP/EN の切り替えでツールバーが動かないように。D-162)lib/i18n.ts に足す★(既にあるキーは足さない)setPage(1)(期間の入力と同じ書き方)。<fieldset disabled> の中なので編集中は自動で押せなくなるlib/ds-alerts/query.ts の sortDsAlerts())には関係させないd-watch-api.yaml
/clean-logs と /clean-logs/export: sort(enum 7値。★default は書かない★。説明に「省略時は既定順」)・order(enum [asc, desc]・default: asc。説明に「施設マスタ(desc)と違い、1回目は悪いほうが上のため asc」「sort が省略でも検証するが並びには効かない」「sort=scheduled_at&order=desc は既定順と同じ」)。 §1-1・§2-2 の定義、「並べ替えは絞り込みのあと・ページングの前」「同じ値は既定順」「下に固定する行」を説明に書く。422 の説明に sort/order を足す。 既定順の記述(:494 付近と CleanLogItem の説明 :2239 付近)は「sort を省略したとき」と書き足す/errors: sort(enum 6値。default なし)・order(/clean-logs と同じ)・schedule(enum [scheduled, unscheduled]。説明: unscheduled = schedule_link が none の行、scheduled = main / precheck / unknown の行。予定の表は引かない)。 ★:909-910 の「ソート指定のクエリパラメータは無い」を書き換える★。422 の説明に sort/order/schedule を足す。sort=detected_at&order=desc は既定順と同じ、と書くlib/table-sort.ts(新規。純粋関数)
TableSortOrder = "asc" | "desc"、SortState<K> = { key: K; order: TableSortOrder } | nullnextSortState(current, key, { first, twoState }) — §1-3 の表をそのまま実装。first は "asc" か "desc"(施設マスタは "desc")displayOrder(state, key, defaultKey?) — 見出しに出す向き。state が null で key === defaultKey なら "desc"(既定順のとき日付・検知時刻に ▼)、 state があれば state.key === key ? state.order : null、それ以外は null。施設マスタは defaultKey を渡さない(今のまま)lib/__tests__/table-sort.test.ts:
nextSortState: §1-3 の5行 + 施設マスタの動き(first: "desc": 既定 → desc → asc → 既定)displayOrder: 「null・日付 → desc」「null・他の列 → null」「{日付, asc}・日付 → asc」「{完了率, asc}・日付 → null」「null・defaultKey なし → null」components/ui/SortableTh.tsx(新規)
<th aria-sort> の中に <button type="button">。props: 見出しの文字、order(displayOrder() の値)、onClick、disabled、ariaLabel(「○○で並べ替え」)、 ★align: "left" | "center" | "right"★(th の text-align とボタンの justify-content に反映。今の見出しのそろえを変えないため)、 ★children★(見出しの中に並べ替え以外のボタンを置く。清掃詳細ログの「判定」の「?」)children があるときは、ボタンを width: auto にして、th の中でボタンと children を inline-flex で1行に並べる (width: 100% のままだと「?」が押し出されて、折り返さない見出しからはみ出す)aria-hidden。CSS は施設マスタの .thSortable 系(FacilitiesScreen.module.css:135-166 付近)を components/ui/SortableTh.module.css に移し、そろえを props で変えられるようにするcomponents/features/facilities/FacilitiesScreen.tsx と .module.css: 上の部品を使うように置き換える(align="center"・first: "desc")。★動きも見た目も1つも変えない★。 置き換えたら施設マスタの画面で ▼ → ▲ → 既定 を確かめるlib/types/index.ts: CleanLogSortKey(7値)を足すlib/clean-logs/query.ts: ★既存の CleanLogQuery(:15。parseCleanLogQuery() が返す QueryParseResult<CleanLogQuery> の query)★ に sort: CleanLogSortKey | null・order: TableSortOrder を足す(★lib/types に同じ名前の型を作らない★)。 parseCleanLogQuery() に検証(表S2)。受け付ける値は配列1か所(CLEAN_LOG_SORT_KEYS)。2状態の列の desc は sort: null に畳む
result パラメータは空文字で 422 になる(:122-127)が、新しい sort/order は施設マスタに合わせて空文字 = 省略★。result の挙動は変えないlib/clean-logs/sort.ts(新規): sortCleanLogs(items, sort, order)。表S1 のテストを lib/clean-logs/__tests__/sort.test.ts に
sort が null なら並べ替えず [...items] を返す★filterCleanLogs() の中に入れないこと★(lib/clean-logs/__tests__/query.test.ts が「入力の順を保つ」ことに依存している)db.cleanLogs()・compareCleanLogItems())の既定順を変えないこと★ — ダッシュボードの problem_logs(先頭5件)がこの順に依存している(app/api/dashboard/route.ts)lib/config/index.ts: 清掃結果の順位表(§2-2)app/api/clean-logs/route.ts: filterCleanLogs() のあと、slice() の前に sortCleanLogs()。capCleanLogs() の位置は今のまま。 ルート冒頭の手順コメントに「並べ替え(sortCleanLogs()。sort 省略時は並べ替えない)」を足す。capCleanLogs() の docstring に「filterCleanLogs() → sortCleanLogs() → ページ切り出しの後」と書き足すapp/api/clean-logs/export/route.ts: ★行数の上限の判定(filtered.length > MAX_CSV_ROWS の 422。:79 付近)の後、buildCleanLogCsv() の直前★に sortCleanLogs() (★入れ忘れると CSV だけ黙って既定順になる★。上限の判定より前に置くと、どうせ 422 で捨てる大量の行まで並べ替えることになる)lib/clean-logs/csv.ts:43 付近・lib/types/index.ts:316 付近・lib/i18n.ts:225 付近components/features/detail-log/DetailLogScreen.tsx: sort の状態(SortState<CleanLogSortKey>)。queryString に sort・order を載せる(既定のときは送らない)。 CSV のリンクは同じ queryString を使っているので自動で揃う(★揃っていることを確かめる★)。並べ替えを変えたら setPage(1)components/features/detail-log/CleanLogTable.tsx: props sort?: SortState<CleanLogSortKey> と onSort?: (key: CleanLogSortKey) => void を足す(★省略可能★)。 onSort が渡されたときだけ7列の見出しを SortableTh にする(向きは displayOrder(sort, key, "scheduled_at"))。 ★ダッシュボード(compact)は渡さない → 今のまま★。「判定」の見出しの「?」ボタンは SortableTh の children に入れるtable-layout: fixed・見出しは折り返さない。★印を足すと EN で見出しがはみ出す列がある★(出発 56px・日付 64px・完了率・清掃結果・判定など。事前調査の計算)。 並べ替える列の見出しの左右の余白を詰める(施設マスタは 12px → 6px)。★列の合計幅(最小 1134px)を広げないで収める★。収まらない列があれば監督に報告lib/db/robot-error-select.ts の ROBOT_ERROR_LIST_KEY_SELECT(1段目)に serial_number・error_level・comment を足す(clean_timetable_id は既にある)。 ★robot_id / robot は足さない★(robot-error-select.test.ts が見張っている)。 :69-71 付近のコメント「clean_timetable_id は並べ替えには使わない」を「予定あり/予定外の絞り込みに使う。serial_number・error_level・comment は並べ替えに使う」に直すlib/db/error-report-mapping.ts(★入力と出力で足すものが違う★):
ErrorReportKeySource(DB の列名)に clean_timetable_id: bigint | null・serial_number: string・error_level: string・comment: string | nullErrorReportKey に serial_number・error_level・note: string | null(comment の生値。空かどうかは並べ替えの側で trim して決める)・scheduled: boolean(scheduledTimetableId(row.clean_timetable_id) !== null)errorReportKeyFromRow() で詰め替えるlib/types/index.ts: ErrorReportSortKey(6値)・ErrorScheduleFilter("scheduled" | "unscheduled")を足す。 ★ErrorReportDbQuery(:807 付近。「引数は必須」の作法 :802)★ に sort: ErrorReportSortKey | null・order: TableSortOrder・schedule: ErrorScheduleFilter | null を足し、 :802-806 の docstring に「sort・order・schedule もアプリ側で処理する」と書き足すlib/error-reports/query.ts: ★既存の ErrorReportQuery(:19。camelCase)★ にも同じ3つを足す(★lib/types に同じ名前の型を作らない★)。 parseErrorReportQuery() に検証(表E2)。filterSortErrorReportKeys() に schedule の絞り込み(key.scheduled を見る)と sort/order の並べ替え(表E1)。 compareErrorReportKeys() は既定順としてそのまま残すlib/db/index.ts の errorReports()(Prisma 版 :1853 付近・fixtures 版 :715 付近): 新しいフィールドを filterSortErrorReportKeys() に渡すだけ。2段目は変えないapp/api/errors/route.ts: パース結果(camelCase)を ErrorReportDbQuery に詰め替えて渡すcomponents/features/errors/ErrorsScreen.tsx: sort の状態と §4 のプルダウン。queryString(:258-276 付近)に sort・order・schedule を載せる
SortableTh(align="left"。向きは displayOrder(sort, key, "detected_at"))に。 ★disabled={editing}★(見出しは <fieldset> の外なので、明示しないと編集中も押せてしまう。押すと別ページの下書きが保存されずに消える — lib/errors/edit-drafts.ts):374 と同じ考え方)COLUMN_COUNT(:71)と <colgroup> は変わらないlib/i18n.tsaria-label の英訳(「機種で並べ替え」"Sort by model" など)と §4 の2語。 ★両画面ぶんをまとめて1か所に足す。両画面に共通の「シリアルで並べ替え」は1回だけ。既にあるキーは足さない★(同じキーを2回書くと tsc エラーになる)sort・order・schedule、キーの serial_number など)を足したために、型エラーや toEqual の形の違いで壊れたテストは、 既定値(sort: null, order: "asc", schedule: null、行の新しい列)を足すだけなら直してよい★。★並び・件数・where の期待値は変えない★lib/db/__tests__/fixture-clean-logs.test.ts:20-34 の memoryQuery()lib/error-reports/__tests__/query.test.ts の key() ヘルパー(:30-40)・base(:55-63)・parse("") の toEqual(:207-221)。★表1(:70-166。監督確定)の期待値は変えない★lib/db/__tests__/fixture-error-reports.test.ts の BASElib/db/__tests__/error-report-mapping.test.ts:390-399・:407-416・:440-448(キーと元の行の形)app/api/errors/__tests__/route.test.ts:557-575(パース結果・1段目の select が定数と一致)app/api/clean-logs/__tests__/route.test.ts(:418 など)・lib/db/__tests__/unscheduled-clean-log.test.ts:190-224・ lib/db/__tests__/fixture-clean-logs.test.ts:212-229・lib/db/__tests__/fixture-error-reports.test.ts:161-191。★変わったら実装の誤り★scheduled_at だけで並べ直すため。lib/db/index.ts:631-633)。 新しい並べ替えは「同じ値なら compareCleanLogItems()」なので、sort を指定したときは両方で同じになる。★既定順(省略時)の食い違いは今回直さない★(ダッシュボード・既存テストに波及するため)。 だから ★sort が null のときは並べ替え関数が何もしない★(§2-1 の表)。fixtures の画面で既定順の同時刻の並びが Prisma の規則と違って見えても、それは今回の不具合ではないgrade は結果待ちでも "×"。★result_pending を先に見る★id は null。並べ替えの比較で id を使わない(既定順に任せる)task_status の 3 と 6 は同じ順位。6 を「キャンセル」と別扱いにしないcancelled / result.query !== queryString)は、並べ替えを queryString に載せれば自動で効く。★別の取得経路を作らない★comment(TEXT)が増えるので転送量が少し増える(P1 の件数なら問題ない。D-160 追記2「解消済みも表示の全件読みはリリース前に見直す」の範囲)★確かめ方★: USE_FIXTURES=true npm run build → USE_FIXTURES=true npx next start -p 3100 で開く(★:3000 のサーバーは触らない★)。終わったら :3100 を止める。
th.scrollWidth <= th.clientWidth)git add -A 禁止。★コミットはしない(監督がまとめてコミットする)★docs/decisions.md・docs/handoff/指示書AY_*・docs/handoff/設計_制御盤ログの取り出し_*・components/features/errors/ErrorContentPicker.tsx・lib/errors/error-content-label.ts とそのテスト・prisma/docs/decisions.md に後で書く。Cursor は書かない)asc(悪いほうが上)。施設マスタは 1回目 desc のままsort 省略は null。2状態の列の desc は null に畳む。sort が null なら並べ替えない(fixtures の既定順を変えないため)監督の確認結果: tsc・lint・vitest 全件(154ファイル・3778件)は通過。fixtures の画面(:3100)で §8 の確認はほぼ通った(並びの切り替え・▼▲⇅・CSV のリンク・ダッシュボードに出ない・「予定外」で3件・編集中に押せない・施設マスタは今までどおり・JP/EN ではみ出さない)。 4観点のコードレビューで27件の指摘、検証して21件が本物(重複・指示書どおり・範囲外の6件は棄却)。★下の全件を直すこと★。直したら §8 をもう一度通し、tsc・lint・vitest 全件の件数を報告する。
components/ui/SortableTh.module.css・SortableTh.tsx: children があるとき(清掃詳細ログの「判定」だけ)、.thSortableWithChildren { display: inline-flex } が ★<th> そのもの★ に付いている。 監督が画面で実測: 「判定」の th が display: inline-flex になり、幅 69px(列は 96px)。th が表のセルでなくなり、見出しの下線が列の一部にしか引かれない。 → th には display を付けない。children があるときだけ th の中に <span className={styles.thInner}>(display: inline-flex; align-items: center; gap: 2px; max-width: 100%)を置き、その中にボタンと children を入れる。そろえは th の text-align に任せる。 直したら「判定」の th が display: table-cell・幅が列と同じ・中央そろえ・th.scrollWidth <= th.clientWidth を確かめるlib/clean-logs/sort.ts:18-19: 清掃結果の順位の「出発失敗=1」「表に無い値=5」が数字の直書き(§2-2 違反)。 → lib/config/index.ts の CLEAN_TASK_STATUS_SORT_RANK の隣に CLEAN_TASK_STATUS_SORT_RANK_NOT_DEPARTED = 1・CLEAN_TASK_STATUS_SORT_RANK_UNKNOWN = 5 を置き(または1つのオブジェクトにまとめ)、sort.ts はそれを読む。docstring に 1〜8 の表を全部書くlib/clean-logs/__tests__/sort.test.ts: 順位の 0(清掃開始前=6)・1(清掃中=7)・負の値(不明=5)を縛るテストが無い。 → 表S1 は変えずに別の it を足す。task_status = 0, 1, 2, 3, 4, 5, 6, 7, -1, null と結果待ちの行を、わざと既定順と違う順で渡し、 asc: null → 5 → 2 → {3, 6} → {7, -1} → 0 → 1 → 4 → 結果待ち、desc: 4 → 1 → 0 → {7, -1} → {3, 6} → 2 → 5 → null → 結果待ち({} の中は既定順のまま。desc でも逆にならない)lib/error-reports/__tests__/query.test.ts:474 付近: 表E1 の入力 BH_KEYS が既定順と同じ並びなので「同じ値どうしは既定順」を確かめられていない。 → 入力を既定順と違う順(例 [BH_E5, BH_E4, BH_E10, BH_E3, BH_E2, BH_E1])にして E1-1〜E1-17 すべてに渡す。★期待値(表E1)は変えない★app/api/errors/__tests__/route.test.ts・lib/db/__tests__/fixture-error-reports.test.ts: ルート → データ層まで sort・order・schedule が届くことを確かめるテストが無い(既定値を足しただけ)。 → route.test.ts に include_resolved=true&schedule=unscheduled&sort=serial_number&order=desc のリクエストを1件(データ層に届いた query か items の順と meta.total)、sort=facility_name の 422 を1件。 fixture-error-reports.test.ts に db.errorReports({ ...BASE, include_resolved: true, schedule: "unscheduled" }) → FX-ERR-04・FX-ERR-12・FX-ERR-20 の3件d-watch-api.yaml の /clean-logs の sort(:552 付近): 清掃結果の順位表が書かれていない(「悪い順↔良い順」だけ)。 → task_status の行に「出発失敗 → 5 清掃異常終了 → 2 清掃中断 → 3・6 清掃キャンセル(同じ順位)→ 表に無い値(不明(n))→ 0 清掃開始前 → 1 清掃中 → 4 清掃正常終了。表示の並びだけで判定には使わない」。grade の行に「結果待ちは grade が × でも下に固定」d-watch-api.yaml の /clean-logs/export の 422(:645 付近): sort/order の不正値が書かれていない。 → 「sort/order の不正値(details はパラメータ名と受け付ける値の一覧。/clean-logs と同じ形)」を足し、行数超過の { total, max } と形が違うことも並べて書くd-watch-api.yaml の /errors の schedule(:1029 付近): 「空文字・空白だけは省略と同じ(すべて)」を足すd-watch-api.yaml:497・:961 付近: 「§下の」は意味が通らない → 「下のパラメータ sort / order の説明に従う」に直すapp/api/clean-logs/__tests__/sort-route.test.ts:30-32: result=all で8行を全部通しているので「meta.total は絞り込み後の件数」を縛れていない。 → 8行より少なくなる絞り込み(例 result=problem)と sort=grade&order=asc&per_page=2&page=2 のケースを足すlib/clean-logs/__tests__/query.test.ts:662-674・lib/error-reports/__tests__/query.test.ts:363 付近: 422 の行が ok === false しか見ていない。 → S2-5・S2-6・E2-4・E2-5 に expect(parsed.status).toBe(422)。E2-6 で sort/order/schedule それぞれの details の形(キーがパラメータ名・中身が受け付ける値の一覧)。order=・order=%20・sort=%20 が省略と同じになるケースlib/__tests__/table-sort.test.ts: §1-3 の2つの遷移が縛られていない。 → nextSortState({key:"grade",order:"desc"}, "scheduled_at", {first:"asc",twoState:true}) → {scheduled_at, asc}、 nextSortState({key:"monitoring",order:"asc"}, "d_security", {first:"desc",twoState:false}) → {d_security, desc} の2件lib/clean-logs/__tests__/sort.test.ts と bh-s1-fixture.ts: 表S1 の8行が2か所に書かれている。 → sort.test.ts も bhS1RowsDefaultOrder() を import して使う(const [R4, R1, R2, R5, R3, R8, R6, R7] = bhS1RowsDefaultOrder();)。期待値は変えないapp/api/clean-logs/export/__tests__/route.test.ts:151: it() の中の await import() はこのファイルの作法(冒頭 :3-7)違反 → ファイル冒頭で静的に import するlib/clean-logs/sort.ts:72-103・lib/error-reports/query.ts:110 付近: (1) completion_rate の分岐の意味の無い if (aTier !== 0) return null; を消す。 (2) 両方 null の行どうしは、主キーの比較の前に明示的に 0 を返す(null < null や null - null に頼らない)。 (3) switch の default を { const _exhaustive: never = sort; return _exhaustive; } にする(キーを足して case を書き忘れたら tsc が止める)。sort.ts の2つと error-reports の1つ。 (4) コメントを足す: 結果待ちを先に見る理由(罠3: grade は "×" のまま)、desc は主キーだけ反転し reverse しない(§2-1)、完了率の固定行の段(0/1/2)の意味app/api/clean-logs/export/route.ts の冒頭に「sort/order も受け付け、CSV は画面と同じ並び」。sortCleanLogs() の直前に「上限の判定の後(捨てる行を並べ替えない)・入れ忘れると CSV だけ既定順」。 parseCleanLogQuery()・parseErrorReportQuery() の docstring に「空文字・空白は省略と同じ/不正値は 422(入力値をエコーしない)/2状態の列の desc は sort null に畳む/sort が null でも order は検証する」。 ErrorReportQuery の3フィールドに JSDoc(「省略時 null(既定順)」「省略時 asc」「null = すべて」)components/ui/SortableTh.module.css: 施設マスタから移したときに理由のコメントが落ちた。.sortButton・.sortMark・.thSortable の上に戻す — ★font: inherit を外さない(外すと日本語が中華字形に落ちる。CLAUDE.md §0-3)★・印の幅を固定する理由(⇅ と ▼▲ で文字が動かない)・余白 6px の理由。 CleanLogTable.module.css:15 付近と ErrorsScreen.module.css:252 付近の列幅の説明に「並べ替える見出しは SortableTh(左右 6px+間隔 3px+印 9px)」を1行足す.sortButton:hover .sortMark { opacity: 1 } を .sortButton:hover:not(:disabled) .sortMark にする(押せないのに濃くならないように)。 ★編集中に並べ替える6列の見出しの文字だけが薄くなり、ほかの3列と濃さがまだらになる★ → ★見出しの文字は薄くしない★。押せないときは印(⇅▲▼)だけを薄くし、カーソルを default にするawait import・表S1 の2か所・CSS コメント)→ R1・R14・R13・R17 にまとめたd-watch-api.yaml が js-yaml で読み込めない(bad indentation)→ ★今回の変更より前(HEAD 38e8f5d)からある★。範囲外。別の作業として監督が扱う§11 の修正は監督が確認済み(tsc・lint・vitest 154ファイル・3788件が通過、fixtures の画面で §8 を再確認)。その上に次の2つを足す。
components/ui/SortableTh.*。3画面共通)| 状態 | 今 | ★変更後★ |
|---|---|---|
| 並べていない列 | ⇅(--tx-m・不透明度 0.5) |
★△(U+25B3。白抜き)・色 --sort-mark・不透明度 0.5★ |
asc |
▲(--tx) |
★▲・色 --sort-mark・不透明度 1★ |
desc |
▼(--tx) |
★▼・色 --sort-mark・不透明度 1★ |
| マウスを重ねた(押せるときだけ) | 並べていない印が不透明度 1 | 同じ(変えない) |
| 押せない(エラー一覧の編集中) | 印だけ 0.35・見出しの文字は薄くしない・カーソル default | 同じ(変えない。§11 R18) |
--sort-mark を新しく作る★(app/globals.css):
:root, [data-theme="dark"]): ★#ffffff★(ユーザー指示の「白」)[data-theme="light"]): ★#1e293b(ライトの --tx と同じ値)★ — ライトの表の背景は --sf = #ffffff なので、白にすると印が背景に溶けて消える(コントラスト 1.0:1)。★監督の判断★.sortMark に #fff を直書きしないこと★(ライトで消える)。テーマは data-theme 属性だけで切り替わる(prefers-color-scheme は使っていない)app/globals.css のトークンにして1か所にまとめる★(CLAUDE.md §0-3「globals.css のフォントスタックを正とする」。並びの写しを CSS モジュールに作らない):
app/globals.css に、テーマのブロックとは別に :root { --font-ja: "Hiragino Sans", "Hiragino Kaku Gothic ProN", "Yu Gothic UI", "Yu Gothic", "Noto Sans JP", Meiryo, sans-serif; } を置く(フォントはテーマによらないので、ライトのブロックには書かない)font-family を var(--font-geist-sans), var(--font-ja); にする(並びの中身は今と同じなので body の見た目は変わらない)。:55-57 のコメントは残すSortableTh.module.css の .sortMark, .sortMarkActive に font-family: var(--font-ja); を付ける。理由をコメントに書く(▲▼ を Geist Fallback(Arial)ではなく △ と同じ日本語フォントで描かせるため。並びの正は globals.css の --font-ja)width: 9px・font-size: 9px・text-align: center)は残す★(印が変わっても見出しの文字が動かない)aria-sort・aria-label・title は変えない(印は aria-hidden)CleanLogTable.tsx の chevron ▼▲。--tx-m・10px)は★変えない★SortableTh.module.css(2か所)、lib/table-sort.ts:46(「▲▼⇅」→「▲▼△」)承知のうえの点(直さない):
desc なので、△(上向き)を押すと ▼ になる。詳細ログ・エラー一覧は △ → ▲エラー一覧(§4)と同じ3択を、清掃詳細ログの API・CSV・画面に足す。
意味(エラー一覧とは中身が違う。YAML にもそう書く):
unscheduled(予定外)= ★CleanLogItem.unscheduled が true の行★(予定外タスク。clean_task_reports.clean_timetable_id が 0 か NULL。判定はデータ層で scheduledTimetableId() 済み)scheduled(予定あり)= ★それ以外の行★(予定の表=本清掃の行。レポートの無い未実施・結果待ちの行も入る)。★clean_timetable_id の値では決めない★ — clean_timetable_id が 0 以外なのに予定の表に無いレポートは、今までどおり一覧に出ない(予定ありにも予定外にも入らない。エラー一覧の「予定あり」はこの形も含むので、中身が違う)共通にするもの(★1か所にまとめる★。2画面で規則がずれないように。既知の罠「表示と関門は同じ道具を通す」):
lib/api/schedule-filter.ts(lib/api/master-id.ts と同じ置き場所・同じ書き方の純粋関数):
export const SCHEDULE_FILTERS = ["scheduled", "unscheduled"] as const(★この順★。details の文言に使う)export function parseScheduleFilter(searchParams: URLSearchParams): QueryParseResult<ScheduleFilter | null> — searchParams.get("schedule")?.trim() || null。配列に無い値は fail(422, "validation_failed", "scheduleの値が不正です", { schedule: ["scheduled / unscheduled のいずれかを指定してください"] })。★入力値をエコーしない★。大文字小文字は区別する(Scheduled は 422)。 判定は今の lib/error-reports/query.ts:187 と同じく SCHEDULE_FILTERS.includes(raw as ScheduleFilter)(★キャストを外すと as const の配列では TS2345★)。 呼び出し側の受け方は parseMasterIdFilters() と同じ: const schedule = parseScheduleFilter(searchParams); if (!schedule.ok) return schedule;、戻り値の query には schedule: schedule.queryexport function matchesScheduleFilter(isScheduled: boolean, filter: ScheduleFilter | null): boolean — filter === null なら true、"scheduled" なら isScheduled、"unscheduled" なら !isScheduled。switch に never の網羅チェックkey.scheduled、清掃詳細ログは ★!r.unscheduled★(向きが逆。取り違えても型エラーにならないので、両方向をテストで縛る)lib/types/index.ts: ErrorScheduleFilter を ★ScheduleFilter★ に名前を変える(使っているのは lib/error-reports/query.ts・lib/types/index.ts・ErrorsScreen.tsx だけ。別名は残さない。★名前を変えたら同じ段階で3ファイルの参照も新しい名前に直す★)。注記を「GET /errors と GET /clean-logs・/clean-logs/export の schedule」に直すlib/error-reports/query.ts: ERROR_SCHEDULE_FILTERS(:36)は消し、:186-191 の検証を parseScheduleFilter() に(:218 は schedule: schedule.query)、:280-281 を matchesScheduleFilter(key.scheduled, query.schedule) に置き換える。 ★検証の順(sort → order → schedule)・message・details の形は変えない★ → エラー一覧の既存テスト(E2-1・E2-2・E2-5・E2-6、route.test.ts、fixture-error-reports.test.ts)は★1文字も変えずに通ること★components/ui/ScheduleFilterSelect.tsx: <select> と3つの選択肢・aria-label/title(「紐づく予定」)を持つ部品。 props は value: ScheduleFilter | null、onChange: (next: ScheduleFilter | null) => void、className: string(★CSS は持たない★。詳細ログの .select は余白 4px 8px、エラー一覧は 6px 10px と違うので、各画面の styles.select と固定幅のクラスを渡す)。 選択肢: 値 "" =「予定:すべて」、"scheduled" =「予定あり」、"unscheduled" =「予定外」(★文言は §4 のまま。変えない★)。t() の取り方は今の ErrorsScreen.tsx に合わせる
ErrorsScreen.tsx の今の <select>(:608-621 付近)をこの部品に置き換える(見た目・位置・<fieldset> の中は変えない)。画面ローカルの型 ScheduleFilterState は不要になれば消す清掃詳細ログ側:
lib/clean-logs/query.ts:
CleanLogQuery に schedule: ScheduleFilter | null(JSDoc「省略・空白は null=すべて」)parseCleanLogQuery(): ★sort・order の検証の後★に parseScheduleFilter()。失敗ならそれを返す。docstring に schedule を足すfilterCleanLogs(): if (!matchesScheduleFilter(!r.unscheduled, query.schedule)) return false;(機種・シリアルの後。result との AND)CleanLogDbQuery(データ層)には足さない★(db.cleanLogs() はダッシュボードも使う。データ層で行を落とすとダッシュボードの数え方に波及する。絞り込みの正は filterCleanLogs())。lib/types/index.ts の CleanLogDbQuery の「ここに入れないもの」に schedule を書き足すapp/api/clean-logs/route.ts・export/route.ts: 本体は変えない(filterCleanLogs() を通るので一覧・meta.total・CSV・CSV の行数上限の判定のすべてに効く)。冒頭の手順コメントに「予定あり/予定外」を足すcomponents/features/detail-log/DetailLogScreen.tsx:
scheduleFilter: ScheduleFilter | null(既定 null)queryString の useMemo で if (scheduleFilter) params.set("schedule", scheduleFilter)、★依存配列にも入れる★(入れ忘れると選んでも取り直さず、CSV のリンクも変わらない。例外は出ない).toolbar の flex-wrap・幅・余白は変えない)★className={${styles.select} ${styles.scheduleFilterSelect}}、変えたら★同じ操作の中で setPage(1)★(useEffect で後追いしない)DetailLogScreen.module.css: .scheduleFilterSelect(エラー一覧の .scheduleFilterSelect と同じ: 幅 160px 固定・overflow: hidden・text-overflow: ellipsis・white-space: nowrap)。JP/EN でツールバーが動かないこと(D-162)d-watch-api.yaml(★先に直す。CLAUDE.md §6-1★):
/clean-logs の parameters(order の後、Page の前)と /clean-logs/export の parameters(order の後)に schedule(enum: [scheduled, unscheduled]、default は書かない)。説明: 上の「意味」・省略と空文字・空白だけはすべて・未知の値は 422schedule の不正値(details はパラメータ名と受け付ける値の一覧。sort/order と同じ形)」/errors の schedule の説明に「清掃詳細ログの schedule とは予定ありの中身が違う」と1行lib/i18n.ts: ★足すキーは無い★(「予定:すべて」「予定あり」「予定外」「紐づく予定」は既にある)表P — parseScheduleFilter()(新規 lib/api/__tests__/schedule-filter.test.ts)と parseCleanLogQuery()(lib/clean-logs/__tests__/query.test.ts)
| # | クエリ | 結果 |
|---|---|---|
| P-1 | なし | null |
| P-2 | schedule= |
null |
| P-3 | schedule=%20 |
null |
| P-4 | schedule=scheduled |
"scheduled" |
| P-5 | schedule=unscheduled |
"unscheduled" |
| P-6 | schedule=%20unscheduled%20 |
"unscheduled"(前後の空白を落とす。エラー一覧と同じ) |
| P-7 | schedule=all・schedule=main・schedule=Scheduled・schedule=UNSCHEDULED |
422。message「scheduleの値が不正です」、details ★{ schedule: ["scheduled / unscheduled のいずれかを指定してください"] }(toEqual で丸ごと)★。本文に入力値が出ない |
| P-8 | sort=bad&schedule=bad(parseCleanLogQuery()) |
422 で details のキーは ★sort だけ★(先に sort を見る) |
表M — matchesScheduleFilter()(同じテストファイル)
| # | isScheduled | filter | 結果 |
|---|---|---|---|
| M-1 | true | null | true |
| M-2 | false | null | true |
| M-3 | true | "scheduled" |
true |
| M-4 | false | "scheduled" |
false |
| M-5 | true | "unscheduled" |
false |
| M-6 | false | "unscheduled" |
true |
表F — GET /clean-logs(app/api/clean-logs/__tests__/sort-route.test.ts に足す。行は bhS1RowsDefaultOrder() の8行。予定外は rp-9001(×)の1行だけ)
| # | クエリ | meta.total |
items の row_key(この順) |
|---|---|---|---|
| F-1 | result=all&schedule=unscheduled |
1 | rp-9001 |
| F-2 | result=all&schedule=scheduled |
7 | rp-9001 を除いた7行。順は result=all(schedule なし)の結果から rp-9001 を抜いたものと同じ |
| F-3 | result=problem&schedule=scheduled |
2 | tt-102・tt-103(result=problem の結果から rp-9001 を抜いた順) |
| F-4 | result=problem&schedule=unscheduled |
1 | rp-9001 |
| F-5 | result=critical&schedule=unscheduled |
1 | rp-9001 |
| F-6 | result=all&schedule=scheduled&sort=grade&order=asc&per_page=2&page=1 |
7 | result=all&sort=grade&order=asc の並びから rp-9001 を抜いた先頭2行 |
| F-7 | schedule=all |
— | 422(details のキーは schedule) |
| F-8 | result=all&schedule= |
8 | schedule なしと同じ(空文字は「すべて」。ルートを通しても 422 にならない) |
rp-9001 を filter して作ってよい(数字を手で書き写さない)。★件数(7・2)は直書きで縛る★表X — GET /clean-logs/export(app/api/clean-logs/export/__tests__/route.test.ts に足す。同じ8行)
| # | クエリ | CSV のデータ行 |
|---|---|---|
| X-1 | result=all&schedule=unscheduled |
1行。予定時刻の列が「予定外」 |
| X-2 | result=all&schedule=scheduled |
7行。予定時刻の列に「予定外」が★1つも無い★ |
| X-3 | schedule=bad |
422(JSON。details のキーは schedule) |
エラー一覧(回帰): 既存のエラー一覧のテストは★変えずに★全部通ること。matchesScheduleFilter(key.scheduled, …) の向きは既存の fixture-error-reports.test.ts(FX-ERR-04・12・20 の3件)が縛っている
lib/db/__tests__/fixture-clean-logs.test.ts:20-37 の memoryQuery() は CleanLogQuery の全フィールドを書いているので、schedule: null を足す(★入れ忘れると tsc と next build で落ちるうえ、vitest も落ちる★ — schedule が undefined だと matchesScheduleFilter() は filter === null を通らずに never の分岐へ入り、undefined(偽)を返す。そのため filterCleanLogs() が全行を落とし、#2(:52-69)の expect(sql).toEqual(memory) が赤になる。★段階4では query.ts の変更と同じ手順の中で足し、そのあとで vitest を回す★)key.scheduled(true=予定あり)、清掃詳細ログは r.unscheduled(true=予定外)。matchesScheduleFilter() には★必ず「予定あり=true」を渡す★schedule を result と同じ書き方(?? "all" で受ける)にすると schedule= が 422 になり、エラー一覧と規則が分かれる。parseScheduleFilter() だけを使う。清掃詳細ログ側は query.test.ts の表P(P-2・P-3・P-6)と表F の F-8 が縛るrp-9200000000000000001=施設1・◎、rp-9200000000000000002=施設11・×・清掃キャンセル)。★画面の既定(△×のみ)で「予定外」を選ぶと1件★、「全件」で2件。1件しか出ないのは不具合ではない。★既定(△×のみ)は1ページしかないので、ページを戻す確認は「全件」で行う(12-6)★--font-ja(日本語フォントを先に)を使うので、中国語フォントには落ちにくい。★--font-ja の並びを短くしない・CSS モジュールに写しを作らない★th の幅が変わらない)setPage(1) を入れ忘れていても DetailLogScreen.tsx:237-238 の保険(応答の page が総ページ数を超えたら最終ページへ寄せる)で1ページ目に戻って見える(件数は実行日で少し変わるが、全件と予定ありが2ページ以上・予定外が1ページという関係は変わらない)。 CSV のリンクに schedule= が付き、CSV の行も同じに絞られる。 ★ビューポート 1366px と 1536px の2つの幅で見る★: 1536px ではツールバーが1行。1366px では右の組が2行目に落ちるが不具合ではない(12-2)。 どちらの幅でも、プルダウン自身の幅(160px)が JP/EN や選んだ値で変わらないこと(左端の位置は、前の結果ボタンの文言の幅だけ JP/EN で数 px 動く。結果ボタンは今も JP/EN で幅が変わるので承知のうえ)git grep -n "Hiragino" -- app components lib のヒットが app/globals.css の --font-ja の1行だけであること。ダーク・ライトとも △ と ▲▼ の大きさ・高さがそろうことd-watch-api.yaml(12-2 の YAML)lib/api/schedule-filter.ts+テスト(表P・表M)、lib/types/index.ts(名前の変更と注記)lib/error-reports/query.ts を共通関数に置き換え → ★エラー一覧の既存テストが変えずに通ることを先に確かめる★lib/clean-logs/query.ts、ルートのコメント、fixture-clean-logs.test.ts の schedule: null、表P(P-1〜P-8)・表F・表X のテスト。 ★表P の P-1〜P-7 は lib/clean-logs/__tests__/query.test.ts で parseCleanLogQuery() にも it.each で当てる★(ほかのパラメータは付けない)。ok のケース(P-1〜P-6)は parsed.query.schedule を見る。422 のケース(P-7)は status 422・message「scheduleの値が不正です」・details を toEqual で丸ごと見るcomponents/ui/ScheduleFilterSelect.tsx、ErrorsScreen.tsx の置き換え、DetailLogScreen.tsx・.module.cssapp/globals.css(--sort-mark・--font-ja と body の font-family)、SortableTh.tsx・.module.css(印・色・フォント・コメント)、lib/table-sort.ts:46 のコメントdocs/decisions.md に後で書く)#1e293b(白は背景に溶けるため。監督の判断)。印の字は日本語フォントでそろえるschedule(エラー一覧と同じ3択・同じ文言)。予定ありの中身は2画面で違う(エラー一覧=clean_timetable_id が 0・NULL 以外、清掃詳細ログ=予定の表の行)lib/api/schedule-filter.ts の1か所、プルダウンは components/ui/ScheduleFilterSelect.tsx の1か所(CSS は各画面)app/globals.css の --font-ja にまとめ、body と並べ替えの印の両方がそれを使う(並びの写しを作らない)schedule をデータ層(SQL)に降ろす案(ダッシュボードに波及するため)§12 の実装は監督が確認済み(tsc・lint・vitest 155ファイル・3826件、fixtures の画面)。そのあとのユーザー指示: 「文言を予定なし、予定ありに統一して/プルダウンだけじゃなくて、表の表示も、cursorでなくこちらで直して」
components/ui/ScheduleFilterSelect.tsx)、清掃詳細ログ・ダッシュボードの小さい表・CSV の「予定」列(CLEAN_LOG_UNSCHEDULED_LABEL。lib/format/clean-result.ts)、清掃面積の注記(CleanAreaScreen.tsx)。英訳は既存の「予定なし」"No schedule"schedule=scheduled / unscheduled)・型名・関数名・CleanLogItem.unscheduled・コメントや仕様書の概念語「予定外タスク」(契約と内部の用語。画面に出る文字だけを変えた)