"use client"; import { useEffect, useMemo, useRef, useState } from "react"; import { useI18n } from "@/lib/i18n/useI18n"; type MachineRow = { id: string; name: string; location?: string | null; }; type ShiftRow = { name: string; enabled: boolean; }; type AlertEvent = { id: string; ts: string; eventType: string; severity: string; title: string; description?: string | null; machineId: string; machineName?: string | null; location?: string | null; workOrderId?: string | null; sku?: string | null; durationSec?: number | null; status?: string | null; shift?: string | null; alertId?: string | null; isUpdate?: boolean; isAutoAck?: boolean; }; const RANGE_OPTIONS = [ { value: "24h", labelKey: "alerts.inbox.range.24h" }, { value: "7d", labelKey: "alerts.inbox.range.7d" }, { value: "30d", labelKey: "alerts.inbox.range.30d" }, { value: "custom", labelKey: "alerts.inbox.range.custom" }, ] as const; function formatDuration(seconds: number | null | undefined, t: (key: string) => string) { if (seconds == null || !Number.isFinite(seconds)) return t("alerts.inbox.duration.na"); if (seconds < 60) return `${Math.round(seconds)}${t("alerts.inbox.duration.sec")}`; if (seconds < 3600) return `${Math.round(seconds / 60)}${t("alerts.inbox.duration.min")}`; return `${(seconds / 3600).toFixed(1)}${t("alerts.inbox.duration.hr")}`; } function normalizeLabel(value?: string | null) { if (!value) return ""; return String(value).trim(); } export default function AlertsClient({ initialMachines = [], initialShifts = [], initialEvents = [], }: { initialMachines?: MachineRow[]; initialShifts?: ShiftRow[]; initialEvents?: AlertEvent[]; }) { const { t, locale } = useI18n(); const [events, setEvents] = useState(() => initialEvents); const [machines, setMachines] = useState(() => initialMachines); const [shifts, setShifts] = useState(() => initialShifts); const [loading, setLoading] = useState(() => initialMachines.length === 0 || initialShifts.length === 0); const [loadingEvents, setLoadingEvents] = useState(false); const [error, setError] = useState(null); const [range, setRange] = useState("24h"); const [start, setStart] = useState(""); const [end, setEnd] = useState(""); const [machineId, setMachineId] = useState(""); const [location, setLocation] = useState(""); const [shift, setShift] = useState(""); const [eventType, setEventType] = useState(""); const [severity, setSeverity] = useState(""); const [status, setStatus] = useState(""); const [includeUpdates, setIncludeUpdates] = useState(false); const [search, setSearch] = useState(""); const skipInitialEventsRef = useRef(true); const locations = useMemo(() => { const seen = new Set(); for (const machine of machines) { if (!machine.location) continue; seen.add(machine.location); } return Array.from(seen).sort(); }, [machines]); useEffect(() => { if (initialMachines.length && initialShifts.length) { setLoading(false); return; } let alive = true; async function loadFilters() { setLoading(true); try { const [machinesRes, settingsRes] = await Promise.all([ fetch("/api/machines", { cache: "no-store" }), fetch("/api/settings", { cache: "no-store" }), ]); const machinesJson = await machinesRes.json().catch(() => ({})); const settingsJson = await settingsRes.json().catch(() => ({})); if (!alive) return; setMachines(machinesJson.machines ?? []); const shiftRows = settingsJson?.settings?.shiftSchedule?.shifts ?? []; setShifts( Array.isArray(shiftRows) ? shiftRows .map((row: unknown) => { const data = row && typeof row === "object" ? (row as Record) : {}; const name = typeof data.name === "string" ? data.name : ""; const enabled = data.enabled !== false; return { name, enabled }; }) .filter((row) => row.name) : [] ); } catch { if (!alive) return; } finally { if (alive) setLoading(false); } } loadFilters(); return () => { alive = false; }; }, [initialMachines, initialShifts]); useEffect(() => { let alive = true; const controller = new AbortController(); async function loadEvents() { const isDefault = range === "24h" && !start && !end && !machineId && !location && !shift && !eventType && !severity && !status && !includeUpdates; if (skipInitialEventsRef.current) { skipInitialEventsRef.current = false; if (initialEvents.length && isDefault) return; } setLoadingEvents(true); setError(null); const params = new URLSearchParams(); params.set("range", range); if (range === "custom") { if (start) params.set("start", start); if (end) params.set("end", end); } if (machineId) params.set("machineId", machineId); if (location) params.set("location", location); if (shift) params.set("shift", shift); if (eventType) params.set("eventType", eventType); if (severity) params.set("severity", severity); if (status) params.set("status", status); if (includeUpdates) params.set("includeUpdates", "1"); params.set("limit", "250"); try { const res = await fetch(`/api/alerts/inbox?${params.toString()}`, { cache: "no-store", signal: controller.signal, }); const json = await res.json().catch(() => ({})); if (!alive) return; if (!res.ok || !json?.ok) { setError(json?.error || t("alerts.inbox.error")); setEvents([]); } else { setEvents(json.events ?? []); } } catch { if (alive) { setError(t("alerts.inbox.error")); setEvents([]); } } finally { if (alive) setLoadingEvents(false); } } loadEvents(); return () => { alive = false; controller.abort(); }; }, [ range, start, end, machineId, location, shift, eventType, severity, status, includeUpdates, t, initialEvents.length, ]); const eventTypes = useMemo(() => { const seen = new Set(); for (const ev of events) { if (ev.eventType) seen.add(ev.eventType); } return Array.from(seen).sort(); }, [events]); const severities = useMemo(() => { const seen = new Set(); for (const ev of events) { if (ev.severity) seen.add(ev.severity); } return Array.from(seen).sort(); }, [events]); const statuses = useMemo(() => { const seen = new Set(); for (const ev of events) { if (ev.status) seen.add(ev.status); } return Array.from(seen).sort(); }, [events]); const filteredEvents = useMemo(() => { if (!search.trim()) return events; const needle = search.trim().toLowerCase(); return events.filter((ev) => { return ( normalizeLabel(ev.title).toLowerCase().includes(needle) || normalizeLabel(ev.description).toLowerCase().includes(needle) || normalizeLabel(ev.machineName).toLowerCase().includes(needle) || normalizeLabel(ev.location).toLowerCase().includes(needle) || normalizeLabel(ev.eventType).toLowerCase().includes(needle) ); }); }, [events, search]); function formatEventTypeLabel(value: string) { const key = `alerts.event.${value}`; const label = t(key); return label === key ? value : label; } function formatStatusLabel(value?: string | null) { if (!value) return t("alerts.inbox.table.unknown"); const key = `alerts.inbox.status.${value}`; const label = t(key); return label === key ? value : label; } return (

{t("alerts.title")}

{t("alerts.subtitle")}

{t("alerts.inbox.filters.title")}
{loading &&
{t("alerts.inbox.loadingFilters")}
}
{range === "custom" && ( <> )}
{t("alerts.inbox.title")}
{loadingEvents &&
{t("alerts.inbox.loading")}
}
{error && (
{error}
)} {!loadingEvents && !filteredEvents.length && (
{t("alerts.inbox.empty")}
)} {!!filteredEvents.length && (
{filteredEvents.map((ev) => ( ))}
{t("alerts.inbox.table.time")} {t("alerts.inbox.table.machine")} {t("alerts.inbox.table.site")} {t("alerts.inbox.table.shift")} {t("alerts.inbox.table.type")} {t("alerts.inbox.table.severity")} {t("alerts.inbox.table.status")} {t("alerts.inbox.table.duration")} {t("alerts.inbox.table.title")}
{new Date(ev.ts).toLocaleString(locale)} {ev.machineName || t("alerts.inbox.table.unknown")} {ev.location || t("alerts.inbox.table.unknown")} {ev.shift || t("alerts.inbox.table.unknown")} {formatEventTypeLabel(ev.eventType)} {ev.severity || t("alerts.inbox.table.unknown")} {formatStatusLabel(ev.status)} {formatDuration(ev.durationSec, t)}
{ev.title}
{ev.description && (
{ev.description}
)} {(ev.workOrderId || ev.sku) && (
{ev.workOrderId ? `${t("alerts.inbox.meta.workOrder")}: ${ev.workOrderId}` : null} {ev.workOrderId && ev.sku ? " • " : null} {ev.sku ? `${t("alerts.inbox.meta.sku")}: ${ev.sku}` : null}
)}
)}
); }