/**
* GET /schedules — スケジュール一覧(F-09。読み取り専用)
*
* 形は `app/api/ds-devices/route.ts` と同じ。
* 仕様の正は `d-watch-api.yaml` の /schedules。
*
* ★このAPIに summary は無い★(`items` と `meta` のみ。YAML の /schedules 参照)。
* F-09 は「登録済みスケジュールの台帳」を見る画面で、
* 異常件数のような集計を出す画面ではないため。
*
* ★登録・更新は D-Security 側の画面で行うため、D-Watch は GET のみ★(YAML の description)。
* POST/PUT/DELETE をここに足さないこと(CLAUDE.md §3 のとおり、メインDBへの書き込みは
* `robot_error_reports` / `control_error_histories` の4カラムだけが許可されている)。
*/
import { NextResponse } from "next/server";
import { db } from "@/lib/db";
import { filterRowsByFacilityIds } from "@/lib/facilities/filter-by-ids";
import { resolveFacilityIdsByMasters } from "@/lib/facilities/resolve-by-master";
import {
filterSchedules,
paginateSchedules,
parseScheduleQuery,
sortSchedules,
} from "@/lib/schedules/query";
import { withRequestId } from "@/lib/logger";
import { logApiError } from "@/lib/logger/api";
import type { ApiError, ListResponse, Schedule } from "@/lib/types";
const ROUTE = "/api/schedules";
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 = parseScheduleQuery(searchParams);
if (!parsed.ok) {
return errorResponse(parsed.error, parsed.status);
}
let rows: Schedule[];
// ★分類(`group_id` / `customer_id`)→ 施設IDの配列★(指示書AK 段階4)。
// ★`null` = 絞らない / `[]` = 0件★(★罠1★。`?.length` で分岐しないこと)。
let facilityIds: number[] | null;
try {
// ★解決(施設マスタの読み取り)も同じ try に入れる★ — 失敗したら 500。
// ★黙って絞らずに返さないこと★(他社のスケジュールが混ざったまま台帳に出る)。
facilityIds = await resolveFacilityIdsByMasters(parsed.query);
rows = await db.schedules();
} 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` と同じ方針)。
//
// ★`active` では絞り込まない★ 有効・無効の両方を返し、画面が「有効」列で見せ分ける。
// 詳細は `filterSchedules` の JSDoc。
// ★分類は `Schedule.facility_id` で突き合わせる★(指示書AK 段階4)。
// ★なぜ SQL ではなくここで絞るのかは `lib/facilities/filter-by-ids.ts` の冒頭★。
const filtered = filterSchedules(
filterRowsByFacilityIds(rows, facilityIds),
parsed.query
);
const sorted = sortSchedules(filtered);
const { page, perPage } = parsed.query;
// meta.total は「絞り込み後・ページング前」の総件数(ページャの算出元)。
// page が範囲外なら items は空になるが、total は全件数のまま返す。
const total = sorted.length;
const items = paginateSchedules(sorted, page, perPage);
const body: ListResponse<Schedule> = {
items,
meta: { total, page, per_page: perPage },
};
return NextResponse.json(body);
});
}