/**
* GET /clean-logs/export のCSV組み立て。
*
* ルート(`app/api/clean-logs/export/route.ts`)から切り出してある。
* 理由は★テストを書くため★で、ルートの `route.ts` は Next.js の App Router が
* HTTPメソッド以外の export を想定していないため、ここに置いてから import する。
* ★行の組み立て(`buildCleanLogCsv()`)も同じ理由でここに置く★ —
* 「全列が `csvCell()` を通っているか」はセル単体のテストでは確かめられない。
*/
import { csvDocument, csvRate } from "@/lib/csv";
import {
CLEAN_LOG_UNSCHEDULED_LABEL,
CLEAN_RESULT_PENDING_LABEL,
cleanLogRowView,
} from "@/lib/format/clean-result";
import { formatHm, formatYmdSlash } from "@/lib/format/date";
import type { CleanLogItem } from "@/lib/types";
/**
* ★`csvCell()` の本体は `lib/csv.ts` へ移した★(D-163。レポートの施設別パフォーマンス CSV と共有するため)。
* 既存の import 先(`@/lib/clean-logs/csv`)を壊さないよう、ここから再エクスポートする。
*/
export { csvCell } from "@/lib/csv";
/** ヘッダ行の見出し(モックの列順に合わせる) */
export const CLEAN_LOG_CSV_HEADER = [
"施設名",
"機種",
"シリアル",
"日付",
"予定時刻",
// ★事前チェックは「1回目 / 2回目」と「何分前か」の4列★(D-178。ユーザー判断 2026-10-16)—
// 列は分数ではなく順番で割り当てる(早い方が1回目)ので、見出しに分数を書かず、分数は隣の列に数値で出す。
// ★旧: 「1h前ST」「30分前ST」★(設定で分数が変わると見出しと中身が食い違うため改めた)
"事前チェック1回目",
"1回目(分前)",
"事前チェック2回目",
"2回目(分前)",
"出発",
// ★画面(CleanLogTable)と同じ位置★ — `出発`(✓/✗)も `清掃結果` も出所は同じ
// `clean_task_reports`(行の有無と `status`。指示書AQ §1-1 / §1-2)なので、
// 隣に並べてその場で読めるようにする。★中身は `status` の日本語の表示名(例 `清掃正常終了`)★
// (第10回 N-2。D-155 項2)。★6 は清掃キャンセル、7 以上は「不明(7)」★。★CSV は言語を受け取らないので
// 見出しと同じく日本語固定★(指示書AS D11)。
// 列順を画面と変えると、突き合わせる人が毎回並べ替えることになる(指示書AO 段階3)。
"清掃結果",
"完了率",
"判定",
] as const;
/**
* 清掃詳細ログのCSV本体を組み立てる(UTF-8 BOM付き・CRLF区切り)。
*
* ★全列を例外なく `csvCell()` に通す★
* 「この列は固定文言だから安全」という判断を列ごとにすると、後から列を足した人が
* 同じ判断を迫られて漏れる。実際 `displayRate()` は未実施(`completion_rate: null`)で
* ★`-` を返す★(`lib/judge/grade.ts`)ので、安全に見える列が
* 数式インジェクションの入口になっていた。関門を1つにしておけば、
* 列の追加は「生の文字列を並べる」だけで済む。
* ★`csvCell()` を通すのは `csvDocument()`(`lib/csv.ts`)の中★(D-163。ここでは包まない)。
*
* ★完了率は `csvRate()`(null は空欄)★(ユーザー指示 2026-10-06「値がないとこは空欄で」。D-164)。
* `displayRate()` の `-` を出すと関門が `'-` にして、Excel で `'-` と見えていた。
* ★清掃結果列(`task_status` が null)は値の欠けではなく表示名なので、ここでは変えない★。
*/
export function buildCleanLogCsv(rows: CleanLogItem[]): string {
return csvDocument(
CLEAN_LOG_CSV_HEADER,
rows.map((r) => {
// ★結果待ち(D-173)★ — 見え方は画面と同じ `cleanLogRowView()` で決める(ユーザー判断 2026-10-07)。
// 結果待ちは 出発・清掃結果が「結果待ち」、★完了率・判定は空のセル★(「-」を入れない。D-164)
const view = cleanLogRowView(r);
return [
r.facility_name,
// ★null(清掃エリアに有効なロボットがいない。指示書BB U2・D-182)は空のセル★(`'-` にしない。D-164 と同じ理由)
r.model_name ?? "",
r.serial_number ?? "",
// ★日付は年つき `YYYY/MM/DD`(指示書BB A4・D-182。旧: `M/D`。画面の `formatMd()` は変えない)★
// ★予定外タスクの行(指示書BG §2-1)★ — 日付は `scheduled_at`(= `started_at`)の日付、予定時刻は「予定なし」(ユーザー指示 2026-11-04。旧: 「予定外」)。
// 事前チェックの4列は null なので下で空のセルになる
formatYmdSlash(r.scheduled_at),
r.unscheduled ? CLEAN_LOG_UNSCHEDULED_LABEL : formatHm(r.scheduled_at),
// ★null(事前チェックの予定が無い)は空のセル★(ユーザー指示 2026-10-07。D-171)。
// ★画面の「-」を入れないこと★ — `csvCell()` の数式対策で `'-` になる(D-164 と同じ理由)
r.status_1h ?? "",
// ★分数は数値のまま(例 60)。null(その回の予定が無い)は空のセル★(D-178)
r.status_1h_minutes === null ? "" : String(r.status_1h_minutes),
r.status_30m ?? "",
r.status_30m_minutes === null ? "" : String(r.status_30m_minutes),
view.departed === "pending"
? CLEAN_RESULT_PENDING_LABEL
: view.departed === "ok"
? "出発"
: "未出発",
view.resultLabel,
csvRate(view.completionRate),
view.gradeShown ? r.grade : "",
];
})
);
}