Newer
Older
client_dwatch / docs / handoff / 指示書AN_総清掃面積の画面を作る.md

指示書AN — 総清掃面積の画面を作る

作成: 2026-09-16 / ブランチ: develop / 前提: 指示書AM 完了(D-139 §8)


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

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

■ 最初に読むもの(この順で、全部)
  1. CLAUDE.md(全文。特に §4.2 KPI計算式 / §4.3 端数処理 / §5 モックが正 /
     §7 禁止事項 / §9 実装規約)
  2. docs/handoff/指示書AN_総清掃面積の画面を作る.md(全文)
  3. components/features/reports/ReportsScreen.tsx(★画面の手本★)
  4. lib/hooks/use-classification-filter.ts と lib/masters/options.ts
     (★分類の絞り込みは既にある。作り直さない★)
  5. lib/db/report-mapping.ts の aggregateCleanStatsByFacility()
     (★面積のΣを既にやっている。読んでから §3 を読むこと★)

■ やること
  指示書AN の 段階1 から 段階7 までを、★番号順に★実行してください。
  ★段階2(集計の純粋関数)は「期待値表をテストに落としてから実装する」
    順序を崩さないでください★。

■ 段階ごとに必ずやること
  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/judge/** / lib/db/prisma.ts

■ 絶対にやらないこと
  ・生SQL($queryRaw / $executeRaw / *Unsafe / Prisma.raw)— ESLint が落とします
  ・メインDBのスキーマ変更・マイグレーション作成
  ・★単位の文字列を画面やAPIに直書きすること★ — lib/config の AREA_UNIT から読むこと(§2-4)
  ・閾値・件数のハードコード
  ・新しい CSS に font-family を書くこと
  ・仕様が不明なところを推測で実装すること

■ 動作確認
  ★テストDB(133.125.238.144)は読み取り専用★。書き込まないでください。
  ★ローカルDB(dwatch_local)には 2グループ・3顧客・11施設★が入っています。
  期待される結果は指示書AN の §9 に書いてあります。
  ★dev server はポート3000で動いています(起動済み)★。

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

0. この指示書の位置づけ

★ユーザーの新要件4件の、最後の1件★です。

要件 状態
要レスキュー店舗 ★完了★(D-135 / D-137)
詳細ログの期間上限2年 + CSV上限 ★完了★(D-136)
分類の復活 ★完了★(D-138 / D-140)
★総清掃面積の新画面★ ★★この指示書★★

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

・総清掃面積(指定期間での絞り込みも) モニタリングに新項目追加、累計の清掃総面積表示、ホールディングス、各グループ(ブランド)、店舗、指定期間での絞り込み

確認したところ:

新画面

★確定: 左メニューの「モニタリング」に4つ目の画面を追加する。★


1. ★先に知っておくこと(調査で分かった事実)★

★どれも実測です。★ ★設計の前提なので、着手前に読むこと。★

1-1. 面積はどこにあるか

clean_status_histories.clean_area   実際に清掃した面積(分子)
clean_status_histories.task_area    清掃対象の面積(分母)

★CLAUDE.md §4.2 の「清掃完了率 = Σ清掃面積 ÷ Σ清掃対象面積」の、まさにその2つ★です。

1-2. ★★サマリテーブルの面積は使えません★★

facility_summaries / customer_summaries にも task_area / clean_area があります。 ★合計になっていません。★ 実測:

task_area ★全24行が 1000 ちょうど★(task_count = 100 の行でも 1000)
clean_area ★8月の月次と9月第2週で、施設ごとに完全に同じ値★

★期間が違うのに同じ値★です。★合計ではありません。★

★★TeamWorks に照会中★★(TeamWorks確認事項_第9回_20260915.md の Q2-1)。

★したがって、この画面は常に clean_status_histories から集計します。★

★D-102(「サマリはサマリテーブルを読む。履歴から再計算しない」)との関係★: ★D-102 の対象は KPI 4指標です。★ ★面積は画面に出していなかったので、対象外★と読みます。 ★★この判断を記録に残すこと。★★

1-3. ★丸め誤差は問題になりません(実測済み)★

行数 double の順次加算と厳密値の差 整数表示
538(実データ) ★0★ 一致
262,800(2年ぶん) ★0.000006★ ★一致★
1,440,000 ★0.00004★ ★一致★

★★Kahan 加算も整数化も要りません。★★ ★普通に足してください。★

1-4. ★面積を画面に出すのは初めてです★

★d-watch-api.yaml に面積のスキーマが1つもありません。★ ★YAML の追加から始まります★(CLAUDE.md §6-1)。

1-5. ★索引が無い★

clean_status_histories.started_at   ★索引なし★

★期間で絞っても全行を走査します。★ ★第9回 1-4 で依頼中★です。

★★ただし今回は致命的ではありません★★ — ★1本の集計クエリの全走査★であり、 ★詳細ログのような「塊ごとに全走査 × 254回」にはなりません★。


2. ★監督が決めたこと(実装前に読むこと。覆すなら理由を残すこと)★

2-1. 画面名

★「清掃面積」(📐)★

★既存は「清掃詳細ログ / アラート / レポート」で、名詞2〜5文字・動詞なし★です。★語調を揃えます。★

★★「総清掃面積」「累計清掃面積」にしないこと★★ — ★期間で絞れる画面なので、「総」「累計」を名前に入れると定義を先に固定してしまいます。★

2-2. ★「累計」と「指定期間」をどう両立させるか★

期間の既定 = 未指定(=全期間)
期間を指定すると、その範囲の合計になる

★★既定が「全期間」なので、開いた瞬間が「累計」です。★★ ★期間を入れれば「指定期間の合計」になります。★ ★1つの仕組みで両方を満たします。★

★★注意: データの保持は2年★★(docs/spec.md)。★厳密には「累計」ではなく「直近2年の合計」★です。 ★★画面にその旨を明記すること★★ — ★書かないと、3年目に数字が減ったときに問い合わせが来ます。★

2-3. 期間の単位

★自由な日付範囲(date_from / date_to)。週・月の粒度にしない。★

★理由★: ★サマリを読まないので、週・月の粒度に揃える必要がありません。★ ★月跨ぎ週の帰属ルール(D-071)に乗る必要もなくなります★ — ★新しい期間バグを作らずに済みます。★

★上限★: ★設けない★(詳細ログと同じ考え方)。★1本の集計クエリなので、期間が広くても行数は増えません★ (★返るのは施設数ぶん★)。

2-4. ★単位は「㎡」で表示する(ユーザー確定 2026-09-16)★

ユーザー指示(原文):

単位は㎡でよいです

★確定: ㎡ を表示する。★

★★ただし、TeamWorks からの確認は取れていません。★★ ★その事実は消えません。★

  • ★DFA側の資料に「㎡」と書かれています★が、★★これはDFAが書いたもの★★です
  • ★TeamWorks からの回答は受け取っていません★(第9回 5章で照会中)
  • ★実データは task_area が全行 1000 ちょうどで、★★桁から推定することもできません★★★

★★つまりこれは「DFAの判断で ㎡ として表示する」という決定です。★★ ★★もし TeamWorks の回答が違えば、全画面が間違った単位を主張することになります。★★ ★★記録に、ユーザーの原文と、この risk の両方を残すこと。★★ ★第9回 5章の照会は取り下げないこと。★

★★単位の文字列は lib/config/index.ts の1箇所に置くこと★★ — ★回答で変わったときに1行で直せるようにするためです。★ ★画面やAPIに "㎡" を直書きしないこと。★

★見出しは「清掃面積」「清掃対象面積」で、★数値の側に単位を付ける★。

2-5. ★集計はSQLに降ろす。ただし番人を置く★

★groupBy + _sum を使います★(★lib/db/index.ts に controlReport.groupBy の前例が1本あります★)。

prisma.cleanStatusHistory.groupBy({
  by: ["facility_id"],
  where: { started_at: {...}, ...(facilityIds !== null ? { facility_id: { in: facilityIds } } : {}) },
  _sum: { clean_area: true, task_area: true },
  _count: { _all: true },
})

★返るのは最大でも施設数ぶん(200行程度)★です。★期間の広さに関係しません。★

★★ただし危険が1つあります★★:

1つの clean_timetable に clean_status_histories が2行以上あると、
SUM() が同じ清掃の面積を2回数える。
★例外もテスト失敗も出ない。数字が大きくなるだけ。★

★実データは全538行が1対1★でしたが、★既存コードは「最新1行に畳んでから足す」実装★になっており、 ★N行を想定しています★(latestHistoryByTimetableId())。

★★TeamWorks に照会中★★(第9回 Q2-3)。

★決定: groupBy + _sum を使う。そのうえで番人を置く。★

★番人★: ★集計と同時に「1つの clean_timetable_id に履歴が2行以上ある件数」を数え、 0でなければ logEvent("warn", ...) を出すこと★。

★★静かに壊れる代わりに、ログに出るようにします。★★ ★lib/logger/events.ts の既存の作法(data.anomaly)に倣うこと。★ ★★原因の断定は書かないこと★★(docs/logging-design.md §7)。★差分だけを残す。★

2-6. ★完了率も出すこと★

★面積だけでは意味が読み取りにくい★です。

清掃面積        341,635
清掃対象面積    494,000
完了率          69%

★完了率は lib/judge/kpi.ts の completionRate() を使うこと★。★自前で割り算を書かないこと。★ ★分母0で null を返します。★ ★∞ や NaN を出さない。★

★★レポート画面のKPIと数字が違う可能性があります★★ — ★経路が違う★ためです (★レポートは確定期間をサマリから読む★)。★この画面は常に履歴から集計します。★ ★★この差を記録に書くこと。★★

2-7. 分類の絞り込み

★作り直さないこと。★ ★既にあります。★

lib/hooks/use-classification-filter.ts   画面側の状態と取得
lib/masters/options.ts                   選択肢の組み立てとカスケード
resolveFacilityIdsByMasters()            group_id / customer_id → 施設IDの配列

★★facility_ids の null と [] を区別すること★★(!== null で分岐)。 ★?.length で判定すると「0件ヒット」が「全件」に化けます。★


3. ★触ってはいけないもの★

対象 理由
README.md / docs/code-map.md / docs/handoff/管理画面_設計検討_20260910.md / .claude/worktrees/** ★別作業★
d-watch-mock.jsx ★モックは原型として保存する★(§7)
★lib/judge/**★ ★判定ロジックは変えません★
lib/db/prisma.ts ★未コミットの変更が乗っている★
★テストDB(133.125.238.144)への書き込み★ ★★読み取り専用★★

★★禁止★★: git add -A / 生SQL / スキーマ変更 / ★単位文字列の直書き★ / 閾値・件数のハードコード / ★新しいCSSへの font-family★ / 推測での実装


4. 段階1 — 設定値

lib/config/index.ts に足す。

/**
 * 面積の表示単位(ユーザー指示 2026-09-16「単位は㎡でよいです」/ 指示書AN)
 *
 * ★★これは DFA の判断であって、TeamWorks の確認ではない★★ —
 *   ★DFA側の資料に「㎡」とあるが、それはDFAが書いたもの★。
 *   ★TeamWorks からの回答は受け取っていない★(第9回 5章で照会中)。
 *   ★実データは task_area が全行 1000 ちょうどで、桁から推定することもできない★。
 *
 * ★★回答が違っていたら、全画面が間違った単位を主張することになる★★。
 *   ★だから1箇所に置いている。直すのはここ1行で済む★。
 *   ★★画面やAPIに "㎡" を直書きしないこと。★★
 */
export const AREA_UNIT = "㎡" as const;

/**
 * 清掃面積の画面で、内訳を何件まで出すか(指示書AN)
 *
 * ★施設数が上限★(施設ごとに最大1行)。★「もっと表示」で全件開く★のは
 *   要レスキュー(D-137)と同じ作法。
 */
export const CLEAN_AREA_INITIAL_VISIBLE = 10;

★数値を直書きしないこと。★ ★必ずここから読むこと。★


5. 段階2 — 集計の純粋関数(★テストが先★)

5-1. 置き場所

★新規 lib/clean-area/★(lib/clean-logs/ と同じ階層)。

★★components/ に判定や集計を書かないこと★★ — ★vitest.config.ts の include は lib/** と app/** で、components/** は走りません★(D-120 項2)。

5-2. 何をする関数か

施設ごとの { facility_id, clean_area, task_area, executed_count }
            + 施設マスタ(customer_id / customer_name / group_id / group_name)
     → 粒度(group / customer / facility)でロールアップした配列
     + 全体の合計

★純粋関数にすること★。★DBアクセスを混ぜない。★

5-3. ★期待値表(先にテストへ)★

A群: 合計そのもの

# 入力 期待
A-1 0件 clean_area: 0 / task_area: 0 / ★completion_rate: null★(★0% ではない★)
A-2 _sum が null(Prisma の0件時) ★?? 0 で 0★。★null が画面に漏れないこと★
A-3 task_area = 0 の行だけ(=未実施) 合計0 / ★completion_rate: null★
★A-4★ ★clean_area > 0 かつ task_area = 0★ ★★completion_rate: null★★(★∞ / NaN にしない★)
A-5 小数を持つ行の合計 厳密値と一致(§1-3。★誤差が出たら実装が壊れた合図★)

B群: 組織のロールアップ

# 入力 期待
B-1 1グループ → 1屋号 → 3店舗 ★グループ合計 = 屋号合計 = 3店舗の和★
B-2 1グループ → 2屋号 → 各2店舗 ★各階層で和が一致★
★B-3★ ★group_id = null の顧客★ ★★「グループなし」として1行にまとめる★★。★「グループ0」を作らない★。★★その屋号の面積が全体合計から落ちないこと★★
B-4 施設マスタに無い facility_id ★落ちないこと★。★どう扱うかを決めて、理由をコメントに書くこと★
★B-5★ ★全体合計★ ★★粒度を変えても全体合計は変わらないこと★★

★★B-5 が要です★★ — ★粒度タブを切り替えて合計が変わったら、ロールアップが壊れています。★

C群: 並びと表示

# 入力 期待
C-1 面積がばらばら ★clean_area の降順★
C-2 同じ面積が2件 ★IDの昇順★(★安定した順序★)
C-3 11件・初期表示10件 ★10件+「もっと表示(あと1件)」★
C-4 ★ちょうど10件★ ★★「もっと表示」を出さない★★

★C-3 と C-4 の両方を入れること★(> と >= の取り違え防止)。


6. 段階3 — データ取得

6-1. DataSource に追加

★fixtureSource と prismaSource の両方に実装すること★。 ★★fixtures 側を notImplemented にしないこと★★。

6-2. Prisma 側

★§2-5 のとおり groupBy + _sum。★

★★include を使わないこと★★ — ★親行の数ぶん IN が膨らみます★(D-136 で実測済み)。 ★施設マスタは別に1回読んで、JSで突き合わせること★(★施設は200件程度★)。

6-3. ★★番人(§2-5)★★

1つの clean_timetable_id に履歴が2行以上ある件数を数える
0 でなければ logEvent("warn", { event: "data.anomaly", metric: "...", expected: 0, actual: n })

★★原因の断定を書かないこと★★。★差分だけを残す★(docs/logging-design.md §7)。

★このクエリが重くならないようにすること★ — ★対象期間・対象施設に絞って数えること★。 ★全期間・全施設を毎回数え直さない。★

6-4. fixtures 側

★lib/fixtures/ の既存データから、同じ集計を通すこと★(CLAUDE.md §9.5)。 ★★fixtures 用に別の集計を書かないこと。★★

★面積を持つ fixtures は lib/fixtures/areas.ts にあります★。★足りなければ足すこと★ (★足したら、既存のテストが落ちないか確かめる★)。


7. 段階4 — API契約とルート

7-1. ★YAML を先に直すこと★(CLAUDE.md §6-1)

7-2. 案

  /clean-area:
    get:
      tags: [clean-area]
      summary: 清掃面積の集計(ホールディングス/屋号/店舗 × 期間)
      description: |
        指定期間の清掃面積を、指定した粒度で集計して返す。

        ★clean_status_histories の clean_area / task_area の合計★。
        ★サマリテーブルの task_area / clean_area は読まない★ —
        実データで全行 1000 固定であり、期間内の合計になっていないため(指示書AN §1-2。
        TeamWorks 第9回 Q2-1 で照会中)。
        ★group_summaries は存在しない★ため、ホールディングス単位は
        facilities → customers → groups のロールアップで作る。

        ★期間を指定しないときは全期間★。ただしデータの保持は2年なので、
        実際には直近2年の合計になる(docs/spec.md)。

        ★単位は ㎡(ユーザー確定 2026-09-16)。ただし TeamWorks の確認は未取得(第9回 5章で照会中)★。
      parameters:
        - name: date_from
          in: query
          schema: { type: string, format: date }
          description: 清掃開始日時の範囲(from)。★省略時は下限なし★
        - name: date_to
          in: query
          schema: { type: string, format: date }
          description: 清掃開始日時の範囲(to)。★省略時は上限なし★。★日数の上限は設けない★
        - $ref: "#/components/parameters/GroupId"
        - $ref: "#/components/parameters/CustomerId"
        - name: facility_name
          in: query
          schema: { type: string }
          description: 施設名(部分一致)。記号と大文字小文字を無視して照合する(D-129)
        - name: group_by
          in: query
          schema:
            type: string
            enum: [group, customer, facility]
            default: facility
          description: 集計の粒度
      responses:
        "200": ...  # total(全体合計)+ items(内訳)+ meta
        "401": { $ref: "#/components/responses/Unauthorized" }
        "422": { $ref: "#/components/responses/ValidationError" }

★items の各行★:

scope / id(nullable。★グループなしは null★)/ name /
group_id / group_name / customer_id / customer_name /
clean_area / task_area / completion_rate(nullable)/ executed_count

★★completion_rate は丸めない生値★★(CLAUDE.md §9.3)。

7-3. ルート

★手本は app/api/reports/facility-performance/route.ts★(★分類の絞り込みが既に入っている★)。

  • ★認証はルートに書かない★(proxy.ts が横断処理)
  • ★GET だけ★
  • ★resolveFacilityIdsByMasters() を使うこと★。★新しい解決関数を作らない★

8. 段階5 — 画面

8-1. メニューに足す位置

components/layout/AppShell.tsx の MENU_MONITORING:

{ href: "/detail-log", ... },
{ href: "/alerts", ... },
{ href: "/reports", ... },
★ここに足す★
// ★デモまで一時的に隠す(D-079 項3)★  ← 点検記録のコメント塊

★★点検記録のコメント塊より前に置くこと★★ — ★後ろに置くとコメントアウトの続きに見えます。★

8-2. 作るファイル

ファイル 手本
app/(dashboard)/clean-area/page.tsx(3行) app/(dashboard)/reports/page.tsx
components/features/clean-area/CleanAreaScreen.tsx components/features/reports/ReportsScreen.tsx
components/features/clean-area/CleanAreaScreen.module.css 同ディレクトリ
lib/i18n.ts に語を追加 —

8-3. 画面の構成

┌ ヘッダ ─────────────────────────────┐
│ h1「清掃面積」                            │
├ 絞り込み ────────────────────────────┤
│ [全ホールディングス▼][全屋号▼][施設名__][検索] │ ← ★既存の共有ロジックに乗せる★
│ [開始日___][終了日___]                      │ ← ★未指定なら全期間★
├ 数値カード ──────────────────────────┤
│ 清掃面積 341,635 / 清掃対象面積 494,000 / 完了率 69% │
│ ★「直近2年ぶん」と明記★                      │
├ 内訳 ───────────────────────────────┤
│ [ホールディングス][屋号][店舗] ← 粒度タブ        │
│ 面積の降順。初期10件+「もっと表示」            │
└────────────────────────────────────┘

8-4. 作法

  • ★数値は3桁区切りにすること★(341635 は読めません)。 ★★lib/format/ に数値整形の関数がありません。新設すること★★ (★toLocaleString / Intl.NumberFormat の使用は本番コードに0件★)
  • ★★単位は AREA_UNIT から出すこと★★(§2-4)。★"㎡" を直書きしない★
  • ★取得は apiFetch★。★fetch を画面に直書きしない★
  • ★draft と確定を分けること★(★検索ボタンを置く★。ReportsScreen と同じ)
  • ★色は CSS 変数★。★★新しいCSSに font-family を書かない★★
  • ★文言は t() を通す★。★数値は画面側で差し込む★
  • ★「もっと表示」の判定は lib/ 側の純粋関数に切り出すこと★ (★lib/dashboard/rescue-visibility.ts と同じ形★)

8-5. ★モックに対応する画面がありません★

★実測: d-watch-mock.jsx に「面積」「㎡」「累計」のヒットは0件★です。

CLAUDE.md §5 は「モックが画面レイアウト・文言・遷移の正」と定めています。★前例が3件★:

決定 何を
D-120 モックに無いページ送りUIを新規作成
D-121 モックと違う色に
★D-137★ ★サイドバーのスクロールを独立させた★

★同じ作法を踏襲すること:★

  1. ★d-watch-mock.jsx は1文字も書き換えない★
  2. docs/decisions.md に ★「CLAUDE.md §5 の例外。ユーザーの明示指示(2026-09-15)に基づく」★ と記録し、★指示の原文を引用する★
  3. ★CleanAreaScreen.tsx の冒頭に理由コメントを残す★ — ★★書かないと、次にモックと突き合わせた人が「余計なものがある」と判断して消します★★

9. ★動作確認★

9-1. 前提

★dev server はポート3000で動いています。★ ★.env.local はテストDBを向いています。★ ★★テストDBは読み取り専用。書き込まないこと。★★

★テストDBの実測値★(自分で確かめ直すこと):

clean_status_histories  538行
Σ clean_area            341,635
Σ task_area             494,000

9-2. 確かめること

GET /api/clean-area
GET /api/clean-area?group_by=group
GET /api/clean-area?group_by=customer
GET /api/clean-area?date_from=2026-09-01&date_to=2026-09-13
GET /api/clean-area?group_id=9999
# 期待
★9-1★ ★期間指定なしの合計が Σ341,635 / Σ494,000 と一致すること★
★9-2★ ★★粒度を変えても全体合計が変わらないこと★★
9-3 期間で絞ると合計が減ること
★9-4★ ★group_id=9999 で0件(★全件に化けない★)★
9-5 ★完了率が丸められていないこと★
★9-6★ ★単位が lib/config の AREA_UNIT から出ていること★(★画面やAPIに直書きされていないこと★)
9-7 ★数値カードに「直近2年ぶん」の注記が出ること★

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

9-3. ★ローカルDBで階層を確かめる★

★ローカルDB(2グループ・3顧客・11施設)★で:

# 期待
★9-8★ ★★group_by=group の合計 = group_by=customer の合計 = group_by=facility の合計★★
9-9 ★グループなしの顧客が「グループなし」として出ること★(★「グループ0」でないこと★)

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

9-4. 画面

★★ログインが要るので、目視は難しいかもしれません。★★ ★できなければ「できなかった」と正直に報告すること。★ ★推測で「動いているはず」と書かないこと。★


10. 段階6 — 記録

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

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

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

書くこと:

  • ★サマリの面積を使わず、常に履歴から集計すると決めた理由★(★実データで合計になっていない★)
  • ★D-102 との関係★(★D-102 の対象はKPI 4指標で、面積は対象外と読んだ★)
  • ★★レポート画面のKPIと数字が違いうること★★と、その理由(経路が違う)
  • ★単位を ㎡ にしたのは★★DFAの判断★★であり、★★TeamWorks の確認ではない★★こと(ユーザー原文を転記。第9回 5章の照会は継続)
  • ★groupBy + _sum を選び、番人を置いた理由★(1:N で静かに多重計上する)
  • ★期間の既定を「全期間」にした理由★と、★★実際には直近2年であること★★
  • ★画面名を「清掃面積」にした理由★(「総」「累計」を名前に入れない)
  • ★モックに無いUIを作ったこと★(CLAUDE.md §5 の例外。★ユーザー原文を転記★)

docs/handoff.md に追記:

  • ★★面積の単位は TeamWorks 未確認のまま ㎡ で表示している★★(第9回 5章)。★回答が違えば AREA_UNIT 1行で直す★
  • ★サマリの面積が合計かどうか未確認★(第9回 Q2-1)
  • ★1タスクに履歴が何行できるか未確認★(第9回 Q2-3)— ★番人がログを出したら、ここを見ること★
  • ★clean_status_histories.started_at の索引が未着★(第9回 1-4)
  • ★レポート画面のKPIと数字が違いうること★

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

# 落とし穴 どうなるか
★1★ ★サマリテーブルの面積を使う★ ★全行 1000 固定の嘘の数字が出る★。★例外もテスト失敗も出ない★
★2★ ★1タスクに履歴が2行あると多重計上する★ ★★数字が大きくなるだけ。番人を置くこと★★
★3★ ★facility_ids を ?.length で分岐する★ ★「0件ヒット」が「全件」に化ける★
★4★ ★粒度を変えると全体合計が変わる★ ★ロールアップが壊れている。B-5 でテストすること★
★5★ ★group_id = null の顧客を落とす★ ★全体合計と内訳の和が合わなくなる★
★6★ ★"㎡" を画面やAPIに直書きする★ ★★TeamWorks の確認が未取得。回答が違ったとき、直す場所が散らばる★★
★7★ ★分母0で ∞ や NaN を出す★ ★completionRate() を使えば null になる★
★8★ ★include を使う★ ★親行の数ぶん IN が膨らむ★(D-136 の実測)
★9★ ★3桁区切りにしない★ ★341635 は読めない★
★10★ ★「直近2年」の注記を出さない★ ★3年目に数字が減って問い合わせが来る★
★11★ ★fixtures 側を notImplemented にする★ ★USE_FIXTURES=true で画面が落ちる★
★12★ ★判定や集計を components/ に書く★ ★テストが走らない★
★13★ ★新しいCSSに font-family を書く★ ★漢字が中華字形になる★
★14★ ★テストDBに書き込む★ ★★読み取り専用★★

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

項目 なぜ
★面積のCSVエクスポート★ ★ユーザーが求めていない★。要るなら別途
★週・月の粒度での期間指定★ ★自由な日付範囲にした★(§2-3)。★月跨ぎ週の帰属に乗る必要が無くなる★
★サマリテーブルからの集計★ ★面積が合計になっていない★(第9回 Q2-1 で照会中)
★グループ単位のサマリテーブル★ ★TeamWorks の領域★(第9回 6章)
★単位が ㎡ であることの TeamWorks への確認★ ★第9回 5章で照会中。回答待ち★
★点検記録の絞り込み★ 画面自体が未実装
★スマートフォン幅のレイアウト★ 別課題
★started_at の索引★ ★TeamWorks に依頼済み★(第9回 1-4)