Newer
Older
client_dwatch / lib / fixtures / clean-logs.ts
import type { CleanLogItem } from "@/lib/types";
import { GRADE_THRESHOLDS, PRECHECK_MINUTES_BEFORE } from "@/lib/config";
import { judgeEscalationLevel, judgeGrade } from "@/lib/judge";
import {
  scheduledCleanLogRowKey,
  unscheduledCleanLogItem,
} from "@/lib/db/clean-log-mapping";
import { at } from "./_helpers";
import { cleanTaskReportStatus } from "./task-status-rule";

/**
 * ★fixtures の清掃ログは必ずロボット(機種・シリアル)を持つ★(指示書BB U2・D-182)—
 *   API の `CleanLogItem` は「清掃エリアに有効なロボットがいない」ときの `null` を許すが、fixtures には0台の清掃エリアが無い。
 *   ★型で固定しておく★(`scripts/seed-local-db.ts`・`./areas`・`./clean-task-reports` がシリアルを鍵に使うため。データは変えない)
 */
export type FixtureCleanLogItem = CleanLogItem & {
  // ★予定のある行だけ★(予定外タスクの行は `unscheduledCleanLogs` に別に持つ。指示書BG §2-1)—
  //   `./areas`・`./clean-task-reports`・シードが予定の id を鍵に使うので、型で `number` に固定する
  id: number;
  unscheduled: false;
  model_name: string;
  serial_number: string;
};

/**
 * 清掃詳細ログのサンプルデータ
 *
 * docs/fixtures.md §2 のケースを網羅しています。
 *   §2.1 判定の境界値10ケース ★最重要★
 *   §2.2 エスカレーション(L1〜L3、スケジュール非存在日を挟む連続)
 *   §2.3 ステータスの組み合わせ
 *   §2.4 データ量・表示崩れ(31日分 / 11件以上の△× / 該当0件 / 長い施設名 / 1施設2台)
 */

/**
 * grade / escalation_level は `lib/judge/` の本番ロジックで埋めています。
 *
 * サンプルデータであっても判定は本番と同じ関数を通します。
 * fixture専用の判定を書くと、画面で見えている判定とテストが通している判定が
 * 食い違う可能性が出てくるためです(D-044)。
 */

// ---------------------------------------------------------------------------
// ★`task_status`(`clean_task_reports.status` の生値)★(指示書AQ §1-2 / §5-2)
//
// ★★規則は `./task-status-rule.ts` の `cleanTaskReportStatus()` が唯一の定義★★ —
//   `lib/fixtures/clean-task-reports.ts` も同じ関数で `status` を作るので、
//   `USE_FIXTURES` の true/false で同じ清掃に同じ `status` が出る(担当F の設計意図)。
//
// ★★`./clean-task-reports` から import しないこと(循環 import)★★ —
//   あちらは ★モジュール評価時に★ このファイルの `cleanLogs` / `history` を読む。
//   このファイルから import すると、このファイルが先に読まれた場合
//   (`@/lib/fixtures/clean-logs` を直接 import するテストが複数ある)に `history` が undefined のまま
//   あちらの本体が走り、`TypeError: Cannot read properties of undefined (reading 'filter')` で落ちる
//   (2026-09-30 担当J2 が実測)。★依存の無い `./task-status-rule` から import する★。
//   ★2つの一致は `lib/fixtures/__tests__/areas.test.ts` で引き続き固定している★。
//
// ★値の意味は第10回 N-2 で受領済み(表は `lib/config` の `CLEAN_TASK_STATUS_LABELS`)★だが、
//   ここには「何番が何か」を書かない(表を2箇所に持たない)。
//   6 は 3 と同じ「清掃キャンセル」(ユーザー指示 2026-10-30。第11回 P-4 の時点では先方も意味が分からなかった。D-158)。
// ---------------------------------------------------------------------------

let seq = 1000;
function nextId(): number {
  seq += 1;
  return seq;
}

interface RowInput {
  facility_id: number;
  facility_name: string;
  serial_number: string;
  model_name?: string;
  daysBack: number;
  time: string;
  /**
   * 省略時は既定値「待機中」(fixtures は事前チェックがある前提の行)。
   * ★`null` = 事前チェックの予定が無い★(ユーザー指示 2026-10-07。D-171)。明示した行だけ null にする
   */
  status_1h?: string | null;
  status_30m?: string | null;
  /**
   * ★事前チェック1回目・2回目が何分前か★(`CleanLogItem.status_1h_minutes` / `status_30m_minutes`。D-178)。
   * ★省略時は、その回の status が null でなければ `PRECHECK_MINUTES_BEFORE[0]` / `[1]`(60 / 30)、null なら null★。
   * ★status が null の回に数値を渡すと例外★(不変条件「status が null のときだけ minutes も null」を fixtures でも守る)
   */
  status_1h_minutes?: number | null;
  status_30m_minutes?: number | null;
  departed: boolean;
  completion_rate: number | null;
  consecutive_problem_days?: number;
  /**
   * ★結果待ち★(`CleanLogItem.result_pending`。ユーザー判断 2026-10-07。D-173)。★省略は `false`★。
   * ★fixtures は時刻で決めず、行に直書きする★(`isResultPending()` は `now` に依存するので、
   * fixtures の行が実行時刻で変わらないように)。★`true` にしてよいのは `departed: false` の行だけ★
   */
  result_pending?: boolean;
}

/**
 * 事前チェック1回分の分数を決める(D-178)。★status が null なら null★、
 * 指定が無ければ設定の分数(`PRECHECK_MINUTES_BEFORE` の同じ添字)。
 */
function precheckMinutes(
  status: string | null,
  given: number | null | undefined,
  fallback: number,
  label: string
): number | null {
  if (status === null) {
    if (given !== undefined && given !== null) {
      throw new Error(`[fixtures] ${label} の status が null なのに分数 ${given} が指定されています(D-178 の不変条件)`);
    }
    return null;
  }
  // ★`??` にしないこと★ — 明示の null(status があるのに分数が無い)を既定値で隠さず、下で例外にする
  if (given === undefined) return fallback;
  if (given === null) {
    throw new Error(`[fixtures] ${label} の status があるのに分数が null です(D-178 の不変条件)`);
  }
  return given;
}

function row(input: RowInput): FixtureCleanLogItem {
  const consecutive = input.consecutive_problem_days ?? 0;
  const id = nextId();
  const status1h = input.status_1h === undefined ? "待機中" : input.status_1h;
  const status30m = input.status_30m === undefined ? "待機中" : input.status_30m;
  return {
    id,
    row_key: scheduledCleanLogRowKey(id),
    report_id: null,
    unscheduled: false,
    facility_id: input.facility_id,
    facility_name: input.facility_name,
    model_name: input.model_name ?? "PUDU CC1",
    serial_number: input.serial_number,
    scheduled_at: at(input.daysBack, input.time),
    // ★日本語6値は fixtures 由来の暫定値であり、実データから来る値ではない★(D-077 項3)。
    //   `robots.status` の46値は動作フェーズのみでハンドルアップ・オフラインを表現していない。
    //   ★省略(undefined)だけを既定値にする★ — ★`??` にしないこと★(`null`(事前チェックの予定が無い。D-171)まで
    //   「待機中」に化ける。tsc も lint も捕まえない)
    status_1h: status1h,
    status_1h_minutes: precheckMinutes(status1h, input.status_1h_minutes, PRECHECK_MINUTES_BEFORE[0], "status_1h"),
    status_30m: status30m,
    status_30m_minutes: precheckMinutes(status30m, input.status_30m_minutes, PRECHECK_MINUTES_BEFORE[1], "status_30m"),
    departed: input.departed,
    // ★`clean_task_reports.status` の生値(指示書AQ §1-2 / §5-2。旧 `clean_error` から改名)★ —
    //   ★実施済み(`departed`)なら `cleanTaskReports` の同じ清掃 id の行と同じ `status`、
    //   未実施ならレポートの行が無いので `null`★(指示書AQ §1-1。`clean-task-reports.ts` も
    //   未実施には行を作らない)。★旧コード表(`CLEAN_ERROR`)には対応づけない★(D-077)。
    //   ★★例外★★: `clean_timetable_id` を NULL にした `history` の数行(`cleanTaskReportNullTimetableLogIds`)は、
    //   ★DB 経由では「出発失敗・`task_status` null」になるが、ここでは実施済みのまま★
    //   (`departed` / `completion_rate` がこのファイルの入力で、あちらの Set はそれから作られるため、
    //   こちらを合わせると Set が空になり落とし穴1 の再現ケースが消える)。★意図した非対称★
    //   (`clean-task-reports.ts` の `cleanTaskReportNullTimetableLogIds` の docstring / `docs/fixtures.md` §2.6)。
    task_status: input.departed ? cleanTaskReportStatus(id) : null,
    result_pending: input.result_pending ?? false,
    completion_rate: input.completion_rate,
    grade: judgeGrade(input.completion_rate),
    consecutive_problem_days: consecutive,
    escalation_level: judgeEscalationLevel(consecutive),
  };
}

// ---------------------------------------------------------------------------
// §2.1 判定の境界値 ★最重要★
//
// この10ケースは判定ロジックのテスト(M1)と同じ値を使うこと。
//
// ★境界はすべて「上のランクに入る」★(D-139。2026-09-16)。
//   ★× の境界(10%未満)だけは変わらない★
//
//   完了率  期待判定  表示(整数丸め)
//   90.1     ◎        90%     ← 丸めると90だが◎(丸めてから判定すると消える)
//   90.0     ◎        90%     ← 「90%以上」なので90.0は◎(★D-139 で ○ から変わった★)
//   89.5     ○        90%     ← 表示は90%だが判定は○(★丸めた値で判定しない★)
//   70.1     ○        70%
//   70.0     ○        70%     ← 「70%以上」なので70.0は○(★D-139 で △ から変わった★)
//   10.1     △        10%
//   10.0     △        10%     ← 「10%未満」が×なので10.0は△(CLAUDE.md 4.1 が正)
//    9.9     ×        10%
//    0       ×         0%
//   null     ×         -      ← 未実施
// ---------------------------------------------------------------------------
export const boundaryCases: FixtureCleanLogItem[] = [
  row({ facility_id: 1, facility_name: "メガバリューマート中央本店", serial_number: "8111R5628060011", daysBack: 0, time: "02:00", departed: true, completion_rate: 90.1 }),
  row({ facility_id: 2, facility_name: "バリューマート北町店", serial_number: "8111R5628060022", daysBack: 0, time: "02:00", departed: true, completion_rate: 90.0 }),
  row({ facility_id: 3, facility_name: "メガバリューマート東口店", serial_number: "8111R5628060033", daysBack: 0, time: "01:30", departed: true, completion_rate: 89.5 }),
  row({ facility_id: 4, facility_name: "バリューマート西通り店", serial_number: "8111R5628060044", daysBack: 0, time: "03:00", departed: true, completion_rate: 70.1 }),
  // ★70.0 は D-139 で ○(問題なし)になった★ので連続日数は 0。
  //   `escalation_level` は `judgeEscalationLevel(0)` = null に自動で追従する。
  row({ facility_id: 5, facility_name: "メガバリューマート港南総本店", serial_number: "8111R5628060055", daysBack: 0, time: "02:00", departed: true, completion_rate: 70.0, consecutive_problem_days: 0 }),
  row({ facility_id: 6, facility_name: "バリューマート南丘駅前店", serial_number: "8111R5628060077", daysBack: 0, time: "02:30", departed: true, completion_rate: 10.1, consecutive_problem_days: 1 }),
  row({ facility_id: 8, facility_name: "メガバリューマート桜台店", serial_number: "8111R5628060122", daysBack: 0, time: "02:00", departed: true, completion_rate: 10.0, consecutive_problem_days: 1 }),
  row({ facility_id: 9, facility_name: "メガバリューマート三ツ橋中央ショッピングセンター店", serial_number: "8111R5628060133", daysBack: 0, time: "02:30", departed: true, completion_rate: 9.9, consecutive_problem_days: 1 }),
  row({ facility_id: 10, facility_name: "フレッシュタウン大和店", serial_number: "8111R5628060144", daysBack: 0, time: "03:00", departed: true, completion_rate: 0, consecutive_problem_days: 1 }),
  row({ facility_id: 12, facility_name: "フレッシュタウン湖畔店", serial_number: "8111R5628060166", daysBack: 0, time: "03:30", departed: false, completion_rate: null, consecutive_problem_days: 1 }),
];

// ---------------------------------------------------------------------------
// §2.2 エスカレーション
//
//   ケース                              連続  レベル  ルール
//   初回の出発失敗                        1     L1     R1
//   完了率×(初回)                       1     L1     R2
//   2日連続△以下                          2     L2     R3
//   3日連続                               3     L3     R4
//   5日連続(長期化)                      5     L3     R4
//   スケジュール非存在日を挟む連続          3     L3     R4  ← カレンダー上は5日
//
// 最後のケースが重要。「月水金」スケジュールの施設で月・水・金と3回連続で失敗した場合、
// カレンダー上は5日間だが、スケジュール存在日の連続は3日なのでL3になる。
// ---------------------------------------------------------------------------
export const escalationCases: FixtureCleanLogItem[] = [
  // R1: 初回の出発失敗(L1)
  // ★B2 が発火することを保つため "Handrail Lifted"(B2の暫定マッチ値。D-078項2)にしてある★
  row({ facility_id: 2, facility_name: "バリューマート北町店", serial_number: "8111R5628060022", daysBack: 1, time: "02:00", status_1h: "Handrail Lifted", status_30m: "Handrail Lifted", departed: false, completion_rate: null, consecutive_problem_days: 1 }),

  // R3: 2日連続△以下(L2)
  row({ facility_id: 3, facility_name: "メガバリューマート東口店", serial_number: "8111R5628060033", daysBack: 2, time: "01:30", departed: true, completion_rate: 45, consecutive_problem_days: 1 }),
  row({ facility_id: 3, facility_name: "メガバリューマート東口店", serial_number: "8111R5628060033", daysBack: 1, time: "01:30", departed: true, completion_rate: 38, consecutive_problem_days: 2 }),

  // R4: 3日連続(L3)
  // ★この「オフライン」は fixtures 由来の暫定値で、実データには存在しない★(D-077項3 / D-078項2)。
  //   B3のマッチ配列(lib/config/robot-status-match.ts の B3_OFFLINE_STATUSES)は空なので、
  //   この行では B3 は発火しない。★それが現在の期待挙動★(R3/R4は連続日数ベースなので影響を受けない)。
  //   実データでオフラインを表す値が判明したら、この値と設定値の両方を差し替えること。
  row({ facility_id: 6, facility_name: "バリューマート南丘駅前店", serial_number: "8111R5628060077", daysBack: 3, time: "02:30", status_1h: "オフライン", status_30m: "オフライン", departed: false, completion_rate: null, consecutive_problem_days: 1 }),
  row({ facility_id: 6, facility_name: "バリューマート南丘駅前店", serial_number: "8111R5628060077", daysBack: 2, time: "02:30", status_1h: "オフライン", status_30m: "オフライン", departed: false, completion_rate: null, consecutive_problem_days: 2 }),
  row({ facility_id: 6, facility_name: "バリューマート南丘駅前店", serial_number: "8111R5628060077", daysBack: 1, time: "02:30", status_1h: "オフライン", status_30m: "オフライン", departed: false, completion_rate: null, consecutive_problem_days: 3 }),

  // 5日連続(長期化。L3のまま)
  row({ facility_id: 9, facility_name: "メガバリューマート三ツ橋中央ショッピングセンター店", serial_number: "8111R5628060133", daysBack: 5, time: "02:30", departed: true, completion_rate: 22, consecutive_problem_days: 1 }),
  row({ facility_id: 9, facility_name: "メガバリューマート三ツ橋中央ショッピングセンター店", serial_number: "8111R5628060133", daysBack: 4, time: "02:30", departed: true, completion_rate: 18, consecutive_problem_days: 2 }),
  row({ facility_id: 9, facility_name: "メガバリューマート三ツ橋中央ショッピングセンター店", serial_number: "8111R5628060133", daysBack: 3, time: "02:30", departed: true, completion_rate: 25, consecutive_problem_days: 3 }),
  row({ facility_id: 9, facility_name: "メガバリューマート三ツ橋中央ショッピングセンター店", serial_number: "8111R5628060133", daysBack: 2, time: "02:30", departed: true, completion_rate: 15, consecutive_problem_days: 4 }),
  row({ facility_id: 9, facility_name: "メガバリューマート三ツ橋中央ショッピングセンター店", serial_number: "8111R5628060133", daysBack: 1, time: "02:30", departed: true, completion_rate: 12, consecutive_problem_days: 5 }),

  // ★スケジュール非存在日を挟む連続★
  // 施設4は「月水金」スケジュール。5日前・3日前・1日前の3回だけ実行され、いずれも失敗。
  // カレンダー上は5日間だが、スケジュール存在日の連続は3日なのでL3。
  row({ facility_id: 4, facility_name: "バリューマート西通り店", serial_number: "8111R5628060044", daysBack: 5, time: "03:00", departed: true, completion_rate: 41, consecutive_problem_days: 1 }),
  row({ facility_id: 4, facility_name: "バリューマート西通り店", serial_number: "8111R5628060044", daysBack: 3, time: "03:00", departed: true, completion_rate: 33, consecutive_problem_days: 2 }),
  row({ facility_id: 4, facility_name: "バリューマート西通り店", serial_number: "8111R5628060044", daysBack: 1, time: "03:00", status_1h: "セキュリティ有効", status_30m: "セキュリティ有効", departed: false, completion_rate: null, consecutive_problem_days: 3 }),
];

// ---------------------------------------------------------------------------
// §2.3 ステータスの組み合わせ
//   待機中 → 待機中(正常)
//   充電中 → 待機中(1h前異常だが解消)
//   ハンドルアップ → ハンドルアップ(未解消)
//   緊急停止 → 待機中(復旧)
//   オフライン → オフライン
//   セキュリティ有効(D-Sec契約施設のみ)
//
// ★事前チェックの分数(D-178。ユーザー判断 2026-10-16)★ — 列は分数ではなく順番で割り当てる:
//   施設7 ロボット0100 01:30 … 60分前・★10分前★(設定 [60,30] と違う分数でも2列目に出る)
//   施設7 ロボット0177 02:00 … ★30分前だけ★(1回しか無いので1列目に 30、2列目は null =「-」)
//   ★行は足さず既存の2行の分数だけを変えた★(件数・id・完了率・判定は変えない。事前チェックは判定に使わない。D-170)
//   60 / 30 は `PRECHECK_MINUTES_BEFORE` から読む。★10 は「設定に無い分数」であることが本題なので直書きする★
//   (`lib/db/__tests__/error-report-mapping.test.ts` の E9 と同じ考え方)
// ---------------------------------------------------------------------------
/** ★設定(`PRECHECK_MINUTES_BEFORE` = 60/30)に無い分数★(D-178 のケース) */
const PRECHECK_OFF_CONFIG_MINUTES = 10;
export const statusCases: FixtureCleanLogItem[] = [
  row({ facility_id: 1, facility_name: "メガバリューマート中央本店", serial_number: "8111R5628060011", daysBack: 1, time: "02:00", status_1h: "充電中", status_30m: "待機中", departed: true, completion_rate: 92 }),
  row({ facility_id: 7, facility_name: "メガバリューマート緑野店", serial_number: "8111R5628060099", daysBack: 1, time: "01:30", status_1h: "緊急停止", status_30m: "待機中", departed: true, completion_rate: 62, consecutive_problem_days: 1 }),
  // 1施設に複数ロボット = 別タスクで2行になることの確認(施設7は3台)
  //   ★事前チェックが60分前・10分前(D-178)★
  row({ facility_id: 7, facility_name: "メガバリューマート緑野店", serial_number: "8111R5628060100", daysBack: 1, time: "01:30", status_30m_minutes: PRECHECK_OFF_CONFIG_MINUTES, departed: true, completion_rate: 94 }),
  //   ★事前チェックが30分前の1回だけ(D-178)★ — 1列目に 30、2列目は null
  row({ facility_id: 7, facility_name: "メガバリューマート緑野店", serial_number: "8111R5628060177", daysBack: 1, time: "02:00", status_1h_minutes: PRECHECK_MINUTES_BEFORE[1], status_30m: null, departed: true, completion_rate: 88 }),
  row({ facility_id: 5, facility_name: "メガバリューマート港南総本店", serial_number: "8111R5628060055", daysBack: 1, time: "02:00", status_1h: "セキュリティ有効", status_30m: "セキュリティ有効", departed: false, completion_rate: null, consecutive_problem_days: 1 }),
  row({ facility_id: 5, facility_name: "メガバリューマート港南総本店", serial_number: "8111R5628060088", daysBack: 1, time: "02:30", departed: true, completion_rate: 97 }),
];

// ---------------------------------------------------------------------------
// §2.4 データ量
//
// 6〜30日前の履歴を機械生成する。日付フィルタの上限(31日)テスト用。
// 7日前は「△×が1件もない日」= 全部◎(該当0件の表示確認)にしてある。
// ---------------------------------------------------------------------------
const historyFacilities: Array<{
  id: number;
  name: string;
  serial: string;
  time: string;
  /** ★true = 事前チェックの無い施設(status_1h / status_30m が null。D-171)★ */
  noPrecheck?: boolean;
}> = [
  { id: 1, name: "メガバリューマート中央本店", serial: "8111R5628060011", time: "02:00" },
  { id: 2, name: "バリューマート北町店", serial: "8111R5628060022", time: "02:00" },
  { id: 3, name: "メガバリューマート東口店", serial: "8111R5628060033", time: "01:30" },
  { id: 5, name: "メガバリューマート港南総本店", serial: "8111R5628060055", time: "02:00" },
  { id: 6, name: "バリューマート南丘駅前店", serial: "8111R5628060077", time: "02:30" },
  { id: 7, name: "メガバリューマート緑野店", serial: "8111R5628060099", time: "01:30" },
  { id: 8, name: "メガバリューマート桜台店", serial: "8111R5628060122", time: "02:00" },
  { id: 9, name: "メガバリューマート三ツ橋中央ショッピングセンター店", serial: "8111R5628060133", time: "02:30" },
  { id: 10, name: "フレッシュタウン大和店", serial: "8111R5628060144", time: "03:00" },
  // ★川下店は「事前チェックの無い施設」★(status_1h / status_30m が null。ユーザー指示 2026-10-07。D-171)。
  //   ★件数・完了率・判定は変えない★(null にするのは事前チェックの2列だけ。事前チェックは判定に使わない。D-170)。
  //   ★川下店はアラートの fixtures(`./prechecks.ts`)に出てこない施設を選んだ★(予定が無いのにアラートがある、にならないように)
  { id: 11, name: "バリューマート川下店", serial: "8111R5628060155", time: "03:30", noPrecheck: true },
];

/** 決定的な擬似乱数(実行ごとに値が変わると表示確認しづらいため) */
function pseudoRandom(seed: number): number {
  const x = Math.sin(seed * 12.9898) * 43758.5453;
  return x - Math.floor(x);
}

function buildHistory(): FixtureCleanLogItem[] {
  const out: FixtureCleanLogItem[] = [];
  for (let d = 6; d <= 30; d += 1) {
    const allSuccessDay = d === 7; // 該当0件の日
    historyFacilities.forEach((f, i) => {
      const r = pseudoRandom(d * 100 + i);
      let rate: number | null;
      let departed = true;
      if (allSuccessDay) {
        rate = 91 + r * 8; // 全部◎
      } else if (r < 0.06) {
        rate = null; // 未実施
        departed = false;
      } else if (r < 0.14) {
        rate = 5 + r * 40; // △〜×
      } else if (r < 0.3) {
        rate = 72 + r * 15; // ○
      } else {
        rate = 90.5 + r * 9; // ◎
      }
      if (rate !== null) rate = Math.round(rate * 10) / 10;
      out.push(
        row({
          facility_id: f.id,
          facility_name: f.name,
          serial_number: f.serial,
          daysBack: d,
          time: f.time,
          ...(f.noPrecheck ? { status_1h: null, status_30m: null } : {}),
          departed,
          completion_rate: rate,
          consecutive_problem_days:
            rate === null || rate < GRADE_THRESHOLDS.good ? 1 : 0,
        })
      );
    });
  }
  return out;
}

export const history: FixtureCleanLogItem[] = buildHistory();

// ---------------------------------------------------------------------------
// ★結果待ち(D-173。ユーザー判断 2026-10-07)★
//
// ★今日の行のうち、結果待ち(`result_pending: true`)はこの1行だけ★。
//   ★施設11(川下店)の今日 03:30★ — 今日の行が無かった施設を選んだ(既存の行・境界値ケースを変えないため)。
//   ★施設12 03:30(§2.1 の「未実施(null)」)は確定した出発失敗のまま★(監督判断 2026-10-07)。
//   ★`departed: false` / `task_status: null` / `completion_rate: null`(→ grade ×)は出発失敗と同じ値★
//   (結果待ちは別の旗で表す)。★連続日数は 0★(結果待ちは連続日数の入力から外す。表2 C10。
//   Prisma 版は `consecutiveById` に載らず 0 になる)。川下店は事前チェックの無い施設なので2列は null(D-171)。
//   ★`history` の後ろで作る★ — 前に置くと `nextId()` の採番がずれ、既存の行の id が全部変わる。
// ---------------------------------------------------------------------------
export const pendingCases: FixtureCleanLogItem[] = [
  row({ facility_id: 11, facility_name: "バリューマート川下店", serial_number: "8111R5628060155", daysBack: 0, time: "03:30", status_1h: null, status_30m: null, departed: false, completion_rate: null, consecutive_problem_days: 0, result_pending: true }),
];

// ---------------------------------------------------------------------------
// ★予定外タスク(指示書BG §1・§2-1。ユーザー指示 2026-10-29)★
//
// ★`cleanLogs`(予定のある行)には混ぜない★ — `./areas`・`./clean-task-reports`・シード(`scripts/seed-local-db.ts`)が
//   `cleanLogs` の予定の id を鍵に行を作るため。★混ぜるのは `fixtureSource.cleanLogs()` だけ★(`lib/db/index.ts`)。
// ★行は `unscheduledCleanLogItem()`(Prisma 版と同じ関数)で作る★ — fixtures 専用の組み立てを書かない(D-044)。
//
//   施設1  1日前 21:00 … 完了率 95.9(◎)・status 4(表U1 U1-1)。同じ日に予定のある ◎(92)があるので連続 0
//   施設11 2日前 22:00 … clean_area 0 → 完了率 0(×)・status 6・★出発 ✓(未実施ではない)★(表U1 U1-2)。
//                        施設11 は2〜5日前に予定が無い(`history` は6日前から・今日は結果待ち)ので、この日だけで連続 1(L1)
//   ★`report_id` は19桁の文字列★(実データの `clean_task_reports.report_id` と同じ桁。安全な整数を超える)。
//   ★`history` の後ろで作っても予定の行の `nextId()` には影響しない★(予定外の行は採番しない)。
// ---------------------------------------------------------------------------
export const unscheduledCleanLogs: CleanLogItem[] = [
  unscheduledCleanLogItem(
    {
      report_id: "9200000000000000001",
      clean_timetable_id: null,
      status: 4,
      task_area: 1000,
      clean_area: 959,
      clean_ratio: 95.9,
      started_at_iso: at(1, "21:00"),
    },
    {
      facility_id: 1,
      facility_name: "メガバリューマート中央本店",
      model_name: "PUDU CC1",
      serial_number: "8111R5628060011",
    },
    0
  ),
  unscheduledCleanLogItem(
    {
      report_id: "9200000000000000002",
      clean_timetable_id: 0,
      status: 6,
      task_area: 800,
      clean_area: 0,
      clean_ratio: 0,
      started_at_iso: at(2, "22:00"),
    },
    {
      facility_id: 11,
      facility_name: "バリューマート川下店",
      model_name: "PUDU CC1",
      serial_number: "8111R5628060155",
    },
    1
  ),
];

/** 新しい順(scheduled_at 降順) */
export const cleanLogs: FixtureCleanLogItem[] = [
  ...boundaryCases,
  ...escalationCases,
  ...statusCases,
  ...history,
  ...pendingCases,
].sort((a, b) => (a.scheduled_at < b.scheduled_at ? 1 : -1));