/**
* GET /errors — エラー一覧(`robot_error_reports` の未対応エラー。D-160 / 指示書AW 段階4-2)
*
* 形は `app/api/clean-logs/route.ts`(動く見本)と `app/api/prechecks/route.ts` に揃えている。
* 仕様の正は `d-watch-api.yaml` の /errors。
*
* ★対象は `robot_error_reports` の全行★(事前チェック・本清掃・予定なし・制御盤・セキュリティ・清掃開始失敗を問わない)。
* `/prechecks`(アラート画面)は事前チェック行に紐づくエラーと出発失敗だけなので、そちらとは別のルートにした。
*
* このルートがやっていること
* 1. クエリパラメータのパース(`parseErrorReportQuery()`。不正なら422)
* 2. ★施設名・分類 → 施設ID の解決★(`resolveFacilityIdsByName()` / `resolveFacilityIdsByMasters()` を
* `intersectFacilityIds()` で AND に畳む。`/clean-logs` と同じ。★`null` = 絞らない / `[]` = 0件★)
* 3. `db.errorReports()` — ★絞り込み・並べ替え・ページ分けはデータ層の中★(2段階で読む。指示書AW D8)。
* ★`errored_at ?? created_at` の順は Prisma のクエリビルダで書けない★ので、ルートで並べ直す必要は無い
* (fixtures 版・Prisma 版とも `filterSortErrorReportKeys()` を通る)
* 4. `{ items, meta }` 形式で返却(`summary` は無い)
*
* ★対応状況の更新(PATCH /errors/{error_id}/status)は `app/api/errors/[error_id]/status/route.ts`★
*/
import { NextResponse } from "next/server";
import { db } from "@/lib/db";
import { parseErrorReportQuery } from "@/lib/error-reports/query";
import { intersectFacilityIds } from "@/lib/facilities/intersect-ids";
import { resolveFacilityIdsByMasters } from "@/lib/facilities/resolve-by-master";
import { resolveFacilityIdsByName } from "@/lib/facilities/resolve-by-name";
import { withRequestId } from "@/lib/logger";
import { logApiError } from "@/lib/logger/api";
import type { ApiError, ErrorReportItem, ListResponse } from "@/lib/types";
const ROUTE = "/api/errors";
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 = parseErrorReportQuery(searchParams);
if (!parsed.ok) {
return errorResponse(parsed.error, parsed.status);
}
const q = parsed.query;
let result: { items: ErrorReportItem[]; total: number };
try {
// ★★2本の解決結果は `intersectFacilityIds()` で AND に畳む★★(`/clean-logs` と同じ。指示書AW D10)—
// ★どちらか片方を捨てないこと★(`??` で書くと片方の条件が静かに消える)。
// ★積集合が空なら `[]`(=0件)★。`null` にすると「絞らない」になり、0件ヒットが全件表示に化ける(罠1)
const [nameIds, masterIds] = await Promise.all([
resolveFacilityIdsByName(q.facilityName),
resolveFacilityIdsByMasters(q),
]);
result = await db.errorReports({
include_resolved: q.includeResolved,
facility_ids: intersectFacilityIds(nameIds, masterIds),
// ★`parseErrorReportQuery()` を通った値を渡す★(空文字は `null` に正規化済み)
date_from: q.dateFrom,
date_to: q.dateTo,
error_type: q.errorType,
page: q.page,
per_page: q.perPage,
});
} 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 body: ListResponse<ErrorReportItem> = {
items: result.items,
meta: { total: result.total, page: q.page, per_page: q.perPage },
};
return NextResponse.json(body);
});
}