Newer
Older
client_dwatch / lib / format / clean-result.ts
/**
 * 清掃結果(★`clean_task_reports.status`★)の表示整形(指示書AQ §1-2)。
 *
 * ★★取得元が変わった(指示書AQ 決定2・決定3。2026-09-30 ユーザー確定)★★ —
 *   清掃実績の参照元が `clean_status_histories` → ★`clean_task_reports`★ になり、
 *   ★新しいテーブルに `clean_error` 列が存在しない★(`prisma/schema.prisma:517-558`)。
 *   旧 `formatCleanResult(clean_error)` は旧 `CLEAN_ERROR_LABELS`(定義書の原文。2026-10-19 削除)で
 *   日本語ラベルを引いていたが、★その対応表は `clean_status_histories.clean_error` のもの★
 *   であり、★`clean_task_reports.status` に流用できる根拠は無い★。
 *
 * ★★`status` は表示名にする★★(2026-10-02。D-155 項2)—
 *   TeamWorks 第10回 N-2 で値の意味を受領した(`CLEAN_TASK_STATUS_LABELS`。原文ママ):
 *   0 清掃開始前 / 1 清掃中 / 2 清掃中断 / 3 清掃キャンセル / 4 清掃正常終了 / 5 清掃異常終了。
 *   ★6 は 3 と同じ「清掃キャンセル」★(N-2 の表に無い値。ユーザー指示 2026-10-30)。
 *   ★表に無い値(7 以上など)は「不明(7)」★(数字を残す。ユーザー指示 2026-10-30)。
 *   ★「その他」「不明」に潰さないこと★(D-089 項3 / D-104 項2)。
 *   ★判定には使わない★(表示だけ。D-155 項2)。★英語は画面側で `t()` に通す★(`lib/i18n.ts`。仮訳)。
 *   ★CSV は日本語の表示名のまま★(見出しも日本語固定。指示書AS D11)。
 *   ★旧: 定義書に値の説明が無かった(`prisma/schema.prisma:528-529`)ので生値をそのまま返していた★
 *   (D-077 / `CLAUDE.md` §4.8。指示書AQ §1-2)。
 *   ★★`CLEAN_ERROR` に対応づけないこと★★ — 別のテーブルの別のコード表(`clean_status_histories.clean_error`)。
 *
 * ★★関数名を `formatCleanResult()` から改名した理由★★ —
 *   引数の意味が `clean_error` → `status` に変わったのに、★どちらも `number | null` なので
 *   旧名のままだと呼び出し側が `r.clean_error` を渡し続けても tsc が通ってしまう★
 *   (fail-silent。画面には日本語ラベルの代わりに数字が出るだけで、誰も気づけない)。
 *   ★名前を変えて呼び出し側をコンパイルエラーで止める★。
 *   ★旧名の再export は作らないこと★(指示書AQ 担当C 指示)。
 *
 * ★★`null` を「出発失敗」と表示する根拠★★(★従来から変わっていない★):
 *   - ★指示書AQ §1-1★: 「未実施 = 本清掃の `clean_timetables` 行に対応する
 *     `clean_task_reports` の行が無い」。★「出発失敗」と「未実施」は同一扱いのまま★
 *     (ユーザー確認 2026-09-30)
 *   - ★ユーザー確定(2026-09-16)★: 「`null` は『出発失敗』と表示する」
 *   - `lib/db/clean-log-mapping.ts` の `notExecutedFromReport()` — ★レポート行が無ければ
 *     `departed === false` / `completion_rate === null`★。★本関数の `null` と同じ事実を指す★
 *
 * ★`formatRawLabel()` の `EMPTY_VALUE_LABEL`(`(値なし)`)を使わない理由★:
 *   あちらは「DBのカラムが空である」という事実だけを述べる文言だが、ここは
 *   「レポート行が無い=出発していない」と★意味が確定している★場面である(文言はユーザー指定)。
 *
 * ★DB アクセスなし・例外を投げない純粋関数。lib/judge/ には置かない★
 * (あちらは◎○△×等の評価ロジックの置き場所。こちらは表示整形。CLAUDE.md §9.1)。
 */
import { cleanTaskStatusLabel, isKnownCleanTaskStatus } from "@/lib/config";
import type { CleanLogItem } from "@/lib/types";

/** レポート行が無い(=出発失敗/未実施)ときの表示。★ユーザー確定 2026-09-16★ */
export const CLEAN_RESULT_NOT_DEPARTED_LABEL = "出発失敗";

/**
 * `clean_task_reports.status` → 詳細ログ「清掃結果」列 / CSV の表示文字列。
 *
 * @param status `clean_task_reports.status` の生値。
 *               ★`null` は「対応するレポート行が無い」ことを表す★(値が NULL なのではない。
 *               `status` 列自体は `NOT NULL DEFAULT 0`)。
 * @returns `null` なら `CLEAN_RESULT_NOT_DEPARTED_LABEL`、それ以外は ★`cleanTaskStatusLabel(status)`★
 *          (日本語の表示名。★表に無い値は「不明(7)」★)。★日本語のまま返す★ — 画面は `t()` に通し、
 *          CSV はそのまま出す(指示書AS D11)
 */
export function formatCleanTaskStatus(status: number | null): string {
  // ★`??` を `||` にしないこと★ — `0`(清掃開始前)が「出発失敗」に化ける
  if (status === null) {
    return CLEAN_RESULT_NOT_DEPARTED_LABEL;
  }
  return cleanTaskStatusLabel(status);
}

/**
 * ★結果待ち(`CleanLogItem.result_pending === true`)の行の表示★(ユーザー判断 2026-10-07。D-173)。
 * 画面(`t()` で "Pending")と CSV(日本語のまま。出発列にも入れる)で使う。
 */
export const CLEAN_RESULT_PENDING_LABEL = "結果待ち";

/**
 * ★予定外タスクの行(`CleanLogItem.unscheduled === true`)の「予定」列★(指示書BG §2-1。ユーザー指示 2026-10-29)。
 * 時刻の代わりに出す。画面は `t()` で "No schedule"、CSV は日本語のまま(「予定時刻」列)。
 * ★文言は「予定なし」★(ユーザー指示 2026-11-04「文言を予定なし、予定ありに統一して」。旧: 「予定外」/ EN "Unscheduled")。
 *   概念名(予定外タスク)・`unscheduled` などのコード上の名前は変えない。
 */
export const CLEAN_LOG_UNSCHEDULED_LABEL = "予定なし";

/** 詳細ログの1行の見え方(`cleanLogRowView()` の戻り) */
export interface CleanLogRowView {
  /** 出発列。ok = ✓ / ng = ✗ / ★pending = …(結果待ち。✓✗ の色を使わない)★ */
  departed: "ok" | "ng" | "pending";
  /** 清掃結果列(★日本語★。画面は `t()` に通し、CSV はそのまま出す) */
  resultLabel: string;
  /**
   * ★表に無い `status`(7 以上など)のときだけ、その生値★。それ以外は null。
   * 画面は `t(CLEAN_TASK_STATUS_UNKNOWN_TEMPLATE)` の `{id}` をこれで置き換える(英語で "Unknown (7)")。
   * `resultLabel`(「不明(7)」)をそのまま `t()` に通しても訳が引けないため。CSV は `resultLabel` のまま
   */
  unknownStatus: number | null;
  /** 完了率列に出す値(★結果待ちは null★。画面は `displayRate()` で「-」、CSV は `csvRate()` で空) */
  completionRate: number | null;
  /** 判定バッジを出すか(★結果待ちは出さない★。画面は「-」、CSV は空) */
  gradeShown: boolean;
  /**
   * 行を開けるか(△× のアドバイス)。★結果待ちは開けない★。
   * 予定あり・予定なしとも △× なら true(予定なしは `/api/clean-logs/unscheduled/{report_id}/advice`。指示書BI)
   */
  expandable: boolean;
  /**
   * 「エリア別詳細を見る →」を出すか。★予定のある行だけ true★(`/areas` は予定の id が要る。指示書BI)
   */
  areaDetail: boolean;
}

/**
 * 清掃詳細ログの1行の見え方を決める(表2 C2・C4。D-173)。★純粋関数★。
 *
 * ★★結果待ちは `departed: false` / `task_status: null` / `completion_rate: null` / `grade: "×"` のまま★★
 * (不変条件を壊さない)なので、★旗(`result_pending`)を見ないと出発失敗と区別できない★。
 * 表(`CleanLogTable.tsx`。ダッシュボードの縮小表示も)と CSV(`lib/clean-logs/csv.ts`)が
 * ★ここを共有する★ — 片方だけ直すと、同じ行が画面では結果待ち・CSV では出発失敗になる(例外もテスト失敗も出ない)。
 * ★判定(`grade`)は書き換えない★(API から受け取ったまま。§9.6)。出すか出さないかだけを決める。
 */
export function cleanLogRowView(
  row: Pick<
    CleanLogItem,
    "departed" | "task_status" | "completion_rate" | "grade" | "result_pending" | "unscheduled"
  >
): CleanLogRowView {
  if (row.result_pending) {
    return {
      departed: "pending",
      resultLabel: CLEAN_RESULT_PENDING_LABEL,
      unknownStatus: null,
      completionRate: null,
      gradeShown: false,
      expandable: false,
      areaDetail: false,
    };
  }
  const expandable = row.grade === "△" || row.grade === "×";
  return {
    departed: row.departed ? "ok" : "ng",
    resultLabel: formatCleanTaskStatus(row.task_status),
    unknownStatus:
      row.task_status !== null && !isKnownCleanTaskStatus(row.task_status) ? row.task_status : null,
    completionRate: row.completion_rate,
    gradeShown: true,
    expandable,
    areaDetail: expandable && !row.unscheduled,
  };
}