"use client"; import { useEffect, useMemo, useState } from "react"; import { useI18n } from "@/lib/i18n/useI18n"; import type { RecapMachine, RecapResponse } 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"; 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); 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]); 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 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")} {selectedMachine?.workOrders.active?.startedAt ? new Date(selectedMachine.workOrders.active.startedAt).toLocaleTimeString(locale, { hour: "2-digit", minute: "2-digit" }) : "--:--"}
) : null} {bannerStop ? (
{t("recap.banner.stopped", { minutes: toMinutesLabel(selectedMachine?.downtime.ongoingStopMin ?? null) })}
) : null} {loading ?
{t("common.loading")}
: null}
); }