import Link from "next/link"; import { getHoursReport, formatMinutes } from "@/lib/reports"; export const dynamic = "force-dynamic"; /** * Plan-vs-actual hours report. * * Two tables: by machine, by operator. Durations come from closed TimeLog * rows in the selected window (default: last 30 days). "Plan" is the sum * of Operation.plannedMinutes for ops that had ANY TimeLog in the window * — scoped the same way as actual so the two columns are apples-to-apples. * Operator plans don't exist in the schema (an op has no planned operator), * so the operator table only shows actuals. */ type RangeKey = "7d" | "30d" | "90d" | "all"; function rangeStart(key: RangeKey): Date { const now = new Date(); if (key === "all") return new Date(0); const days = key === "7d" ? 7 : key === "30d" ? 30 : 90; return new Date(now.getTime() - days * 86_400_000); } function isRange(v: unknown): v is RangeKey { return v === "7d" || v === "30d" || v === "90d" || v === "all"; } export default async function ReportsPage({ searchParams, }: { searchParams: Promise<{ range?: string }>; }) { const { range: rawRange } = await searchParams; const range: RangeKey = isRange(rawRange) ? rawRange : "30d"; const since = rangeStart(range); const report = await getHoursReport(since); const planVarianceClass = (actual: number, planned: number | null) => { if (planned == null) return "text-slate-500"; if (planned === 0) return "text-slate-500"; const ratio = actual / planned; if (ratio > 1.15) return "text-red-600 font-medium"; if (ratio < 0.85) return "text-emerald-600 font-medium"; return "text-slate-700"; }; return (
Plan vs actual based on closed time logs. Longer sessions where an operator paused and resumed count every segment separately.
No machine-attached time in this window.
) : (| Machine | Ops | Planned | Actual | Variance |
|---|---|---|---|---|
| {row.name} | {row.operations} | {row.plannedMinutes == null ? "—" : formatMinutes(row.plannedMinutes)} | {formatMinutes(row.actualMinutes)} | {row.plannedMinutes == null || row.plannedMinutes === 0 ? "—" : `${Math.round( ((row.actualMinutes - row.plannedMinutes) / row.plannedMinutes) * 100, )}%`} |
| Ops without a machine assigned | — | — | {formatMinutes(report.unassignedMachineMinutes)} | — |
Plans are per-operation, not per-operator, so only actual time is shown here.
No operator time in this window.
) : (| Operator | Ops touched | Actual |
|---|---|---|
| {row.name} | {row.operations} | {formatMinutes(row.actualMinutes)} |
{label}
{value}
{hint}