Files
MIS-Contro-Tower/app/api/analytics/coverage/route.ts
2026-01-21 01:45:57 +00:00

65 lines
2.0 KiB
TypeScript

import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { requireSession } from "@/lib/auth/requireSession";
import { coerceDowntimeRange, rangeToStart } from "@/lib/analytics/downtimeRange";
const bad = (status: number, error: string) =>
NextResponse.json({ ok: false, error }, { status });
export async function GET(req: Request) {
const session = await requireSession();
if (!session) return bad(401, "Unauthorized");
const orgId = session.orgId;
const url = new URL(req.url);
// ✅ Parse params INSIDE handler
const range = coerceDowntimeRange(url.searchParams.get("range"));
const start = rangeToStart(range);
const machineId = url.searchParams.get("machineId"); // optional
const kind = (url.searchParams.get("kind") || "downtime").toLowerCase();
// coverage is only meaningful for downtime
if (kind !== "downtime") return bad(400, "Invalid kind (downtime only)");
let resolvedMachineId: string | null = null;
// If machineId provided, validate ownership
if (machineId) {
const m = await prisma.machine.findFirst({
where: { id: machineId, orgId },
select: { id: true },
});
if (!m) return bad(404, "Machine not found");
resolvedMachineId = m.id;
}
const rows = await prisma.reasonEntry.findMany({
where: {
orgId,
...(resolvedMachineId ? { machineId: resolvedMachineId } : {}),
kind: "downtime",
capturedAt: { gte: start },
},
select: { durationSeconds: true, episodeId: true },
});
const receivedEpisodes = new Set(rows.map((r) => r.episodeId).filter(Boolean)).size;
const receivedMinutes =
Math.round((rows.reduce((acc, r) => acc + (r.durationSeconds ?? 0), 0) / 60) * 10) / 10;
return NextResponse.json({
ok: true,
orgId,
machineId: resolvedMachineId, // null => org-wide
range,
start,
receivedEpisodes,
receivedMinutes,
note:
"Control Tower received coverage (sync health). True coverage vs total downtime minutes can be added once CT has total downtime minutes per window.",
});
}