"use client"; import { useEffect, useMemo, useState } from "react"; import { Bar, BarChart, CartesianGrid, Cell, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis, } from "recharts"; type RangeKey = "24h" | "7d" | "30d" | "custom"; type ReportSummary = { oeeAvg: number | null; availabilityAvg: number | null; performanceAvg: number | null; qualityAvg: number | null; goodTotal: number | null; scrapTotal: number | null; targetTotal: number | null; scrapRate: number | null; topScrapSku?: string | null; topScrapWorkOrder?: string | null; }; type ReportDowntime = { macrostopSec: number; microstopSec: number; slowCycleCount: number; qualitySpikeCount: number; performanceDegradationCount: number; oeeDropCount: number; }; type ReportTrendPoint = { t: string; v: number }; type ReportPayload = { summary: ReportSummary; downtime: ReportDowntime; trend: { oee: ReportTrendPoint[]; availability: ReportTrendPoint[]; performance: ReportTrendPoint[]; quality: ReportTrendPoint[]; scrapRate: ReportTrendPoint[]; }; distribution: { cycleTime: { label: string; count: number; rangeStart?: number; rangeEnd?: number; overflow?: "low" | "high"; minValue?: number; maxValue?: number; }[]; }; insights?: string[]; }; type MachineOption = { id: string; name: string }; type FilterOptions = { workOrders: string[]; skus: string[] }; function fmtPct(v?: number | null) { if (v === null || v === undefined || Number.isNaN(v)) return "--"; return `${v.toFixed(1)}%`; } function fmtNum(v?: number | null) { if (v === null || v === undefined || Number.isNaN(v)) return "--"; return `${Math.round(v)}`; } function fmtDuration(sec?: number | null) { if (!sec) return "--"; const h = Math.floor(sec / 3600); const m = Math.floor((sec % 3600) / 60); if (h > 0) return `${h}h ${m}m`; return `${m}m`; } function downsample(rows: T[], max: number) { if (rows.length <= max) return rows; const step = Math.ceil(rows.length / max); return rows.filter((_, idx) => idx % step === 0); } function formatTickLabel(ts: string, range: RangeKey) { const d = new Date(ts); if (Number.isNaN(d.getTime())) return ts; const hh = String(d.getHours()).padStart(2, "0"); const mm = String(d.getMinutes()).padStart(2, "0"); const month = String(d.getMonth() + 1).padStart(2, "0"); const day = String(d.getDate()).padStart(2, "0"); if (range === "24h") return `${hh}:${mm}`; return `${month}-${day}`; } function CycleTooltip({ active, payload }: any) { if (!active || !payload?.length) return null; const p = payload[0]?.payload; if (!p) return null; let detail = ""; if (p.overflow === "low") { detail = `Below ${p.rangeEnd?.toFixed(1)}s`; } else if (p.overflow === "high") { detail = `Above ${p.rangeStart?.toFixed(1)}s`; } else if (p.rangeStart != null && p.rangeEnd != null) { detail = `${p.rangeStart.toFixed(1)}s - ${p.rangeEnd.toFixed(1)}s`; } const extreme = p.overflow && (p.minValue != null || p.maxValue != null) ? `Extremes: ${p.minValue?.toFixed(1) ?? "--"}s - ${p.maxValue?.toFixed(1) ?? "--"}s` : ""; return (
{p.label}
Cycles: {p.count}
{detail ? (
Range: {detail}
) : null} {extreme ?
{extreme}
: null}
); } function DowntimeTooltip({ active, payload }: any) { if (!active || !payload?.length) return null; const row = payload[0]?.payload ?? {}; const label = row.name ?? payload[0]?.name ?? ""; const value = row.value ?? payload[0]?.value ?? 0; return (
{label}
Downtime: {Number(value)} min
); } function buildCsv(report: ReportPayload) { const rows = new Map>(); const addSeries = (series: ReportTrendPoint[], key: string) => { for (const p of series) { const row = rows.get(p.t) ?? { timestamp: p.t }; row[key] = p.v; rows.set(p.t, row); } }; addSeries(report.trend.oee, "oee"); addSeries(report.trend.availability, "availability"); addSeries(report.trend.performance, "performance"); addSeries(report.trend.quality, "quality"); addSeries(report.trend.scrapRate, "scrapRate"); const ordered = [...rows.values()].sort((a, b) => { const at = new Date(String(a.timestamp)).getTime(); const bt = new Date(String(b.timestamp)).getTime(); return at - bt; }); const header = ["timestamp", "oee", "availability", "performance", "quality", "scrapRate"].join(","); const lines = ordered.map((row) => [ row.timestamp, row.oee ?? "", row.availability ?? "", row.performance ?? "", row.quality ?? "", row.scrapRate ?? "", ] .map((v) => (v == null ? "" : String(v))) .join(",") ); const summary = report.summary; const downtime = report.downtime; const sectionLines: string[] = []; sectionLines.push("section,key,value"); const addRow = (section: string, key: string, value: string | number | null | undefined) => { sectionLines.push( [section, key, value == null ? "" : String(value)] .map((v) => (v.includes(",") ? `"${v.replace(/\"/g, '""')}"` : v)) .join(",") ); }; addRow("summary", "oeeAvg", summary.oeeAvg); addRow("summary", "availabilityAvg", summary.availabilityAvg); addRow("summary", "performanceAvg", summary.performanceAvg); addRow("summary", "qualityAvg", summary.qualityAvg); addRow("summary", "goodTotal", summary.goodTotal); addRow("summary", "scrapTotal", summary.scrapTotal); addRow("summary", "targetTotal", summary.targetTotal); addRow("summary", "scrapRate", summary.scrapRate); addRow("summary", "topScrapSku", summary.topScrapSku ?? ""); addRow("summary", "topScrapWorkOrder", summary.topScrapWorkOrder ?? ""); addRow("loss_drivers", "macrostopSec", downtime.macrostopSec); addRow("loss_drivers", "microstopSec", downtime.microstopSec); addRow("loss_drivers", "slowCycleCount", downtime.slowCycleCount); addRow("loss_drivers", "qualitySpikeCount", downtime.qualitySpikeCount); addRow("loss_drivers", "performanceDegradationCount", downtime.performanceDegradationCount); addRow("loss_drivers", "oeeDropCount", downtime.oeeDropCount); for (const bin of report.distribution.cycleTime) { addRow("cycle_distribution", bin.label, bin.count); } if (report.insights?.length) { report.insights.forEach((note, idx) => addRow("insights", String(idx + 1), note)); } return [header, ...lines, "", ...sectionLines].join("\n"); } function downloadText(filename: string, content: string) { const blob = new Blob([content], { type: "text/csv;charset=utf-8;" }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.setAttribute("download", filename); document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); } function buildPdfHtml( report: ReportPayload, rangeLabel: string, filters: { machine: string; workOrder: string; sku: string } ) { const summary = report.summary; const downtime = report.downtime; const cycleBins = report.distribution.cycleTime; const insights = report.insights ?? []; return ` Report Export

Reports

Range: ${rangeLabel} | Machine: ${filters.machine} | Work Order: ${filters.workOrder} | SKU: ${filters.sku}
OEE (avg)
${summary.oeeAvg != null ? summary.oeeAvg.toFixed(1) + "%" : "--"}
Availability (avg)
${summary.availabilityAvg != null ? summary.availabilityAvg.toFixed(1) + "%" : "--"}
Performance (avg)
${summary.performanceAvg != null ? summary.performanceAvg.toFixed(1) + "%" : "--"}
Quality (avg)
${summary.qualityAvg != null ? summary.qualityAvg.toFixed(1) + "%" : "--"}
Top Loss Drivers
MetricValue
Macrostop (sec)${downtime.macrostopSec}
Microstop (sec)${downtime.microstopSec}
Slow Cycles${downtime.slowCycleCount}
Quality Spikes${downtime.qualitySpikeCount}
Performance Degradation${downtime.performanceDegradationCount}
OEE Drops${downtime.oeeDropCount}
Quality Summary
MetricValue
Scrap Rate${summary.scrapRate != null ? summary.scrapRate.toFixed(1) + "%" : "--"}
Good Total${summary.goodTotal ?? "--"}
Scrap Total${summary.scrapTotal ?? "--"}
Target Total${summary.targetTotal ?? "--"}
Top Scrap SKU${summary.topScrapSku ?? "--"}
Top Scrap Work Order${summary.topScrapWorkOrder ?? "--"}
Cycle Time Distribution
${cycleBins .map((bin) => ``) .join("")}
BinCount
${bin.label}${bin.count}
Notes for Ops
${insights.length ? `
    ${insights.map((n) => `
  • ${n}
  • `).join("")}
` : "
None
"}
`.trim(); } export default function ReportsPage() { const [range, setRange] = useState("24h"); const [report, setReport] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [machines, setMachines] = useState([]); const [filterOptions, setFilterOptions] = useState({ workOrders: [], skus: [] }); const [machineId, setMachineId] = useState(""); const [workOrderId, setWorkOrderId] = useState(""); const [sku, setSku] = useState(""); const rangeLabel = useMemo(() => { if (range === "24h") return "Last 24 hours"; if (range === "7d") return "Last 7 days"; if (range === "30d") return "Last 30 days"; return "Custom range"; }, [range]); useEffect(() => { let alive = true; async function loadMachines() { try { const res = await fetch("/api/machines", { cache: "no-store" }); const json = await res.json(); if (!alive) return; setMachines((json?.machines ?? []).map((m: any) => ({ id: m.id, name: m.name }))); } catch { if (!alive) return; setMachines([]); } } async function load() { setLoading(true); setError(null); try { const params = new URLSearchParams({ range }); if (machineId) params.set("machineId", machineId); if (workOrderId) params.set("workOrderId", workOrderId); if (sku) params.set("sku", sku); const res = await fetch(`/api/reports?${params.toString()}`, { cache: "no-store" }); const json = await res.json(); if (!alive) return; if (!res.ok || json?.ok === false) { setError(json?.error ?? "Failed to load reports"); setReport(null); } else { setReport(json); } } catch { if (!alive) return; setError("Network error"); setReport(null); } finally { if (alive) setLoading(false); } } loadMachines(); load(); return () => { alive = false; }; }, [range, machineId, workOrderId, sku]); useEffect(() => { let alive = true; async function loadFilters() { try { const params = new URLSearchParams({ range }); if (machineId) params.set("machineId", machineId); const res = await fetch(`/api/reports/filters?${params.toString()}`, { cache: "no-store" }); const json = await res.json(); if (!alive) return; if (!res.ok || json?.ok === false) { setFilterOptions({ workOrders: [], skus: [] }); } else { setFilterOptions({ workOrders: json.workOrders ?? [], skus: json.skus ?? [], }); } } catch { if (!alive) return; setFilterOptions({ workOrders: [], skus: [] }); } } loadFilters(); return () => { alive = false; }; }, [range, machineId]); const summary = report?.summary; const downtime = report?.downtime; const trend = report?.trend; const distribution = report?.distribution; const oeeSeries = useMemo(() => { const rows = trend?.oee ?? []; const trimmed = downsample(rows, 600); return trimmed.map((p) => ({ ts: p.t, label: formatTickLabel(p.t, range), value: p.v, })); }, [trend?.oee, range]); const scrapSeries = useMemo(() => { const rows = trend?.scrapRate ?? []; const trimmed = downsample(rows, 600); return trimmed.map((p) => ({ ts: p.t, label: formatTickLabel(p.t, range), value: p.v, })); }, [trend?.scrapRate, range]); const cycleHistogram = useMemo(() => { return distribution?.cycleTime ?? []; }, [distribution?.cycleTime]); const downtimeSeries = useMemo(() => { if (!downtime) return []; return [ { name: "Macrostop", value: Math.round(downtime.macrostopSec / 60) }, { name: "Microstop", value: Math.round(downtime.microstopSec / 60) }, ]; }, [downtime]); const downtimeColors: Record = { Macrostop: "#FF3B5C", Microstop: "#FF7A00", }; const machineLabel = useMemo(() => { if (!machineId) return "All machines"; return machines.find((m) => m.id === machineId)?.name ?? machineId; }, [machineId, machines]); const workOrderLabel = workOrderId || "All work orders"; const skuLabel = sku || "All SKUs"; const handleExportCsv = () => { if (!report) return; const csv = buildCsv(report); downloadText("reports.csv", csv); }; const handleExportPdf = () => { if (!report) return; const html = buildPdfHtml(report, rangeLabel, { machine: machineLabel, workOrder: workOrderLabel, sku: skuLabel, }); const win = window.open("", "_blank", "width=900,height=650"); if (!win) return; win.document.open(); win.document.write(html); win.document.close(); win.focus(); setTimeout(() => win.print(), 300); }; return (

Reports

Trends, downtime, and quality analytics across machines.

Filters
{rangeLabel}
Range
{(["24h", "7d", "30d", "custom"] as RangeKey[]).map((k) => ( ))}
Machine
Work Order
setWorkOrderId(e.target.value)} placeholder="All work orders" className="mt-2 w-full rounded-lg border border-white/10 bg-white/5 px-3 py-2 text-sm text-zinc-300 placeholder:text-zinc-500" /> {filterOptions.workOrders.map((wo) => (
SKU
setSku(e.target.value)} placeholder="All SKUs" className="mt-2 w-full rounded-lg border border-white/10 bg-white/5 px-3 py-2 text-sm text-zinc-300 placeholder:text-zinc-500" /> {filterOptions.skus.map((s) => (
{loading &&
Loading reports...
} {error && !loading && (
{error}
)}
{[ { label: "OEE", value: fmtPct(summary?.oeeAvg), tone: "text-emerald-300" }, { label: "Availability", value: fmtPct(summary?.availabilityAvg), tone: "text-white" }, { label: "Performance", value: fmtPct(summary?.performanceAvg), tone: "text-white" }, { label: "Quality", value: fmtPct(summary?.qualityAvg), tone: "text-white" }, ].map((kpi) => (
{kpi.label} (avg)
{kpi.value}
{summary ? "Computed from KPI snapshots." : "No data in selected range."}
))}
OEE Trend
{oeeSeries.length ? ( { const row = payload?.[0]?.payload; return row?.ts ? new Date(row.ts).toLocaleString() : ""; }} formatter={(val: any) => [`${Number(val).toFixed(1)}%`, "OEE"]} /> ) : (
No trend data yet.
)}
Downtime Pareto
{downtimeSeries.length ? ( } /> {downtimeSeries.map((row, idx) => ( ))} ) : (
No downtime data yet.
)}
Cycle Time Distribution
{cycleHistogram.length ? ( } /> ) : (
No cycle data yet.
)}
Scrap Trend
{scrapSeries.length ? ( { const row = payload?.[0]?.payload; return row?.ts ? new Date(row.ts).toLocaleString() : ""; }} formatter={(val: any) => [`${Number(val).toFixed(1)}%`, "Scrap Rate"]} /> ) : (
No scrap data yet.
)}
Top Loss Drivers
{[ { label: "Macrostop", value: fmtDuration(downtime?.macrostopSec) }, { label: "Microstop", value: fmtDuration(downtime?.microstopSec) }, { label: "Slow Cycle", value: downtime ? `${downtime.slowCycleCount}` : "--" }, { label: "Quality Spike", value: downtime ? `${downtime.qualitySpikeCount}` : "--" }, { label: "OEE Drop", value: downtime ? `${downtime.oeeDropCount}` : "--" }, { label: "Perf Degradation", value: downtime ? `${downtime.performanceDegradationCount}` : "--", }, ].map((row) => (
{row.label} {row.value}
))}
Quality Summary
Scrap Rate
{summary?.scrapRate != null ? fmtPct(summary.scrapRate) : "--"}
Top Scrap SKU
{summary?.topScrapSku ?? "--"}
Top Scrap Work Order
{summary?.topScrapWorkOrder ?? "--"}
Notes for Ops
Suggested actions
{report?.insights && report.insights.length > 0 ? (
{report.insights.map((note, idx) => (
{note}
))}
) : (
No insights yet. Generate reports after data collection.
)}
); }