/**
* GET /reports/continuous-problems — 連続問題施設一覧(2日以上連続で△以下 or 出発失敗)
*
* 形は `app/api/ds-alerts/route.ts` に合わせている。
* 仕様の正は `d-watch-api.yaml` の /reports/continuous-problems。
*/
import { NextResponse } from "next/server";
import { CONSECUTIVE_DAYS_DISPLAY } from "@/lib/config";
import { db } from "@/lib/db";
import { capContinuousProblems } from "@/lib/db/report-mapping";
import { resolveFacilityIdsByMasters } from "@/lib/facilities/resolve-by-master";
import {
filterContinuousProblems,
parseReportQuery,
selectContinuousProblemRows,
sortContinuousProblems,
toReportDbQuery,
} from "@/lib/reports/query";
import { withRequestId } from "@/lib/logger";
import { logApiError } from "@/lib/logger/api";
import { loadConsecutiveDaysMax } from "@/lib/settings/display-limits";
import type {
ApiError,
ContinuousProblem,
ContinuousProblemRow,
ListResponse,
} from "@/lib/types";
const ROUTE = "/api/reports/continuous-problems";
function errorResponse(error: ApiError, status: number) {
return NextResponse.json(error, { status });
}
export async function GET(request: Request) {
return withRequestId(request, async () => {
const { searchParams } = new URL(request.url);
const parsed = parseReportQuery(searchParams);
if (!parsed.ok) {
return errorResponse(parsed.error, parsed.status);
}
let rows: ContinuousProblemRow[];
// ★表示上限も try の中で取る★(ファイル読み込みは失敗しうる)。
// `loadConsecutiveDaysMax()` は内部で既定値に倒れる設計(指示書AF 段階3-3)なので
// 通常は例外にならないが、★「設定が読めない」だけで一覧を 500 にしない★ため、
// 下の catch とは分けて既定値で続行する。
// ★型注釈 `: number` が要る★ — `CONSECUTIVE_DAYS_DISPLAY` は `as const` なので
// 注釈が無いとリテラル型 `30` に推論され、`loadConsecutiveDaysMax()` の戻りを代入できない
let maxDays: number = CONSECUTIVE_DAYS_DISPLAY.defaultMax;
try {
maxDays = await loadConsecutiveDaysMax();
} catch {
// 既定値のまま続行する(一覧そのものは返せる)
}
try {
// ★★期間を SQL に降ろす★★(指示書AC 段階5)— ★`parseReportQuery()` を通った値を渡す★。
// ★★`selectContinuousProblemRows()` / `filterContinuousProblems()` /
// `sortContinuousProblems()` は残す★★(絞り込み規則の「正」。指示書AC C-5)。
// ★★分類(`group_id` / `customer_id`)は施設IDの配列に解決してから渡す★★
// (指示書AK 段階4b)— ★`null` = 絞らない / `[]` = 0件★(★罠1★)。
// ★このAPIに施設名の条件は無い★(`d-watch-api.yaml` の
// `/reports/continuous-problems` のパラメータは period/year/month/week/
// group_id/customer_id/limit のみ)ので積集合は要らない。
// ★★施設名で絞りたくなったら先に YAML を直し、`/reports/facility-performance` と
// 同じく `intersectFacilityIds()` で AND に畳むこと★★(`CLAUDE.md` 作業ルール1)。
rows = await db.continuousProblems(
toReportDbQuery(parsed.query, await resolveFacilityIdsByMasters(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 errorResponse(body, 500);
}
// ここから下は純粋な絞り込み・並べ替え。すべて try の外
// (`app/api/ds-alerts/route.ts` と同じ方針)。
const inPeriod: ContinuousProblem[] = selectContinuousProblemRows(
rows,
parsed.query
);
// 2日以上のみ返す(L1(初回)は含めない)。閾値は `lib/config` から読む
// (CLAUDE.md §7 ハードコード禁止。判断は `filterContinuousProblems` 側)。
const filtered = filterContinuousProblems(inPeriod);
const sorted = sortContinuousProblems(filtered);
// ★meta.total は「2日以上の絞り込み後・limit 適用前」の件数★(YAML の meta の説明)
// 画面の「他N件を表示」の算出元がこれ。
const total = sorted.length;
const { limit } = parsed.query;
// ★表示上限は「絞り込み → 並べ替え → 切り出し」の**最後**にかける★(指示書AF 段階5-2)
// 先にかけると上限に達した施設どうしの降順が潰れる。`meta.total` も上限とは無関係
// (件数は変わらない)。
// ★データソース(`lib/db/`)の中ではなくルート層でかける★ —
// `USE_FIXTURES` の両方で同じ挙動にするため(`CLAUDE.md` §9.5)。
const items = capContinuousProblems(sorted.slice(0, limit), maxDays);
// page / per_page パラメータを持たない一覧なので meta.page は常に1、
// meta.per_page は実際に適用された limit を返す(YAML の meta の説明)。
const body: ListResponse<ContinuousProblem> = {
items,
meta: { total, page: 1, per_page: limit },
};
return NextResponse.json(body);
});
}