"use client"; import Link from "next/link"; import { useEffect, useState, useTransition } from "react"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { useI18n } from "@/lib/i18n/useI18n"; import type { RecapDetailResponse, RecapRangeMode, RecapTimelineResponse } from "@/lib/recap/types"; import RecapBanners from "@/components/recap/RecapBanners"; import RecapKpiRow from "@/components/recap/RecapKpiRow"; import RecapProductionBySku from "@/components/recap/RecapProductionBySku"; import RecapDowntimeTop from "@/components/recap/RecapDowntimeTop"; import RecapWorkOrders from "@/components/recap/RecapWorkOrders"; import RecapMachineStatus from "@/components/recap/RecapMachineStatus"; import RecapFullTimeline from "@/components/recap/RecapFullTimeline"; type Props = { machineId: string; initialData: RecapDetailResponse; }; function toInputDate(value: string) { 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 normalizeInputDate(value: string) { const d = new Date(value); if (!Number.isFinite(d.getTime())) return null; return d.toISOString(); } export default function RecapDetailClient({ machineId, initialData }: Props) { const { t, locale } = useI18n(); const router = useRouter(); const pathname = usePathname(); const searchParams = useSearchParams(); const [isPending, startTransition] = useTransition(); const [timeline, setTimeline] = useState(null); const [nowMs, setNowMs] = useState(() => Date.now()); const [customStart, setCustomStart] = useState(toInputDate(initialData.range.start)); const [customEnd, setCustomEnd] = useState(toInputDate(initialData.range.end)); const requestedRange = (searchParams.get("range") as RecapRangeMode | null) ?? initialData.range.requestedMode ?? initialData.range.mode; const selectedRange = requestedRange; const shiftAvailable = initialData.range.shiftAvailable ?? true; const shiftFallbackReason = initialData.range.fallbackReason; const shiftFallbackActive = selectedRange === "shift" && initialData.range.mode !== "shift"; function pushRange(nextRange: RecapRangeMode, start?: string, end?: string) { const params = new URLSearchParams(searchParams.toString()); params.set("range", nextRange); if (nextRange === "custom" && start && end) { params.set("start", start); params.set("end", end); } else { params.delete("start"); params.delete("end"); } startTransition(() => { router.push(`${pathname}?${params.toString()}`); }); } function applyCustomRange() { const start = normalizeInputDate(customStart); const end = normalizeInputDate(customEnd); if (!start || !end || end <= start) return; pushRange("custom", start, end); } const machine = initialData.machine; const generatedAtMs = new Date(initialData.generatedAt).getTime(); const freshAgeSec = Number.isFinite(generatedAtMs) ? Math.max(0, Math.floor((nowMs - generatedAtMs) / 1000)) : null; const timelineStart = timeline?.range.start ?? initialData.range.start; const timelineEnd = timeline?.range.end ?? initialData.range.end; const timelineSegments = timeline?.segments ?? machine.timeline; const timelineHasData = timeline?.hasData ?? true; useEffect(() => { let alive = true; async function loadTimeline() { try { const params = new URLSearchParams({ start: initialData.range.start, end: initialData.range.end, }); const res = await fetch(`/api/recap/${machineId}/timeline?${params.toString()}`, { cache: "no-store" }); const json = await res.json().catch(() => null); if (!alive || !res.ok || !json) return; setTimeline(json as RecapTimelineResponse); } catch { } } void loadTimeline(); return () => { alive = false; }; }, [initialData.range.end, initialData.range.start, machineId]); useEffect(() => { const timer = window.setInterval(() => setNowMs(Date.now()), 1000); return () => window.clearInterval(timer); }, []); return (
{`← ${t("recap.detail.back")}`}

{machine.name || machineId}

{machine.location || t("common.na")}
{freshAgeSec != null ? (
{t("recap.grid.updatedAgo", { sec: freshAgeSec })}
) : null}
{(["24h", "shift", "yesterday", "custom"] as const).map((range) => ( ))}
{!shiftAvailable ? (
{t("recap.range.shiftUnavailable")}
) : null} {shiftFallbackActive ? (
{shiftFallbackReason === "shift-inactive" ? t("recap.range.shiftFallbackInactive") : t("recap.range.shiftFallbackUnavailable")}
) : null} {selectedRange === "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} {isPending ?
{t("common.loading")}
: null}
); }