/**
* GET /reports/facility-performance/export — 施設別パフォーマンスの CSV エクスポート(D-163)
*
* /reports/facility-performance と同一のパラメータを受け付ける(★`limit` は無視して全件を出す★)。
* UTF-8 BOM付き、ヘッダ行あり、★画面の言語によらず日本語★。
* 仕様の正は `d-watch-api.yaml` の /reports/facility-performance/export。
*/
import { NextResponse } from "next/server";
import { MAX_CSV_ROWS } from "@/lib/config";
import { withRequestId } from "@/lib/logger";
import { logApiError } from "@/lib/logger/api";
// ★CSVの組み立ては lib/reports/facility-performance-csv.ts★ — App Router の route.ts は
// HTTPメソッド以外の export を想定しておらず、ここに置くとテストから呼べない
// (`app/api/clean-logs/export/route.ts` と同じ理由。D-125)
import {
buildFacilityPerformanceCsv,
facilityPerformanceCsvFilename,
} from "@/lib/reports/facility-performance-csv";
import { loadFacilityPerformance } from "@/lib/reports/facility-performance-load";
import { parseReportQuery } from "@/lib/reports/query";
import type { ApiError, FacilityPerformance } from "@/lib/types";
const ROUTE = "/api/reports/facility-performance/export";
export async function GET(request: Request) {
return withRequestId(request, async () => {
const { searchParams } = new URL(request.url);
// ★検証は一覧と同じ★(period / year / month / week / group_id / customer_id が不正なら 422)。
// `limit` も読まれるが、不正値でも 422 にはならない(丸めるだけ)うえ、下で使わない。
const parsed = parseReportQuery(searchParams);
if (!parsed.ok) {
return NextResponse.json(parsed.error, { status: parsed.status });
}
let items: FacilityPerformance[];
try {
// ★★一覧(`/reports/facility-performance`)と**同じ1本**を呼ぶ★★(D-163)—
// ★★片方だけ絞ると、画面の表と CSV の中身が食い違います★★。
// ★`limit` で切らない★ — 画面の「全施設表示」の200件上限に縛られず、絞り込み後の全施設を出す。
items = await loadFacilityPerformance(parsed.query);
} catch (e) {
const body: ApiError = {
error: { code: "internal_error", message: "データの取得に失敗しました", details: null },
};
logApiError({ route: ROUTE, method: "GET", body, status: 500, err: e });
return NextResponse.json(body, { status: 500 });
}
// ★★行数の歯止め(清掃詳細ログの CSV と揃える。D-163)★★
//
// 施設数なので実際にはまず起きないが、★上限・形・境界は `/clean-logs/export` と同じ★にする
// (`MAX_CSV_ROWS` を共有。★上限は「含む」= `>` であって `>=` ではない★)。
// ★CSV を組み立てる**前**に数える★(組み立ててから長さで判断しない)。
// ★500 ではなく 422★ — 利用者の指定が上限を超えているという話で、サーバの不具合ではない。
if (items.length > MAX_CSV_ROWS) {
const body: ApiError = {
error: {
code: "validation_failed",
message:
`CSVで出力できません。対象の施設が${items.length}件で、` +
`上限の${MAX_CSV_ROWS}件を超えています。` +
`分類・施設名・ステータスで絞り込んでください。`,
// ★実数を入れる★ — 「あと何件減らせばよいか」が文言を読まずに分かる
details: { total: items.length, max: MAX_CSV_ROWS },
},
};
return NextResponse.json(body, { status: 422 });
}
const csv = buildFacilityPerformanceCsv(items, parsed.query.period);
// ★ファイル名は parse 済みの数値から組み立てる★(生のクエリ文字列を入れない)ので、
// Content-Disposition に `"` や改行などの制御文字は入らない。
const filename = facilityPerformanceCsvFilename(parsed.query);
return new NextResponse(csv, {
status: 200,
headers: {
"Content-Type": "text/csv; charset=utf-8",
"Content-Disposition": `attachment; filename="${filename}"`,
},
});
});
}