コンテンツにスキップ

画面項目の算出方法

各画面に表示される 派生項目 (計算で求まる値) の算出式・入力ソース・端数/欠損の扱いを、 リポジトリの実コードを典拠 にまとめたリファレンスです。各行の「典拠」に ファイル:行 を 併記しているので、必ず一次情報を確認できます。スキーマ・状態遷移・設計判断の詳細は重複を避け、 概念モデル (ER)状態遷移図・ 各 ADR へ相互リンクします。

null / 0 の表記規約

純粋ヘルパーは 未記録・計算不能を null で表し、UI 側で null を「—」「未確定」等に落とします (0 円・0 km と混ぜない)。0 は「確定したゼロ」 です。集計の合計は欠損 (null) を ?? 0 で 0 とみなして加算する箇所と、null を維持して伝播する箇所があり、本書では列ごとに明示します。 丸めは原則 表示時のみ (toLocaleString('ja-JP') 等)、保存値は丸めません (重量集計の 2 桁丸めのみ例外)。


日次運行ログ / コスト指標 (1 台 × 1 日 → 月次集計)

走行距離・燃費・円/km は apps/web/src/lib/daily-log.ts の純粋ヘルパーで算出します (ユニットテスト対象)。燃焼燃料は 軽油 + ガソリン で、アドブルー (尿素水) は燃料ではないため 分母に 含めません。詳細は ADR 0012 日次運行ログと燃料の記録

項目 算出式 (コードから) 入力ソース 端数・欠損 (null/0) 処理 典拠 (file:line)
走行距離 distance odometerIn − odometerOut 入庫/出庫メーター (整数) odometerOut または odometerInnull なら null apps/web/src/lib/daily-log.ts:4-10 (resolveDistance)
燃焼燃料 combustionLiters (diesel ?? 0) + (gasoline ?? 0) 軽油 L・ガソリン L 両方 null なら null。片方のみ null0 とみなして 合算。アドブルーは対象外 apps/web/src/lib/daily-log.ts:23-29 (combustionLiters)
燃費 fuelEfficiencyKmPerL distance ÷ liters 走行距離・燃焼燃料 L distancenulllitersnull、または liters ≤ 0 なら null apps/web/src/lib/daily-log.ts:11-17 (fuelEfficiencyKmPerL)
円/km costPerKm fuelCost ÷ distance 燃料費 (円)・走行距離 fuelCostnulldistancenull、または distance ≤ 0 なら null apps/web/src/lib/daily-log.ts:30-33 (costPerKm)
出庫メーター既定値 当日既存ログ有→その出庫値 / 新規→前回ログの入庫メーター 当日ログ・前回ログ 値が無ければ '' (既定なし) apps/web/src/lib/daily-log.ts:51-59 (resolveOdometerOutDefault)

DB 側の生成列 distance_km

vehicle_daily_logs.distance_kmDB の生成列 generated always as (odometer_in - odometer_out) stored です。クライアントの resolveDistance と同じ式で、明細行の log.distanceKm はこの生成列に由来します。 典拠: supabase/migrations/00000000000029_vehicle_daily_logs.sql:13


コスト集計ダッシュボード (/collector/fleet/costs)

「総コスト」「燃料費」「走行距離」「給油量」「燃費」「円/km」のカードは、費目以外コスト燃料費 (コスト入力)運行ログ月次 の 3 系統を別クエリで取得して合成します。 燃料費は給油量から算出できず、請求書 (コスト入力 category='fuel') で後から確定するため、 fuel 入力が 0 件なら 「未確定」 です。典拠ファイルは apps/web/src/pages/collector/fleet/VehicleCostDashboardPage.tsx

項目 算出式 (コードから) 入力ソース 端数・欠損 (null/0) 処理 典拠 (file:line)
合計金額 (燃料費を除く) grandTotal Σ nonFuelMonthly.totalAmount vehicle_cost_entries 月次集計のうち category ≠ 'fuel' 行が無ければ 0 円 VehicleCostDashboardPage.tsx:321-330
燃料費 fuelSummary Σ fuelRows.totalAmount (0 件なら 未確定) category = 'fuel' 固定の別クエリ (費目フィルタ非依存) fuel 行 0 件 → undetermined:true, amount:0。1 件以上なら 0 円入力も「確定済みの 0 円」 VehicleCostDashboardPage.tsx:135-141,243-259,338
総コスト (表示) grandTotal + fuelSummary.amount 上 2 行 燃料費が未確定のとき amount=0 のため 確定分のみ (0 を混ぜない)。見出しも「燃料費を除く確定分/含む」を切替 VehicleCostDashboardPage.tsx:599-623
データ有無 hasAnyCostData nonFuelCount > 0 または dailyCount > 0 または fuelCount > 0 3 系統の件数 いずれか 1 件でも「データ有り」。燃料費のみのテナントも有データ扱い VehicleCostDashboardPage.tsx:168-174,401-405
走行距離 (合計) Σ dailyMonthly.totalDistanceKm 月次運行サマリビュー 各行 ?? 0 で加算 VehicleCostDashboardPage.tsx:271-274,339-342
給油量 (軽油/アドブルー/ガソリン) 種別ごとに Σ total*Liters 月次運行サマリビュー 各行 ?? 0 で加算 VehicleCostDashboardPage.tsx:343-354
燃費 (カード) fuelEfficiencyKmPerL(totalDistanceKm, combustionLiters(diesel, gasoline)) 上記合計 計算不能なら「—」。分母は軽油+ガソリンのみ VehicleCostDashboardPage.tsx:719-727
円/km (カード) costPerKm(fuelSummary.amount, totalDistanceKm) 燃料費・走行距離 燃料費が未確定なら同じく 未確定。それ以外で計算不能なら「—」 VehicleCostDashboardPage.tsx:739-748
月次運行サマリ表「燃料費」列 その車両×月の fuel 入力合計 (fuelCellForVehicleMonth) category='fuel' 月次 キー (車両×月) が無ければ 未確定?? 0 で 0 に潰さない VehicleCostDashboardPage.tsx:153-161,1191

燃料費は運行ログの fuel_cost を使わない

ダッシュボードの燃料費は コスト入力 (category='fuel') 由来です。日次運行ログの旧 fuel_cost 列は燃料費として 使いません (二重計上の防止)。典拠: VehicleCostDashboardPage.tsx:243-245


月次集計ビュー vehicle_daily_logs_monthly

走行距離・給油量の月次サマリは DB ビューで集約します。security_invoker = true のため 閲覧者の RLS (工場スコープ・テナント分離) を尊重 します。グルーピングは (tenant_id, factory_id, vehicle_id, 月) です。

SQL 集約 備考 典拠 (file:line)
month date_trunc('month', log_date)::date 各月の 1 日に正規化 00000000000029_vehicle_daily_logs.sql:164
total_distance_km sum(distance_km) 走行距離合計 (画面の「走行距離」) 00000000000029_vehicle_daily_logs.sql:166
total_diesel_liters sum(diesel_liters) 軽油 L 合計 00000000000047_daily_log_fuel_types.sql:40
total_adblue_liters sum(adblue_liters) アドブルー L 合計 00000000000047_daily_log_fuel_types.sql:41
total_gasoline_liters sum(gasoline_liters) ガソリン L 合計 00000000000047_daily_log_fuel_types.sql:42
total_fuel_cost sum(fuel_cost) LEGACY。後方互換で残す旧列。画面の「燃料費」ではない 00000000000047_daily_log_fuel_types.sql:36
total_fuel_liters sum(fuel_liters) LEGACY。種別別 L 列へ移行済みの旧列 00000000000047_daily_log_fuel_types.sql:38
log_count count(*)::int 対象ログ件数 00000000000047_daily_log_fuel_types.sql:39

total_fuel_cost / total_fuel_liters は LEGACY

画面の 燃料費はコスト入力 (vehicle_cost_entries.category='fuel') から集計します。ビューの total_fuel_cost / total_fuel_liters は migration 47 で種別別 L 列へ移行した後も 後方互換のため 残している旧列 で、ダッシュボードの燃料費・燃費には使いません。 GROUP BY (tenant_id, factory_id, vehicle_id, date_trunc('month', log_date)::date)security_invoker = true。典拠: 00000000000047_daily_log_fuel_types.sql:6-9,29-44


配車ボードの表示順 (stop_order → 表示番号)

ドライバー列に並ぶストップの 表示番号 は、ストレージの stop_order そのものではなく 並べ替え後のリスト内の位置 (0 始まり index) + 1 です。stop_order はドライバー毎に 0 始まりで 採番されますが、削除・移動で 欠番や重複が生じうる (driver_id × stop_order に unique 制約は無い) ため、表示は compareStopOrder で安定ソートしてから連番を振り直します。 純粋ロジックは apps/web/src/lib/dispatch-order.ts

項目 算出式 (コードから) 入力ソース 端数・欠損 (null/0) 処理 典拠 (file:line)
並べ替え compareStopOrder stop_order 昇順、同値は id 昇順でタイブレーク 取得した stops 欠番・重複が混じってもリフェッチ毎の揺れを消す apps/web/src/lib/dispatch-order.ts:25-30
表示番号 (配車ボード) ソート後リストの index + 1 compareStopOrder 後の配列 — (ストレージ stop_order とは独立) DispatchBoardPage.tsx:237,1058
表示番号 (ドライバー用ホーム) 取得順リストの index + 1 当日の自分のストップ — (2026-07-03 に「受託済みの案件一覧」を撤去し、当日の自ルートのみ) CollectorDriverHome.tsx:169-173StopRow の描画は :419
表示番号 (コース編集) 編集中配列の index + 1 編集中の course stops 保存時に normalizeOrder で 0 始まりへ再採番 CourseBuilderPage.tsx:999dispatch-order.ts:3-5
追加時の stop_order nextStopOrder max(stop_order) + 1 (空列は 0) その列の既存 stops 列長ではなく max+1。欠番列への length 追加による衝突を回避 apps/web/src/lib/dispatch-order.ts:14-17DispatchBoardPage.tsx:344

関連: ADR 0008 配車コースモデル状態遷移図 (配車ストップ)


未割当プール (computePoolSites)

配車ボード左の「未割当プール」は、その日に表示すべき回収先を apps/web/src/lib/dispatch-pool.ts で算出します。コース生成 RPC はドライバー列の事前生成のみを担い、プールの源泉ではありません (プールの源泉は回収先個別の繰り返し設定)。

項目 算出式 (コードから) 入力ソース 端数・欠損 (null/0) 処理 典拠 (file:line)
プール対象 computePoolSites active かつ (isSiteDueOnDate(schedules, date) または extraSiteIds.has(id)) かつ assignedSiteIds.has(id) でない 回収先 + 繰り返し設定・当日 stops extraSiteIds 未指定は false 扱い。assignedSiteIds が優先 (再割当された回収先は extra に残っても出ない) apps/web/src/lib/dispatch-pool.ts:30-42
assignedSiteIds 当日 stops の collectionSiteId 集合 dispatch_stops DispatchBoardPage.tsx:243-249
extraSiteIds (= manuallyUnassignedSiteIds) ドライバー列から手動で未割当へ外した回収先 (セッション内のみ) UI 状態 繰り返し非該当のコース専用回収先でも、外した直後にプールへ合流表示する DispatchBoardPage.tsx:258

関連: ADR 0008 配車コースモデル (コース生成とプールの責務分離)。


横乗り (同乗者) のグルーピングと候補

横乗りは「回収先にドライバーと同行する業務」で、紐付け単位は ドライバーの当日ルート (driver_id × dispatch_date) です。配車ルート (dispatch_stops / stop_order)・生成 RPC・D&D には 一切影響しません。純粋ロジックは apps/web/src/lib/dispatch-ride-along.ts

項目 算出式 (コードから) 入力ソース 端数・欠損 (null/0) 処理 典拠 (file:line)
ルート別グルーピング groupRideAlongsByDriver driverId 単位で束ね、{ id, profileId } へ射影 その日の全横乗り 入力順 (登録順) を保持 apps/web/src/lib/dispatch-ride-along.ts:33-41
追加候補 computeRideAlongCandidates 全メンバー − そのルートのドライバー本人 − 既に横乗り登録済みの profileId テナント全メンバー・ドライバー・既存横乗り 候補が空配列なら追加導線を出さない apps/web/src/lib/dispatch-ride-along.ts:48-56

関連: ADR 0016 横乗り (当日ルート単位の同乗者)シーケンス図 (g) 横乗りの追加/削除


案件の実効ステータス / 依頼可能判定

ダッシュボードの KPI・バッジに出る案件ステータスは、jobs.status の書込ではなく job_assignments 群から派生 します。受託承認は assignment の status を変えるだけで jobs.statusopen_for_quotes のまま固定されるためです。純粋ロジックは apps/web/src/lib/job-lifecycle.ts

項目 算出式 (コードから) 入力ソース 端数・欠損 (null/0) 処理 典拠 (file:line)
active な assignment isActiveAssignment declined_by_discharger / declined_by_collector / expired / cancelled 以外 assignment の status これら 4 状態は占有を解く (非 active) apps/web/src/lib/job-lifecycle.ts:19-28
実効ステータス resolveJobEffectiveStatus completed 有→completedaccepted_by_discharger 有→matched (ただし jobs.statusin_progress/completed ならそれを優先)/それ以外は jobs.status job + assignments active な assignment のみ評価 apps/web/src/lib/job-lifecycle.ts:52-66
依頼可能 isJobRequestable jobs.status = 'open_for_quotes' かつ active な assignment が 1 件も無い job + assignments 受託済み・依頼中の過剰割当を防ぐ apps/web/src/lib/job-lifecycle.ts:74-77

関連: 状態遷移図 (案件 / アサインメント)


回収先一覧 (取引先詳細) のフィルタ / ソート

純粋ロジックは apps/web/src/lib/collection-site-filter.ts。曜日・週序数・住所の 複数条件は AND。 週序数の語彙 WEEK_ORDINALS = ['every','1','2','3','4','5','last']recurrence.ts 由来です。

項目 算出式 (コードから) 入力ソース 端数・欠損 (null/0) 処理 典拠 (file:line)
フィルタ filterSites 曜日・週序数の 両方指定時は「同一 schedule が両方に一致」、片方のみはその軸だけ。住所は trim 後の部分一致 回収先 + 繰り返し設定 曜日 0(日) は有効値 (?? null で null/undefined のみ「すべて」)。週序数 '' は「すべて」。住所 null は空文字扱い apps/web/src/lib/collection-site-filter.ts:40-64
並び替えキー (recurrence) 各 schedule を ordinalRank × 7 + weekday に符号化し 最小値 繰り返し設定 schedules 空は null (= 末尾)。ordinalRank の未知値は末尾扱い apps/web/src/lib/collection-site-filter.ts:66-86
ソート sortSites name/address は localeCompare('ja')、recurrence はキー差 回収先 値が空の行 (address 空・繰り返し無し) は 方向に依らず常に末尾。同値は元の入力順 (安定) apps/web/src/lib/collection-site-filter.ts:88-127

配車ボードの工場フィルタ

ドライバー列を所属工場で絞り込む純粋ロジックは apps/web/src/lib/dispatch-filter.ts

項目 算出式 (コードから) 入力ソース 端数・欠損 (null/0) 処理 典拠 (file:line)
工場フィルタ filterDriversByFactory factoryId === '' なら全件。指定時は factoryIds.includes(factoryId) を満たすドライバーのみ ドライバー (所属工場 factoryIds) 複数工場所属でも選択工場を含めば残す。所属工場未設定 (factoryIds 空) は選択時に 除外 apps/web/src/lib/dispatch-filter.ts:16-22

関連: ADR 0006 工場スコープのアクセス制御


回収先の繰り返しサマリ (formatSiteRecurrence)

回収先カードに出す繰り返しのサマリチップは apps/web/src/lib/recurrence.ts で整形します。

項目 算出式 (コードから) 入力ソース 端数・欠損 (null/0) 処理 典拠 (file:line)
サマリチップ formatSiteRecurrence 「毎週」(every) は曜日をまとめて 1 チップ、第n/最終は曜日ごとに 1 チップ。曜日 0→6・週序数 every→1..5→last の安定順 繰り返し設定 schedules 空は [] (UI は RECURRENCE_UNSET_LABEL = 「繰り返し未設定」を表示)。同一 (weekday, weekOrdinal) は畳む apps/web/src/lib/recurrence.ts:33-74,17
当日該当 isSiteDueOnDate weekday 一致 かつ (every または 第n 一致 または last かつ月内最終) 繰り返し設定・対象日 当日プール算出にも使用 apps/web/src/lib/recurrence.ts:90-99

取引先 × 品目の月次重量集計 (/collector/monthly)

純粋ロジックは apps/web/src/lib/db/resource-aggregation.ts。SQL ビューではなく クライアント側の reduce で集計します (weighingsweighing_items を取得して合算)。

項目 算出式 (コードから) 入力ソース 端数・欠損 (null/0) 処理 典拠 (file:line)
重量合計 totalWeightKg (partner_id, item_type_id) ごとに Σ weighing_items.weight_kg weighing_items (+ 親 weighing の partner_id) 小数 2 桁丸め Math.round(x * 100) / 100 apps/web/src/lib/db/resource-aggregation.tsaggregateByPartnerAndItem (L51-65)
対象期間 [from, 翌月1日)from = "YYYY-MM-01"to = 翌月の 1 日 選択中の年月 .gte('weighed_on', from).lt('weighed_on', to) (月末日を -31 等にしない) apps/web/src/lib/db/resource-aggregation.tsfetchMonthlyAggregation (L117-137・.gte/.lt は L136-137)。React Query フック useMonthlyAggregation (L205) が取得を委譲
セル表示 (取引先 × 品目) weightMap[partnerId::itemTypeId] 集計結果 値が無いセルは「—」(0 と区別) CollectorMonthlyPage.tsx:53,95,242-249

顧客持ち込み(walk-in)計量の重量・金額・証明書番号

持ち込み計量(/collector/walk-in-weighing)の各品目行の重量・金額と伝票合計・差引合計は、純関数 apps/web/src/lib/weighing/walkInCompute.ts で算出します(副作用なし・ユニットテスト対象)。方式(逐次差分 / 単発 / 個別)で行正味の求め方が変わります。金額の符号規約は「正=お客様がお支払い(処分>買取)/ 負=お客様へお支払い(買取>処分)」で、total = Σ(処分 amount) − Σ(買取 amount)。証明書番号は確定 RPC finalize_walk_in_weighing(mig111)が採番します。

項目 算出式 (コードから) 入力ソース 端数・欠損 (null/0) 処理 典拠 (file:line)
行正味 lineNetKg(逐次差分) max(0, round2(gross − tare)) 品目行の総量・風袋 gross/tare いずれか null は 0。数値は常に 0 以上へクランプ(Math.max(0, …)・:118)だが、gross < tare のときは黙って 0 にせず別途 invalid='gross_lt_tare' フラグを立てて表面化(UI が確定を非活性化・行を強調) walkInCompute.ts:118(clamp),131-141,137grossLtTare 判定),121-125(flag)
行正味 lineNetKg(個別) max(0, round2(measured)) 品目行の計量値 measured null は 0。gross/tare 概念なし walkInCompute.ts:143-146
行正味 lineNetKg(単発・按分) 伝票 net = max(0, round2(gross − tare)) を各品目の measured 比で 最大剰余法(銭=1/100kg 単位)配分し Σ==net を厳密成立 伝票の総量・風袋・各品目の計量値 net=0 または Σmeasured=0 は全 0(真の 0 = valid) walkInCompute.ts:166-200
課金重量 weightKg(=集計/金額の対象) max(0, round2(lineNetKg − kagoBiki − dustMizuBiki)) 行正味・カゴ引・ダスト水引 数値は常に 0 以上へクランプ(weightKg = Math.max(0, billable)・:120)。控除超過(billable < 0)のときは黙って 0 にするだけでなく別途 invalid='billable_negative'(全方式・確定不可)を立てて表面化。真の 0(控除==lineNet)は valid で flag なし walkInCompute.ts:119-120(clamp),121-125(flag)
金額 amount(行) round(weightKg × unitPrice)Postgres round(numeric) と厳密一致(float 誤差・半数の向きを排除・roundYenLikePg 課金重量・単価 単価 null/非有限は amount=null(未価格)。amount 自体は非負・向きは direction が持つ walkInCompute.ts:62-79,216-232
差引合計 total Σ(charge amount) − Σ(buyback amount) 各行の符号付き amount 単価が 1 つも無ければ null(全行未価格)。証明書 PDF(発行・再発行) は印字明細から導出し印字合計=明細総和を保証(walkInReceiptData.tsderiveReceiptTotal)。一方 確定後の価格パネル UI はサーバの価格 RPC price_walk_in_weighing が返す totalweighing.totalAmount)を表示する(非並行時は両者一致) walkInCompute.ts:216-232walkInReceiptData.ts:85-95WalkInWeighingPage.tsx:1073
伝票合計重量 slipTotalKg round2(Σ lineNetKg)(控除前・証明書表示用) 各行の行正味 walkInCompute.ts:234-237
証明書番号 receiptNo to_char(current_date,'YYYYMMDD') || '-' || lpad(seq,3,'0')(= YYYYMMDD-NNN・テナント別日次連番) 確定時の採番カウンタ next_walk_in_receipt_seq 日付部は採番日 current_date 基準(受付日 weighed_on ではない)。採番は確定時のみ・再発行で不変・取消番号は再利用しない supabase/migrations/00000000000111_walk_in_weighing.sql:724-725,469-501

数値は 0 にクランプしつつ invalid フラグで表面化する理由

確定 RPC(mig111 finalize_walk_in_weighing)は不正入力(逐次差分の gross < tare・全方式の控除超過)を raise してロールバックします。純関数が返す数値lineNetKg / weightKg)は表示・集計を壊さないよう 常に 0 以上へクランプします(Math.max(0, …)・:118,120)が、それだけでは「preview は valid に見える のに finalize は失敗」という乖離が起きます。そこで数値クランプとは別に invalid フラグ (gross_lt_tare / billable_negative・:121-125)を立て、UI が確定ボタンを非活性化して行を強調できる ようにしています(=黙って 0 に丸めて隠さない)。典拠: apps/web/src/lib/weighing/walkInCompute.ts:11-22(設計意図),118-125(clamp + flag)。


login_id → email 変換 と 招待コード検証

ログインは login_id 方式で、GoTrue の identity は常に合成メールです。公開サインアップの招待検証は 多層 で、apps/api/src/routes/signup.ts と共有ユーティリティ (packages/shared) が担います。 詳細は ADR 0004 login_id 認証 / ADR 0011 公開オンボーディング / シーケンス図 (b)

項目 算出式 (コードから) 入力ソース 端数・欠損 (null/0) 処理 典拠 (file:line)
login_id → email loginIdToEmail @ を含めば実メールとしてそのまま、含まなければ <login_id 小文字>@members.waste-link.local login_id trim + 小文字化。ドメインは LOGIN_EMAIL_DOMAIN packages/shared/src/utils/login-id.ts:14,24-28
招待行の事前可否 emailInviteUsable 行が存在 かつ used_at が null かつ 期限内 かつ attempts < 5 signup_invite_codes (email で最新の未使用行) attempts 未設定は ?? 0。期限 null は無期限 packages/shared/src/utils/signup-code.ts:68-78signup.ts:213-234
試行ゲート register_signup_invite_attempt used_at is null かつ attempts < p_max のときだけ attempts + 1 し true を返す (条件付き UPDATE・行ロックで直列化) 招待行 id・上限 5 TOCTOU 対策。上限超過/使用済み/不存在は false 00000000000050_signup_invite_codes_atomic_attempt_gate.sql:39-63signup.ts:242-252
コード照合 matchInviteCode code_hash = sha256("<id>:<code>") の一致 かつ tenant_type 整合 招待行・入力コード code_hash 欠損/不一致は誤り。種別固定があり body と不一致なら不可 signup.ts:160-176signup-code.ts:49-5300000000000049_signup_invite_codes_email_otp.sql:14

失敗試行上限 = 5

SIGNUP_MAX_ATTEMPTS = 5。6 桁コードは低エントロピーのため、ハッシュ単体ではなく 「メール所有 + 試行上限 5 + 短い有効期限 + 単回使用」の多層で守ります。 典拠: packages/shared/src/utils/signup-code.ts:24


排出ポータル: 定期の次回日 / 日付切替 / 集約完了 (v1.5.0)

画面項目 算出 典拠
案件一覧「希望日」(定期) の次回日 nextRecurringDate(schedules, today, 開始日) — 開始日と今日の遅い方から前進探索で最初のスケジュール一致日。既定窓 366 日 (第5◯曜は最大 ~119 日空くため)。見つからなければ開始日へフォールバック apps/web/src/lib/recurrence.ts / apps/web/src/lib/discharger-recurring.ts (deriveJobDisplayDates)
案件一覧「希望日」のフィルタ/ソート値 表示値と同じ displayDate (定期=次回日・非定期=希望日)。表示と絞り込みの不一致を避ける apps/web/src/pages/discharger/DischargerJobListPage.tsx
概要「回収状況」の選択日リスト selectDaySites(sites, 選択日) — スケジュール一致 (予定) ∪ 当日 dispatch_stop あり (実績)。名前昇順。同一日に複数 stop がある回収先は completed 優先で 1 件に畳む (順序非依存・決定的) apps/web/src/lib/discharger-collection-status.ts (summarizeSiteStatusForDate / pickStopForDate)
概要 KPI「本日の対象 / 完了 n 件」 分母 = countDueToday (スケジュール一致)。キャプションの完了 = countDueCompletedToday (対象内の完了のみ。臨時完了を混ぜると分母超えになるため)。独立カード「本日 完了」= countCompletedToday (臨時含む) 同上
案件詳細「本日 x/y 完了」(定期) y = 本日対象の紐付け回収先数、x = そのうち完了 (dueCompletedTodayCount)。臨時完了は x に入れない apps/web/src/lib/job-site-completion.ts (deriveJobSiteCompletion)
案件詳細「本日分 完了」バッジ (定期) cycleCompleteToday = 本日対象 > 0 かつ対象内全完了。jobs.status は書き換えない (表示層のみ) 同上
案件詳細「全回収先 完了」バッジ (spot) allSitesCompleted = 紐付け回収先が 1 件以上かつ全てに完了実績 (RPC の直近 60 日窓内) 同上

関連ドキュメント

  • ADR 索引 — 確定済みの設計判断 (MADR 形式)
  • 概念モデル (ER) — テーブル / カラム / FK
  • 状態遷移図 — 案件・アサインメント・配車ストップ・招待コードの状態
  • シーケンス図 — 認証・オンボーディング・配車・運行ログ・横乗りのフロー