画面項目の算出方法¶
各画面に表示される 派生項目 (計算で求まる値) の算出式・入力ソース・端数/欠損の扱いを、
リポジトリの実コードを典拠 にまとめたリファレンスです。各行の「典拠」に ファイル:行 を
併記しているので、必ず一次情報を確認できます。スキーマ・状態遷移・設計判断の詳細は重複を避け、
概念モデル (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 または odometerIn が null なら null |
apps/web/src/lib/daily-log.ts:4-10 (resolveDistance) |
燃焼燃料 combustionLiters |
(diesel ?? 0) + (gasoline ?? 0) |
軽油 L・ガソリン L | 両方 null なら null。片方のみ null は 0 とみなして 合算。アドブルーは対象外 |
apps/web/src/lib/daily-log.ts:23-29 (combustionLiters) |
燃費 fuelEfficiencyKmPerL |
distance ÷ liters |
走行距離・燃焼燃料 L | distance が null、liters が null、または liters ≤ 0 なら null |
apps/web/src/lib/daily-log.ts:11-17 (fuelEfficiencyKmPerL) |
円/km costPerKm |
fuelCost ÷ distance |
燃料費 (円)・走行距離 | fuelCost が null、distance が null、または 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_km は DB の生成列 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-173(StopRow の描画は :419) |
| 表示番号 (コース編集) | 編集中配列の index + 1 |
編集中の course stops | 保存時に normalizeOrder で 0 始まりへ再採番 |
CourseBuilderPage.tsx:999、dispatch-order.ts:3-5 |
追加時の stop_order nextStopOrder |
max(stop_order) + 1 (空列は 0) |
その列の既存 stops | 列長ではなく max+1。欠番列への length 追加による衝突を回避 | apps/web/src/lib/dispatch-order.ts:14-17、DispatchBoardPage.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.status は open_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 有→completed/accepted_by_discharger 有→matched (ただし jobs.status が in_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 |
回収先の繰り返しサマリ (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 で集計します (weighings → weighing_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.ts の aggregateByPartnerAndItem (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.ts の fetchMonthlyAggregation (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,137(grossLtTare 判定),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.ts の deriveReceiptTotal)。一方 確定後の価格パネル UI はサーバの価格 RPC price_walk_in_weighing が返す total(weighing.totalAmount)を表示する(非並行時は両者一致) |
walkInCompute.ts:216-232、walkInReceiptData.ts:85-95、WalkInWeighingPage.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-78、signup.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-63、signup.ts:242-252 |
コード照合 matchInviteCode |
code_hash = sha256("<id>:<code>") の一致 かつ tenant_type 整合 |
招待行・入力コード | code_hash 欠損/不一致は誤り。種別固定があり body と不一致なら不可 |
signup.ts:160-176、signup-code.ts:49-53、00000000000049_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
- 状態遷移図 — 案件・アサインメント・配車ストップ・招待コードの状態
- シーケンス図 — 認証・オンボーディング・配車・運行ログ・横乗りのフロー