/**
* ★清掃詳細ログの「予定外タスク」の行を読む★(指示書BG §1・§2-1。ユーザー指示 2026-10-29
* 「エラー一覧以外も、予定外タスクを計算して出すようにしてください」)。
*
* 予定外タスク = `clean_task_reports` のうち `clean_timetable_id` が 0 か NULL の行(TeamWorks 回答 2026-10-27)。
* ★判定の正は `scheduledTimetableId()`(`isUnscheduledReport()`)★ — SQL の `UNSCHEDULED_TIMETABLE_ID_WHERE` はその写しで、
* 取り寄せた行はメモリでも絞り直す(指示書BG §5「`=== null` / `{ not: null }` を直書きしない」)。
*
* | 条件 | 予定のある行(`mainTimetableWhere()`) | 予定外の行(ここ) |
* |---|---|---|
* | 起点 | 予定の行(`precheck = 0`) | 清掃結果の行 |
* | 期間 | 予定の `start_at`(JST) | 清掃結果の `started_at`(JST。★実際に始めた時刻。CLAUDE.md §4.7★) |
* | 施設 | `facility_id IN (...)` | 同じ(清掃結果の `facility_id`) |
* | 削除済みの施設・清掃エリア | 出さない | ★同じく出さない★(`FACILITY_SECTION_VISIBLE_WHERE`。表U1 U1-5) |
* | ロボット(機種・シリアル) | 清掃エリアの有効なロボットの先頭 | ★同じ出し方★(`section.robots`。P1 は1清掃エリア=1ロボット) |
*
* ★事前チェックの回は無い★(予定外タスクには予定の行が無い。事前チェックの4列は null)。
*/
import {
formatModelName,
isUnscheduledReport,
type UnscheduledReportLike,
type UnscheduledRowContext,
} from "@/lib/db/clean-log-mapping";
import { dbDateRange } from "@/lib/db/date-range";
import { UNSCHEDULED_TIMETABLE_ID_WHERE } from "@/lib/db/timetable-link";
import { FACILITY_SECTION_VISIBLE_WHERE } from "@/lib/db/visibility";
import { jstIsoFromDb } from "@/lib/format/date";
import type { Prisma, PrismaClient } from "@/lib/generated/prisma/client";
import type { CleanLogDbQuery } from "@/lib/types";
/**
* ★`clean_task_reports` の `where`(予定外の行だけ)★。★純粋関数★(テストで形を固定する)。
*
* - ★`started_at` で期間を絞る★(`dbDateRange()` を通す。DB の DATETIME は JST の壁時計値。指示書AC C-6)
* - ★`facility_ids` は `!== null` で分岐する★(罠1。`[]` は `in: []` で0件)
* - ★単一ID(`query.id`)は見ない★ — 呼び出し側が単一ID の取得では予定外の行を引かない(予定外の行に `{id}` は無い)
*/
export function unscheduledCleanLogWhere(query: CleanLogDbQuery): Prisma.CleanTaskReportWhereInput {
const range = dbDateRange(query.date_from, query.date_to);
return {
...UNSCHEDULED_TIMETABLE_ID_WHERE,
...FACILITY_SECTION_VISIBLE_WHERE,
...(range.gte || range.lt ? { started_at: range } : {}),
...(query.facility_ids !== null
? { facility_id: { in: query.facility_ids.map((id) => BigInt(id)) } }
: {}),
};
}
/** 読む行の形(`include` の結果のうち使う列だけ。テストから作れるようにしてある) */
export interface UnscheduledReportRow {
report_id: string;
facility_id: bigint;
section_id: bigint;
clean_timetable_id: bigint | null;
status: number;
task_area: number;
clean_area: number;
clean_ratio: number;
started_at: Date;
facility: { facility_name: string };
section: {
robots: ReadonlyArray<{
serial_number: string;
device: { maker_id: number; model_name: string };
}>;
};
}
/** `loadUnscheduledCleanLogRows()` の1件 */
export interface UnscheduledCleanLogRow {
report: UnscheduledReportLike;
context: UnscheduledRowContext;
/** ★有効なロボットの台数のログ(`reportSectionRobotCount()`)に使う★ */
section_id: bigint;
robot_count: number;
}
/**
* 読んだ行 → 清掃詳細ログの材料(★純粋関数★)。
* ★予定のある行(`clean_timetable_id` が 0 でも NULL でもない)は落とす★(SQL の写しが外れていても混ぜない)。
*/
export function unscheduledCleanLogRowsFromReports(
rows: readonly UnscheduledReportRow[]
): UnscheduledCleanLogRow[] {
return rows.filter(isUnscheduledReport).map((row) => {
// ★P1 は1清掃エリア=1ロボット(CLAUDE.md §8)★。予定のある行と同じく、0台なら null、2台以上は `id` 昇順の先頭(指示書BB U2・D7)
const robot = row.section.robots[0] ?? null;
return {
report: {
report_id: row.report_id,
clean_timetable_id: row.clean_timetable_id,
status: row.status,
task_area: row.task_area,
clean_area: row.clean_area,
clean_ratio: row.clean_ratio,
// ★DB の DATETIME は JST の壁時計値★ — 必ず `jstIsoFromDb()` を通す(9時間を足さない・引かない。CLAUDE.md §3)
started_at_iso: jstIsoFromDb(row.started_at),
},
context: {
facility_id: Number(row.facility_id),
facility_name: row.facility.facility_name,
model_name:
robot === null ? null : formatModelName(robot.device.maker_id, robot.device.model_name),
serial_number: robot === null ? null : robot.serial_number,
},
section_id: row.section_id,
robot_count: row.section.robots.length,
};
});
}
/**
* ★予定外タスクの行を読む★(`prismaSource.cleanLogs()` から呼ぶ)。
*
* ★`clean_task_reports` には `facility` / `section` のリレーションがある★(`prisma/schema.prisma` の `CleanTaskReport`)ので
* `include` で辿る(FK が無いのは `clean_timetable_id` の側だけ)。
*/
export async function loadUnscheduledCleanLogRows(
prisma: PrismaClient,
query: CleanLogDbQuery
): Promise<UnscheduledCleanLogRow[]> {
const rows = await prisma.cleanTaskReport.findMany({
where: unscheduledCleanLogWhere(query),
include: {
facility: { select: { facility_name: true } },
section: {
include: {
robots: {
where: { deleted_at: null },
// ★2台以上なら `id` 昇順の先頭を採る(予定のある行と同じ。指示書BB D7)★
orderBy: { id: "asc" },
include: { device: true },
},
},
},
},
orderBy: [{ started_at: "desc" }, { report_id: "asc" }],
});
return unscheduledCleanLogRowsFromReports(rows);
}