/**
* GET /prechecks — アラート(事前チェック)一覧+サマリ
*
* ★返すのは事前チェック異常の行だけ★(ユーザー判断 2026-10-29。出発失敗は清掃詳細ログ・ダッシュボードで見る)。
* summary・meta.total も事前チェック異常の行だけを数える(`db.prechecks()` が出発失敗の行を作らないため、ここは変えていない)。
*
* 形は `app/api/clean-logs/route.ts`(動く見本)に合わせている。
* 仕様の正は `d-watch-api.yaml` の /prechecks、status=open の意味は docs/decisions.md D-054。
*
* ★対応状況の更新(PATCH /prechecks/{id}/status)は `app/api/prechecks/[id]/status/route.ts`★(指示書AT)
*/
import { NextResponse } from "next/server";
import { db } from "@/lib/db";
import { resolveFacilityIdsByMasters } from "@/lib/facilities/resolve-by-master";
import {
filterPrechecksByDate,
filterPrechecksByStatus,
parsePrecheckQuery,
sortPrechecks,
summarizePrechecks,
} from "@/lib/prechecks/query";
import { withRequestId } from "@/lib/logger";
import { logApiError } from "@/lib/logger/api";
import type { ApiError, PrecheckItem, SummaryListResponse } from "@/lib/types";
import type { PrecheckSummary } from "@/lib/prechecks/query";
const ROUTE = "/api/prechecks";
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 = parsePrecheckQuery(searchParams);
if (!parsed.ok) {
return errorResponse(parsed.error, parsed.status);
}
let rows: PrecheckItem[];
try {
// ★★日付範囲を SQL に降ろす★★(指示書AC 段階4)—
// ★`filterPrechecksByDate()` は**残す**★(指示書AC C-5。絞り込み規則の「正」は
// 純粋関数のまま。SQL 側はその写し)。
// ★★生の `searchParams` を渡していない★★(§E-2)— `parsePrecheckQuery()` が
// `YYYY-MM-DD` の形式を検証し、不正なら 422 で上で返している。
// ★`filterPrechecksByDate()` と**同じ文字列**を渡すこと★(片方だけ `Date` に
// 直すと、DBの壁時計値との取り違え(C-6)が入り込む)。
// ★★分類(`group_id` / `customer_id`)は施設IDの配列に解決してから SQL に渡す★★
// (指示書AK 段階4)— ★`null` = 絞らない / `[]` = 0件★(★罠1★)。
// ★`/prechecks` には施設名の検索が無い★ので積集合は要らない
// (`d-watch-api.yaml` のクエリパラメータは date_from / date_to / group_id /
// customer_id / status / page / per_page のみ)。
// ★★施設名で絞りたくなったら先に YAML を直し、`/clean-logs` と同じく
// `intersectFacilityIds()` で AND に畳むこと★★(`CLAUDE.md` 作業ルール1)。
//
// ★★サマリ(`summary`)にも効く★★ — ここで母集団ごと絞るので、
// ★一覧と「未対応◯件」のカードが同じ母集団から作られる★
// (指示書AK §7-2 が `/dashboard` の数値カードについて言っているのと同じ)。
rows = await db.prechecks({
date_from: parsed.query.dateFrom,
date_to: parsed.query.dateTo,
facility_ids: 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);
}
const dated = filterPrechecksByDate(rows, parsed.query);
// ★summaryは日付範囲で絞った後・対応状況フィルタの前に計算する(D-054)★
// 状況フィルタの後だと、既定表示(status=open)で解消済みカードが常に0になり、
// サマリーの役目を果たさなくなる。日付範囲を変えたときだけ summary が変わる
const summary: PrecheckSummary = summarizePrechecks(dated);
const sorted = sortPrechecks(
filterPrechecksByStatus(dated, parsed.query.status)
);
const { page, perPage } = parsed.query;
// ★meta.total と summary.total は意味が違う★
// meta.total = 一覧(対応状況フィルタ後)の総件数。ページング計算用
// summary.total = 日付範囲で絞った当日の全件(対応状況フィルタの影響を受けない)
// status=all のときだけ一致する。status=open 等で絞ると meta.total < summary.total になりうる
const total = sorted.length;
const start = (page - 1) * perPage;
const items = sorted.slice(start, start + perPage);
const body: SummaryListResponse<PrecheckItem, PrecheckSummary> = {
summary,
items,
meta: { total, page, per_page: perPage },
};
return NextResponse.json(body);
});
}