Newer
Older
client_dwatch / docs / handoff / 指示書AL_当期KPIの絞り込みとレポート画面の粒度.md

指示書AL — 当期KPIを顧客で絞り、レポート画面を1本ずつ出す

作成: 2026-09-16 / ブランチ: develop / 前提: 指示書AK 完了(D-138)


★Cursor への指示文(ここから下をそのまま貼ってください)★

D-Watch の実装をお願いします。ブランチは develop です。

■ 最初に読むもの(この順で、全部)
  1. CLAUDE.md(全文。特に §4.2 KPI計算式 / §4.7 集計期間 / §7 禁止事項 / §9 実装規約)
  2. docs/handoff/指示書AL_当期KPIの絞り込みとレポート画面の粒度.md(全文)
  3. docs/decisions.md の D-138(grep -nE "^### D-(0|1)[0-9][0-9]" docs/decisions.md で
     位置を出してから読む。★番号順に並んでいません★)
  4. lib/db/index.ts の reportKpis()(確定期間と当期の2本立てを、先に読んで理解すること)

■ やること
  指示書AL の 段階1 から 段階5 までを、★番号順に★実行してください。

■ 段階ごとに必ずやること
  1. npx tsc --noEmit    → 0 であること
  2. npm run lint        → 0 であること
  3. npm test            → 全パス
  4. コミットする(★段階ごとに1コミット★。まとめない)

■ コミットの決まり
  ・git add -A は使わないでください。必ずファイルパスを明示してください
  ・コミットメッセージは日本語。「なぜそうしたか」を1〜2行入れてください
  ・push はしないでください

■ 絶対に触らないファイル
  README.md / docs/code-map.md /
  docs/handoff/管理画面_設計検討_20260910.md / .claude/worktrees/** /
  d-watch-mock.jsx / lib/db/prisma.ts
  ※ lib/judge/ は ★段階4 でだけ★ 触ります(grade.ts の比較演算子)。
    ★段階1〜3 では触らないでください★。lib/judge/advice.ts は段階4 でも触りません

■ 絶対にやらないこと
  ・生SQL / メインDBのスキーマ変更
  ・★閾値の「数値」を変えること★(段階4 で変えるのは★比較演算子だけ★。90 / 70 / 10 はそのまま)
  ・閾値のハードコード(lib/config から読むこと)
  ・新しい CSS に font-family を書くこと
  ・★確定期間の経路(customer_summaries を読むところ)を変えること★
    — 段階6(D-138)で査読を通った実装です。今回は当期だけを直します
  ・仕様が不明なところを推測で実装すること

■ 動作確認
  ★ローカルDB(dwatch_local)に 2グループ・3顧客・11施設 が入っています★
  (前回の検証で再シード済み。npm run seed:local 相当で作り直せます)。
  ★テストDB(133.125.238.144)には書き込まないでください★。
  期待される結果は指示書AL の §7 に書いてあります。

■ 終わったら
  ・段階ごとのコミットハッシュ
  ・tsc / lint / test の結果(テスト本数も)
  ・★§7 の動作確認を実際に行った結果★
  ・指示書どおりにできなかったところと、その理由
  ・判断に迷って自分で決めたところ(あれば全部)

0. なぜこの指示書があるか

★指示書AK の実装は、査読3本とも合格でした。★ ★落とし穴14個すべて回避、テスト 86ファイル・2,013件 全パス、tsc / lint は 0。★

★★直すのは、私(監督)が指示書AK で漏らした1箇所と、その巻き添えで起きている1件です。★★

# 何を漏らしたか
★1★ ★段階4 で絞り込みを効かせるルートを列挙したとき、/reports/kpi を入れ忘れた★。段階6 で確定期間だけを扱い、★当期を誰にも割り当てなかった★
★2★ ★段階6 で「顧客が定まらなければ422」と決めたが、★★画面がその422をどう扱うか★★を指定しなかった★

★★実装者に落ち度はありません。★★ ★むしろ①は実装者が自分で見つけて、D-138 §2・lib/types/index.ts・docs/handoff.md の 3箇所に残課題として記録しています。★ ★隠していません。★


1. ★① 当期のKPIが顧客で絞られていない★

1-1. 何が起きているか(実測)

★ローカルDB(3顧客)での実測★です。

顧客1 顧客2 顧客3 ★存在しないID★
★確定週の完了率★ 87.1 78.4 69.7 ★空(正しい)★
★★当期の完了率★★ ★★55.3★★ ★★55.3★★ ★★55.3★★ ★★55.3(空にならない)★★

★どの顧客を選んでも同じ数字★です。★全顧客の合算★になっています。

SQLでの検算:

customer_id=1 → 57.613889
customer_id=2 → 52.407143
customer_id=3 → 46.44
★全社        → 55.272727★   ← API が返している値

1-2. ★前期比がもっと悪い★

当期   = 全社の数字          ← 絞れていない
前期   = 選んだ1社の数字      ← 絞れている
前期比 = この2つの差分        ← ★意味のない値★

1-3. ★同じ画面の中で矛盾します★

KPIカード      全社の数字
下の施設一覧    選んだ1社だけ   ← facility-performance は当期も絞れている

★例外もテスト失敗も出ません。★

1-4. ★fixtures では絶対に見えません★

USE_FIXTURES=true  当期・customer_id=2 → ★200・空★(正しい)
Prisma 経路        当期・customer_id=2 → ★全社の数字★

★CLAUDE.md §9.5 の「切替で挙動が変わらない」が、当期だけ崩れています。★ ★fixtures で開発している限り、この不具合は一生見つかりません。★


2. 段階1 — 当期のKPIを顧客で絞る

2-1. 変える場所

ファイル 何を
lib/db/index.ts の reportKpis() 当期ブランチ ★cleanTimetable.findMany の where に施設の条件を足す★
lib/reports/query.ts の toReportKpiDbQuery() ★facility_ids を null 固定にしない★
app/api/reports/kpi/route.ts ★施設IDを解決して渡す★

★★resolveFacilityIdsByMasters() を使うこと★★ — ★他の3本(facility-performance / continuous-problems / error-breakdown)が既に使っている、同じ1本★です。 ★新しい解決関数を作らないこと。★

2-2. ★確定期間の経路は触らないこと★

★段階6(D-138)で査読を通った実装です。★ ★customer_summaries を customer_id で絞る部分は、1行も変えないこと。★

★今回足すのは「当期の詳細集計を施設で絞る」だけ★です。

2-3. ★★確定期間と当期で、同じ施設集合になること★★

★これが今回いちばん大事な点です。★

確定期間 → customer_summaries を customer_id で絞る    (顧客粒度)
当期     → clean_timetables を facility_id で絞る      (施設粒度)

★別の経路ですが、同じ顧客を指しているはずです。★

★★target.customerId から施設IDを解決すること★★ — ★リクエストの customer_id / group_id から別々に解決すると、ずれる余地が残ります。★

具体的には:

1) resolveTargetCustomer() で対象顧客を1社に決める(既存。変えない)
2) ★その顧客ID★ から施設IDを解決する
3) 確定期間はその顧客IDで、当期はその施設IDで絞る

★★group_id から直接施設を引かないこと★★ — ★傘下2社なら既に422で止まっている★ので、 ★段階1 に到達した時点で顧客は1社に定まっています。★

2-4. ★既存テストの期待値を作り直すこと★

app/api/reports/__tests__/route.test.ts の「#2 当期」は、 ★customer_id を付けずに施設1〜3を合算する前提★で書かれています。

★★いまの誤った挙動をテストが固定しているわけではありません★★ — ★customer_id 未指定で顧客1社なら、全施設が対象になるのが正しい★からです。

★ただし、絞り込みが入ると通らなくなる可能性があります。★ ★落ちたら、期待値を作り直すこと。★ ★テストを消して逃げないこと。★

2-5. 期待値表

★ローカルDB(2グループ・3顧客・11施設)を前提★とします。

# 入力 期待
★1-1★ 当期・customer_id=1 ★★顧客1の施設だけの数字★★(★全社の数字にならない★)
★1-2★ 当期・customer_id=2 ★1-1 と違う数字★
★1-3★ 当期・customer_id=9999 ★★空(value が null)★★(★★数字が出たら不具合★★)
1-4 当期・group_id=2(傘下1社) その顧客の数字
1-5 当期・指定なし・顧客1社 全施設(=その顧客の全施設)
1-6 当期・指定なし・顧客2社以上 ★422★(段階6 で既に止まる)
★1-7★ ★確定期間と当期の両方を含む期間★ ★★両方が同じ顧客の数字であること★★
★1-8★ ★前期比★ ★当期・前期とも同じ顧客の数字から計算されていること★

★★1-3 が今回の要です★★ — ★存在しない顧客を指定して数字が出るなら、絞れていません。★

★1-7 と 1-8 は、実データ(ローカルDB)で確かめること★。★テストだけで済ませないこと。★

2-6. fixtures 側

★fixtures 経路は既に正しく動いています★(customer_id=2 で空)。 ★壊さないこと。★ ★Prisma 側と挙動が揃うことを確かめること。★


3. ★② レポート画面が、1本の失敗で4本とも消える★

3-1. 何が起きているか

components/features/reports/ReportsScreen.tsx は4本を Promise.all で取り、 ★1本でも失敗すると画面全体をエラー表示にします★。

/reports/kpi                    ← 顧客が定まらないと422
/reports/facility-performance   ← 施設単位。合算に問題なし
/reports/continuous-problems    ← 同上
/reports/error-breakdown        ← 同上

★KPIだけが422になっても、残り3本は問題なく出せます。★ ★いまは画面ごと「顧客またはグループを指定してください」の1行になります。★

3-2. ★fixtures で開発できなくなっています★

★fixtures が4顧客になった★ため、★USE_FIXTURES=true だと分類を選ぶまで レポート画面が何も見られません★。

★実運用(顧客1社)では発火しません★が、★開発中の人が最初に踏みます★。

3-3. ★どう直すか(監督の決定)★

1本ずつ、成功したものは出す。失敗したブロックだけにメッセージを出す。

★Promise.all を Promise.allSettled に変える★のが素直です。

★★ただし、次の2つを守ること★★:

  • ★どのブロックが失敗したのかが分かること★ — ★「読み込みに失敗しました」だけでは、 どれが欠けているのか分かりません★
  • ★★全部失敗したときは、今までどおり画面全体のエラーにすること★★ — ★4つとも空の枠が並ぶより、1行で「読み込めませんでした」のほうが分かります★

3-4. ★422 のときは「選んでください」と誘導すること★

★APIは候補を返しています★(details.customer_ids)。★画面がそれを捨てています。★

KPIのブロックだけに:
  「顧客を選んでください」
  + ★選択肢が3つあることが分かる形★(件数を出す、屋号セレクトを強調する、など)

★具体的な見せ方は実装者判断★です。★ただし「文言を読ませるだけ」で終わらせないこと。★ ★details.customer_ids を使うこと。★

3-5. 期待値表

# 状況 期待
2-1 4本すべて成功 ★今までどおり4ブロック★
★2-2★ ★KPIだけ422★ ★★残り3ブロックが出る★★+KPIのブロックに誘導
2-3 KPIだけ500 ★残り3ブロックが出る★+KPIのブロックにエラー
★2-4★ ★4本とも失敗★ ★★画面全体のエラー(今までどおり)★★
2-5 施設別だけ失敗 残り3ブロックが出る

★★2-2 と 2-4 の両方をテストに入れること。★★

★画面のテストは走りません★(vitest.config.ts の include は lib/** と app/**)。 ★「どのブロックを出すか」を決める部分を lib/ 側の純粋関数に切り出すこと★ — ★指示書AJ で lib/dashboard/rescue-visibility.ts を切り出したのと同じ形★です。


4. 段階3 — 細かい直し4件

★どれも小さいですが、放っておくと次の人が誤読します。★

4-1. YAML の古い一文

d-watch-api.yaml の GroupId の description 末尾:

絞り込みの適用はエンドポイントごとに順次有効化中(指示書AK 段階4)。

★段階4 は完了しました。★ ★この一文は今は正しくありません。★ ★消すか、現状に合わせること。★

4-2. docs/handoff.md の欠け

指示書AK §12-2 の4項目のうち、★「点検記録の絞り込みは、画面が復活したときに足すこと」★が 残課題リストに入っていません(★本文に「対象外」と書かれているだけ★)。

★残課題として足すこと。★ ★画面が復活したときに思い出せる形にすること。★

4-3. 不要な顧客マスタの読み込み

app/api/reports/kpi/route.ts は、★customer_id が明示されていても顧客マスタを読んでいます★。

★resolveTargetCustomer() は customerId !== null のとき顧客リストを見ません★ので、 ★この読み込みは不要★です。★毎リクエスト1クエリ増えています。★

★customer_id が明示されているときは読まないようにすること。★

4-4. customers() に並び順が無い

lib/db/index.ts の customers() に orderBy がありません。 ★422 の details.customer_ids の並びがDB任せ★になっています。

★id の昇順にすること★(/api/customers の並びと揃える)。 ★テストはスタブ順で通ってしまうので、実DBで確かめること。★


5. 段階4 — ★判定の境界を「含める」に変える★

5-1. ユーザー指示(原文)

◯、×は90%以上のように境はふくめてください

確認したところ:

10%未満を×としてください

★確定:★

◎  完了率 90%以上
○  完了率 70%以上
△  完了率 10%以上
×  完了率 10%未満、または未実施

★★すべての境界が「上のランクに入る」で揃います。★★

5-2. ★何が変わるか★

完了率 今 ★変更後★
90.1 ◎ ◎
★90.0★ ★○★ ★★◎★★
89.9 ○ ○
70.1 ○ ○
★70.0★ ★△★ ★★○★★
69.9 △ △
10.1 △ △
10.0 △ ★△(変わらない)★
9.9 × ×
null(未実施) × ×

★★変わるのは 90.0 と 70.0 の2つだけです。★★

5-3. ★これまでの決定を覆します★

docs/decisions.md の ★D-020b★ が、★逆の規則★を定めています。

  • 決定: 完了率 90.0 は ○、70.0 は △、10.0 は △(×ではない)
  • 理由: ◎は「90%超」、○は「70%超」と、★境界値そのものは下のランクに落ちる規則で統一されている★

★★D-020b の見出しに [破棄] の印と行き先を書くこと★★ (D-024 / D-067 / D-053 / D-133 / D-135 と同じ作法)。

★★CLAUDE.md §4.1 は「確定仕様」であり、その本文を書き換えます。★★ ★これは軽い変更ではありません。★ ★ユーザーの明示指示(2026-09-16)に基づくことを、 記録に原文つきで残すこと。★

5-4. ★★見落としやすい連鎖 — 「問題あり」の判定★★

★ここが今回いちばん危ないところです。★

lib/db/clean-log-mapping.ts:282

return completionRate === null || completionRate <= GRADE_THRESHOLDS.good;

★これは「△以下=問題あり」を完了率から直接判定しています。★ ★70.0 が △ だったので <= が正しかった★のですが、 ★★70.0 が ○ になるので < に変えないと、○ の日が「問題あり」のままになります。★★

★この関数が効いている先★:

連続問題日数 → L1 / L2 / L3 → R3 / R4 のアドバイス
              → 要レスキュー店舗の R-2(連続問題レベル3)
              → レポートの「連続問題施設」

★★直し忘れても例外は出ません。★★ ★完了率ちょうど70.0の日だけが、静かに問題日として数えられ続けます。★

★lib/judge/grade.ts の docstring にも、この関係が明記されています★:

あちらは完了率(生値)を入力に取り rate === null || rate <= GRADE_THRESHOLDS.good で判定する。 ★判定結果は本関数と常に一致する★(judgeGrade() の「○ = 70%超」より、△× ⟺ null または 70 以下)。 ★入力が違うだけの同じ規則★なので、★★片方の閾値だけを動かさないこと★★。

★★この注記を書き換えること。★★ ★「70 以下」→「70 未満」、「○ = 70%超」→「○ = 70%以上」。★

5-5. 変える場所

ファイル 何を
★lib/judge/grade.ts★ rate > excellent → ★>=★ / rate > good → ★>=★ / ★rate < poor はそのまま★
同上 ★docstring の表と、isProblemGrade() の長い注記★
★lib/db/clean-log-mapping.ts★ ★<= GRADE_THRESHOLDS.good → <★(§5-4)
lib/config/index.ts ★GRADE_THRESHOLDS のコメント★(「この値を超えると ◎」→「以上で ◎」)。★数値は変えない★
★CLAUDE.md §4.1★ 判定基準の表
★CLAUDE.md §9.4★ ★境界値のリスト★(「90.4 → 表示90だが判定は◎、89.5 → 表示90だが判定は○」はそのまま正しい)
docs/fixtures.md ★境界値表★
docs/decisions.md ★D-020b に [破棄] の印★
d-watch-api.yaml ★grade の説明に「超」「以下」が書かれていないか確かめること★

★★GRADE_THRESHOLDS の数値(90 / 70 / 10)は変えないこと。★★ ★比較演算子だけです。★

5-6. ★確かめるが、勝手に変えないもの★

lib/fixtures/clean-logs.ts:226

rate === null || rate < GRADE_THRESHOLDS.good ? 1 : 0,

★これは fixtures のデータ生成で、既に < です。★ ★「問題あり」と同じ概念なのか、別の概念なのかを確かめてください。★

  • ★同じ概念なら★: 今回の変更後は < が正しいので、★そのままでよい★
  • ★別の概念なら★: ★触らないこと★

★どちらか判断できなければ、手を止めて報告すること。★ ★推測で変えないこと。★

lib/judge/advice.ts:66

completionRate < GRADE_THRESHOLDS.poor

★R2「完了率×(10%未満)」の判定です。★ ★× の境界は変わらないので、< のままで正しい★です。 ★触らないこと。★

5-7. 期待値表

★CLAUDE.md §9.4 が「境界値を必ず含める」と定めています。★ ★10個すべてテストに入れること。★

# 入力 期待
4-1 90.1 ◎
★4-2★ ★90.0★ ★★◎★★(★今は○。ここが変わる★)
4-3 89.9 ○
4-4 70.1 ○
★4-5★ ★70.0★ ★★○★★(★今は△。ここが変わる★)
4-6 69.9 △
4-7 10.1 △
4-8 ★10.0★ ★△(変わらない)★
4-9 9.9 ×
4-10 null ×

★「問題あり」の期待値表★(§5-4):

# 完了率 期待
★4-11★ ★70.0★ ★★問題なし★★(★今は問題あり★)
4-12 69.9 問題あり
4-13 70.1 問題なし
4-14 null 問題あり

★★4-11 を必ず入れること。★★ ★ここが漏れると、連続日数が静かに1日多く数えられます。★

5-8. ★既存テストが落ちます★

★境界値を固定しているテストが複数あります。★ ★期待値を新しい規則に直すこと。★ ★★テストを消して逃げないこと。★★

★落ちたテストの一覧を報告に含めること★ — ★想定外の場所が落ちたら、それは連鎖の見落としです。★


6. 段階5 — 記録

6-1. docs/decisions.md

★★採番の前に必ず実測すること★★:

grep -nE "^### D-(0|1)[0-9][0-9]" docs/decisions.md | tail -3

★番号順に並んでいません。★

書くこと:

  • ★当期KPIを顧客で絞ったこと★と、★★確定期間と当期で同じ施設集合になるよう target.customerId から解決した理由★★
  • ★D-138 に残課題として書かれていた項目が解決したこと★ — ★★D-138 側の残課題の記述にも「D-1xx で解決」と印を付けること★★ (★D-053 / D-133 / D-135 と同じ作法★)
  • ★レポート画面を1本ずつ出すようにした理由★(★fixtures で開発できなくなっていた★)
  • ★全部失敗したときだけ画面全体のエラーにする、という切り分けの理由★

6-2. docs/handoff.md に追記

  • ★USE_FIXTURES の true / false で、レポート画面の見え方が揃ったこと★
  • ★★段階7 の画面(分類セレクト・カスケード)の目視確認が、まだ行われていないこと★★ — ★査読3本とも「ログインが要るので未確認」と報告しています★
  • ★group_id = 0 の顧客が、ローカルDBに投入できないままであること★(第9回 Q7-4 待ち)
  • ★4画面は「1操作=1リクエスト」であること★(★レポート画面だけが検索ボタンで確定★)— ★モックに検索ボタンが無いための判断で、記録済み★。★変えるなら別の判断が要る★

7. ★動作確認★

7-1. 前提

★ローカルDB(dwatch_local)に 2グループ・3顧客・11施設 が入っています。★ ★作り直すなら scripts/seed-local-db.ts。★ ★★テストDB(133.125.238.144)には書き込まないこと。★★

★.env.local はテストDBを向いています。★ ★書き換えないこと★ — ★環境変数を上書きして別に起動するか、ルートハンドラを直接呼ぶこと。★

7-2. 確かめること

/api/reports/kpi?period=monthly&year=2026&month=9&customer_id=1
/api/reports/kpi?period=monthly&year=2026&month=9&customer_id=2
/api/reports/kpi?period=monthly&year=2026&month=9&customer_id=9999
# 期待
★6-1★ ★★当期の数字が、顧客1 と 顧客2 で違うこと★★
★6-2★ ★★customer_id=9999 で当期が空になること★★(★数字が出たら直っていない★)
6-3 確定期間の数字が、指示書AK のときと変わっていないこと
★6-4★ ★前期比が、当期・前期とも同じ顧客から計算されていること★
6-5 指定なし・3顧客 → ★422(今までどおり)★

★★自分でSQLを書いて検算すること。★★ ★APIの出力をそのまま信じないこと。★

7-3. レポート画面

★ログインが要るので、画面の目視は難しいかもしれません。★ ★その場合は、切り出した純粋関数のテストと、API の応答で確かめること。★ ★★「目視できなかった」と正直に報告してよい。★★ ★推測で「動いているはず」と書かないこと。★


8. ★実装者へ — 特に踏みやすい落とし穴★

# 落とし穴 どうなるか
★1★ ★当期を group_id から直接絞る★ ★確定期間(顧客粒度)と母集団がずれる★。★段階1 に来た時点で顧客は1社に定まっている★
★2★ ★確定期間の経路も一緒に直す★ ★査読を通った実装を壊す。今回は当期だけ★
★3★ ★facility_ids を ?.length で分岐する★ ★「0件ヒット」が「全件」に化ける。例外もテスト失敗も出ない★
★4★ ★落ちたテストを消して逃げる★ ★期待値を作り直すこと★
★5★ ★Promise.allSettled にしたが、どれが失敗したか分からない★ 欠けているブロックに気づけない
★6★ ★全部失敗しても空の枠を4つ並べる★ 1行で「読み込めませんでした」のほうが分かる
★7★ ★details.customer_ids を捨てる★ ★APIが用意した候補が活きない★
★8★ ★D-138 の残課題に印を付けない★ ★次の人が「まだ直っていない」と読む★
★9★ ★画面に判定を書く★ ★画面のテストは走らない★。lib/ に切り出すこと
★10★ ★テストDBに書き込む★ ★★読み取り専用★★
★★11★★ ★★judgeGrade() だけ直して isProblemCompletion() を直し忘れる★★ ★★完了率ちょうど70.0の日が「問題あり」のまま数えられ続ける★★。★連続日数 → L1〜L3 → R3/R4 → 要レスキューまで波及する★。★例外もテスト失敗も出ない★
★12★ ★rate < GRADE_THRESHOLDS.poor も <= にする★ ★★× の境界は変えません★★(「10%未満が×」でユーザー確定)。★10.0 が × に落ちる★
★13★ ★GRADE_THRESHOLDS の数値を変える★ ★変えるのは比較演算子だけ★。90 / 70 / 10 はそのまま
★14★ ★lib/judge/advice.ts を直す★ ★R2 は「×(10%未満)」で、× の境界は変わらない★。★触らないこと★
★15★ ★D-020b と CLAUDE.md §4.1 を直さない★ ★★コードと確定仕様が食い違う★★。次の人がどちらを信じるか分からなくなる
★16★ ★lib/fixtures/clean-logs.ts を確かめずに変える★ ★同じ概念か別概念かを先に判断すること★。★判断できなければ手を止めて報告★

9. やらないこと(★この指示書の範囲外★)

項目 なぜ
★4画面の draft / 確定の分離★ ★モックに検索ボタンが無いための判断で、記録済み★。★変えるなら別の判断★
★総清掃面積の新画面★ ★次の指示書★
★複数顧客の合算★ ★4指標のうち2つを合算する材料がDBに無い★(第9回 6章で照会中)
★group_id = 0 の投入★ ★DDLの話。DFAは直せない★(第9回 Q7-4)
★200件超の実行計画対策★ 測れる見積もりが無い
★点検記録の絞り込み★ 画面自体が未実装