"use client"; import { useEffect, useMemo, useState } from "react"; import { useI18n } from "@/lib/i18n/useI18n"; import type { RecapMachine, RecapResponse, RecapTimelineResponse } from "@/lib/recap/types"; import RecapKpiRow from "@/components/recap/RecapKpiRow"; import RecapProductionBySku from "@/components/recap/RecapProductionBySku"; import RecapDowntimeTop from "@/components/recap/RecapDowntimeTop"; import RecapWorkOrderStatus from "@/components/recap/RecapWorkOrderStatus"; import RecapMachineStatus from "@/components/recap/RecapMachineStatus"; import RecapTimeline from "@/components/recap/RecapTimeline"; type Props = { initialData: RecapResponse; initialFilters: { machineId: string; shift: string; start: string; end: string; }; }; type RangeMode = "24h" | "shift" | "custom"; function toInputDate(value: string) { if (!value) return ""; const d = new Date(value); const pad = (n: number) => String(n).padStart(2, "0"); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`; } function toMinutesLabel(minutes: number | null) { if (minutes == null || minutes <= 0) return "0"; return String(Math.round(minutes)); } export default function RecapClient({ initialData, initialFilters }: Props) { const { t, locale } = useI18n(); const [data, setData] = useState(initialData); const [machineId, setMachineId] = useState(initialFilters.machineId || ""); const [shift, setShift] = useState(initialFilters.shift || "shift1"); const [customStart, setCustomStart] = useState(toInputDate(initialFilters.start)); const [customEnd, setCustomEnd] = useState(toInputDate(initialFilters.end)); const [mode, setMode] = useState(() => { if (initialFilters.shift) return "shift"; if (initialFilters.start || initialFilters.end) return "custom"; return "24h"; }); const [loading, setLoading] = useState(false); const [timeline, setTimeline] = useState(null); const shiftOptions = useMemo( () => data.availableShifts?.length ? data.availableShifts : [ { id: "shift1", name: t("recap.shift.1") }, { id: "shift2", name: t("recap.shift.2") }, { id: "shift3", name: t("recap.shift.3") }, ], [data.availableShifts, t] ); useEffect(() => { if (mode !== "shift") return; if (shiftOptions.some((option) => option.id === shift)) return; setShift(shiftOptions[0]?.id ?? "shift1"); }, [mode, shift, shiftOptions]); useEffect(() => { let alive = true; async function load() { setLoading(true); const qs = new URLSearchParams(); if (machineId) qs.set("machineId", machineId); if (mode === "shift") qs.set("shift", shift || "shift1"); if (mode === "custom") { if (customStart) qs.set("start", new Date(customStart).toISOString()); if (customEnd) qs.set("end", new Date(customEnd).toISOString()); } try { const res = await fetch(`/api/recap?${qs.toString()}`, { cache: "no-cache" }); const json = await res.json().catch(() => null); if (!alive || !json) return; setData(json as RecapResponse); } finally { if (alive) setLoading(false); } } const timeout = setTimeout(load, 200); return () => { alive = false; clearTimeout(timeout); }; }, [machineId, mode, shift, customStart, customEnd]); useEffect(() => { async function refresh() { const qs = new URLSearchParams(); if (machineId) qs.set("machineId", machineId); if (mode === "shift") qs.set("shift", shift || "shift1"); if (mode === "custom") { if (customStart) qs.set("start", new Date(customStart).toISOString()); if (customEnd) qs.set("end", new Date(customEnd).toISOString()); } const res = await fetch(`/api/recap?${qs.toString()}`, { cache: "no-cache" }); const json = await res.json().catch(() => null); if (json) setData(json as RecapResponse); } const onFocus = () => { void refresh(); }; const interval = window.setInterval(onFocus, 60000); window.addEventListener("focus", onFocus); return () => { window.clearInterval(interval); window.removeEventListener("focus", onFocus); }; }, [machineId, mode, shift, customStart, customEnd]); const selectedMachine = useMemo(() => { if (!data.machines.length) return null; return data.machines.find((m) => m.machineId === machineId) ?? data.machines[0]; }, [data.machines, machineId]); useEffect(() => { let alive = true; async function loadTimeline() { if (mode !== "24h") { if (alive) setTimeline(null); return; } if (!selectedMachine?.machineId) { if (alive) setTimeline(null); return; } const qs = new URLSearchParams({ machineId: selectedMachine.machineId, hours: "24", start: data.range.start, end: data.range.end, }); const res = await fetch(`/api/recap/timeline?${qs.toString()}`, { cache: "no-cache" }); const json = await res.json().catch(() => null); if (!alive) return; if (res.ok && json && json.segments) { setTimeline(json as RecapTimelineResponse); } else { setTimeline(null); } } void loadTimeline(); return () => { alive = false; }; }, [mode, selectedMachine?.machineId, data.range.start, data.range.end]); const fleet = useMemo(() => { let good = 0; let scrap = 0; let stops = 0; let oeeSum = 0; let oeeCount = 0; for (const m of data.machines) { good += m.production.goodParts; scrap += m.production.scrapParts; stops += m.downtime.stopsCount; if (m.oee.avg != null) { oeeSum += m.oee.avg; oeeCount += 1; } } return { oeeAvg: oeeCount ? oeeSum / oeeCount : null, good, scrap, stops, }; }, [data.machines]); const bannerMold = selectedMachine?.workOrders.moldChangeInProgress; const moldStartMs = selectedMachine?.workOrders.moldChangeStartMs ?? null; const moldStartLabel = moldStartMs ? new Date(moldStartMs).toLocaleTimeString(locale, { hour: "2-digit", minute: "2-digit" }) : "--:--"; const moldElapsedMin = moldStartMs ? Math.max(0, Math.floor((Date.now() - moldStartMs) / 60000)) : null; const bannerStop = (selectedMachine?.downtime.ongoingStopMin ?? 0) > 0; return (

{t("recap.title")}

{t("recap.subtitle")} · {new Date(data.range.start).toLocaleString(locale)} - {new Date(data.range.end).toLocaleString(locale)}

{mode === "shift" ? ( ) : null} {mode === "custom" ? ( <> setCustomStart(event.target.value)} className="rounded-xl border border-white/10 bg-black/40 px-3 py-2 text-zinc-200" /> setCustomEnd(event.target.value)} className="rounded-xl border border-white/10 bg-black/40 px-3 py-2 text-zinc-200" /> ) : null}
{bannerMold ? (
{t("recap.banner.mold")} {moldStartLabel} {moldElapsedMin != null ? ` · ${moldElapsedMin} min` : ""}
) : null} {bannerStop ? (
{t("recap.banner.stopped", { minutes: toMinutesLabel(selectedMachine?.downtime.ongoingStopMin ?? null) })}
) : null} {loading ?
{t("common.loading")}
: null} {timeline ? ( ) : null}
); }