Newer
Older
client_dwatch / lib / reports / facility-performance-csv.ts
/**
 * GET /reports/facility-performance/export の CSV 組み立て(D-163)。
 *
 * ルート(`app/api/reports/facility-performance/export/route.ts`)から切り出してある。
 * 理由は清掃詳細ログ(`lib/clean-logs/csv.ts`)と同じで、★テストを書くため★
 * (Next.js の App Router は `route.ts` から HTTP メソッド以外を export することを想定していない)。
 *
 * ★形式は清掃詳細ログと同じ★: UTF-8 BOM・CRLF・最後も CRLF・ヘッダ行あり・
 * ★全セルを `csvCell()`(数式インジェクション対策 D-125)に通す★(`csvDocument()` の中で行う)。
 * ★画面の言語によらず日本語★(CSV は言語を受け取らない。指示書AS D11)。
 */
import { csvDocument, csvRate } from "@/lib/csv";
import { prevPeriodLabel } from "@/lib/format/period";
import {
  diffText,
  FACILITY_STATUS_LABEL,
  overallDiff,
} from "@/lib/reports/facility-performance-labels";
import type { ReportQuery } from "@/lib/reports/query";
import type { FacilityPerformance, Period } from "@/lib/types";

/**
 * ヘッダ行の見出し。
 *
 * ★列は画面(`FacilityPerformanceTable`)の9列と同じ順に、施設名の直後へシリアルを足したもの★(D-163)。
 * 列順を画面と変えると、突き合わせる人が毎回並べ替えることになる(清掃詳細ログと同じ考え方。指示書AO 段階3)。
 * 「前◯」は `prevPeriodLabel()` の日本語(前週 / 前月 / 前年)で、画面の列見出しと同じ出所。
 */
export function facilityPerformanceCsvHeader(period: Period): string[] {
  const prev = prevPeriodLabel(period);
  return [
    "施設名",
    "シリアル",
    "稼働率",
    `稼働率(${prev})`,
    "出発成功率",
    "完了率",
    `完了率(${prev})`,
    "エラー率",
    "変化",
    "ステータス",
  ];
}

/**
 * 施設別パフォーマンスの CSV 本体を組み立てる。
 *
 * - ★`items` はルートが絞り込み・並べ替えまで済ませた配列★。ここで並べ替えない(画面と同じ順で出す)
 * - ★率は `csvRate()`(四捨五入の整数+`%`、★null は空欄★)★。★丸めは表示のみ★(CLAUDE.md §4.3)。
 *   ★null を空欄にするのはユーザー指示 2026-10-06「値がないとこは空欄で」(D-164)★ —
 *   以前は `displayRate()` の `-` を出しており、`csvCell()` の数式対策で `'-` になって Excel に見えていた
 * - ★ステータスは API の `status` を表示名にするだけ★。ここで判定し直さない(CLAUDE.md §9.6)
 * - 「変化」は画面と同じ `overallDiff()` / `diffText()`(`lib/reports/facility-performance-labels.ts`)
 * - ★`csvCell()` はここでは呼ばない★ — `csvDocument()` が全セルに通す(関門を1つにする。D-125)
 */
export function buildFacilityPerformanceCsv(
  items: readonly FacilityPerformance[],
  period: Period
): string {
  return csvDocument(
    facilityPerformanceCsvHeader(period),
    items.map((item) => [
      item.facility_name,
      item.serial_number,
      csvRate(item.operation_rate.value),
      csvRate(item.operation_rate.prev),
      csvRate(item.launch_rate.value),
      csvRate(item.completion_rate.value),
      csvRate(item.completion_rate.prev),
      csvRate(item.error_rate.value),
      diffText(overallDiff(item)),
      FACILITY_STATUS_LABEL[item.status],
    ])
  );
}

/**
 * ダウンロード時のファイル名(`Content-Disposition` の `filename`)。
 *
 * ★ASCII のみ★(`filename="..."` に日本語を入れると、ブラウザによって文字化けする)。
 * ★値は parse 済みの数値から組み立てる★(生のクエリ文字列を入れない。ヘッダへの注入を防ぐ)。
 * ★月は2桁★。例:
 *   - weekly  → `facility-performance_weekly_2026-10-w1.csv`
 *   - monthly → `facility-performance_monthly_2026-09.csv`
 *   - yearly  → `facility-performance_yearly_2025.csv`(年次は month / week を持たない。CLAUDE.md §4.7)
 */
export function facilityPerformanceCsvFilename(
  q: Pick<ReportQuery, "period" | "year" | "month" | "week">
): string {
  const mm = String(q.month).padStart(2, "0");
  const key =
    q.period === "yearly"
      ? `${q.year}`
      : q.period === "monthly"
        ? `${q.year}-${mm}`
        : `${q.year}-${mm}-w${q.week}`;
  return `facility-performance_${q.period}_${key}.csv`;
}