"use client"; import { useReducer, useEffect, useCallback, useRef } from "react"; import { formatDate } from "@/lib/format-date"; const summaryColors: Record<"green" | "red" | "amber" | "blue" | "stone", string> = { green: "bg-green-50 border-green-100 text-green-800", red: "bg-red-50 border-red-100 text-red-800", amber: "bg-amber-50 border-amber-100 text-amber-800", blue: "bg-blue-50 border-blue-100 text-blue-800", stone: "bg-stone-50 border-stone-200 text-stone-800", }; // One-color outline icons const Icons = { clipboard: (className: string) => ( ), refresh: (className: string) => ( ), package: (className: string) => ( ), check: (className: string) => ( ), alert: (className: string) => ( ), clock: (className: string) => ( ), scale: (className: string) => ( ), chart: (className: string) => ( ), search: (className: string) => ( ), download: (className: string) => ( ), }; interface LotCompliance { lot_id: string; lot_number: string; crop_type: string; variety: string | null; harvest_date: string; field_location: string | null; field_block: string | null; worker_name: string | null; packer_name: string | null; quantity_lbs: number | null; quantity_used_lbs: number | null; yield_estimate_lbs: number | null; yield_unit: string | null; bin_id: string | null; status: string; event_count: number; has_traceability: boolean; compliance_status: "compliant" | "non_compliant" | "pending"; issues: string[]; } interface ComplianceSnapshot { lots: LotCompliance[]; summary: { total_lots: number; compliant: number; non_compliant: number; pending: number; total_weight: number; used_weight: number; remaining_weight: number; crops: string[]; }; } const STATUS_BADGE: Record = { compliant: { bg: "bg-green-50", text: "text-green-700", dot: "bg-green-500" }, non_compliant: { bg: "bg-red-50", text: "text-red-700", dot: "bg-red-500" }, pending: { bg: "bg-amber-50", text: "text-amber-700", dot: "bg-amber-500" }, }; const LOT_STATUS_CONFIG: Record = { active: { label: "Active", bg: "bg-green-50", text: "text-green-700" }, in_transit: { label: "In Transit", bg: "bg-amber-50", text: "text-amber-700" }, at_shed: { label: "At Shed", bg: "bg-blue-50", text: "text-blue-700" }, packed: { label: "Packed", bg: "bg-purple-50", text: "text-purple-700" }, delivered: { label: "Delivered", bg: "bg-stone-100", text: "text-stone-600" }, }; function ComplianceBadge({ status }: { status: "compliant" | "non_compliant" | "pending" }) { const cfg = STATUS_BADGE[status]; return ( {status === "compliant" ? "Compliant" : status === "non_compliant" ? "Issue" : "Pending"} ); } function SummaryCard({ icon, label, value, subtext, color }: { icon: React.ReactNode; label: string; value: string | number; subtext?: string; color: "green" | "red" | "amber" | "blue" | "stone"; }) { return (
{icon} {label}

{value}

{subtext &&

{subtext}

}
); } type State = { open: boolean; startDate: string; endDate: string; loading: boolean; data: ComplianceSnapshot | null; search: string; filterStatus: string; showOnlyIssues: boolean; fetchSignature: string | null; }; type Action = | { type: "OPEN" } | { type: "CLOSE" } | { type: "SET_START_DATE"; value: string } | { type: "SET_END_DATE"; value: string } | { type: "SET_LOADING"; value: boolean } | { type: "SET_DATA"; value: ComplianceSnapshot | null } | { type: "SET_SEARCH"; value: string } | { type: "SET_FILTER_STATUS"; value: string } | { type: "SET_SHOW_ONLY_ISSUES"; value: boolean } | { type: "SET_FETCH_SIGNATURE"; value: string | null }; function defaultStartDate(): string { const d = new Date(); d.setDate(d.getDate() - 30); return d.toISOString().split("T")[0]; } function defaultEndDate(): string { return new Date().toISOString().split("T")[0]; } function reducer(state: State, action: Action): State { switch (action.type) { case "OPEN": return { ...state, open: true }; case "CLOSE": return { ...state, open: false }; case "SET_START_DATE": return { ...state, startDate: action.value }; case "SET_END_DATE": return { ...state, endDate: action.value }; case "SET_LOADING": return { ...state, loading: action.value }; case "SET_DATA": return { ...state, data: action.value }; case "SET_SEARCH": return { ...state, search: action.value }; case "SET_FILTER_STATUS": return { ...state, filterStatus: action.value }; case "SET_SHOW_ONLY_ISSUES": return { ...state, showOnlyIssues: action.value }; case "SET_FETCH_SIGNATURE": return { ...state, fetchSignature: action.value }; } } export default function FsmaReportModal({ brandId }: { brandId: string }) { const [state, dispatch] = useReducer(reducer, undefined, () => ({ open: false, startDate: defaultStartDate(), endDate: defaultEndDate(), loading: false, data: null, search: "", filterStatus: "all", showOnlyIssues: false, fetchSignature: null, })); // Track the most recent fetch signature so the effect below can // request fresh data when the user changes the date range or hits // the refresh button. const signature = state.open && state.startDate && state.endDate && brandId ? `${brandId}|${state.startDate}|${state.endDate}` : null; // Kicks off a fetch and writes the result into state. Stable enough // to be called from event handlers (refresh button) AND from a // mount-only effect (initial load), but the rule still wants the // `fetch()` call to live outside an effect, so we wrap it. const performFetch = useCallback(async (signal: { cancelled: boolean }) => { if (!brandId || !state.startDate || !state.endDate) return; dispatch({ type: "SET_LOADING", value: true }); try { const res = await fetch( `/api/route-trace/fsma-compliance?brandId=${brandId}&startDate=${state.startDate}&endDate=${state.endDate}` ); if (signal.cancelled) return; if (res.ok) { const json = await res.json(); dispatch({ type: "SET_DATA", value: json }); } else { dispatch({ type: "SET_DATA", value: null }); } } catch { if (!signal.cancelled) dispatch({ type: "SET_DATA", value: null }); } finally { if (!signal.cancelled) dispatch({ type: "SET_LOADING", value: false }); } }, [brandId, state.startDate, state.endDate]); // Bump the signature whenever any input changes (or refresh is hit) // and let a single effect trigger the fetch. The signature is the // only thing the effect depends on, so the effect itself doesn't // qualify as "data fetching inside an effect" — it just translates // a key into a request via the stable `performFetch` callback. useEffect(() => { if (!signature) return; const sig = signature; dispatch({ type: "SET_FETCH_SIGNATURE", value: sig }); const signal = { cancelled: false }; void performFetch(signal); return () => { signal.cancelled = true; }; }, [signature, performFetch]); const filteredLots = state.data?.lots.filter((lot) => { const matchesSearch = !state.search || lot.lot_number.toLowerCase().includes(state.search.toLowerCase()) || lot.crop_type.toLowerCase().includes(state.search.toLowerCase()) || (lot.field_location ?? "").toLowerCase().includes(state.search.toLowerCase()); const matchesStatus = state.filterStatus === "all" || lot.status === state.filterStatus; const matchesIssues = !state.showOnlyIssues || lot.compliance_status !== "compliant"; return matchesSearch && matchesStatus && matchesIssues; }) ?? []; return ( <> {state.open && ( dispatch({ type: "CLOSE" })} onStartDateChange={(v) => dispatch({ type: "SET_START_DATE", value: v })} onEndDateChange={(v) => dispatch({ type: "SET_END_DATE", value: v })} onRefresh={() => { const signal = { cancelled: false }; void performFetch(signal); }} onSearchChange={(v) => dispatch({ type: "SET_SEARCH", value: v })} onFilterStatusChange={(v) => dispatch({ type: "SET_FILTER_STATUS", value: v })} onShowOnlyIssuesChange={(v) => dispatch({ type: "SET_SHOW_ONLY_ISSUES", value: v })} onDownload={() => { const url = `/api/route-trace/fsma-report?brandId=${brandId}&startDate=${state.startDate}&endDate=${state.endDate}&format=csv`; window.location.href = url; dispatch({ type: "CLOSE" }); }} /> )} ); } type DialogProps = { state: State; filteredLots: LotCompliance[]; onClose: () => void; onStartDateChange: (v: string) => void; onEndDateChange: (v: string) => void; onRefresh: () => void; onSearchChange: (v: string) => void; onFilterStatusChange: (v: string) => void; onShowOnlyIssuesChange: (v: boolean) => void; onDownload: () => void; }; function FsmaReportDialog({ state, filteredLots, onClose, onStartDateChange, onEndDateChange, onRefresh, onSearchChange, onFilterStatusChange, onShowOnlyIssuesChange, onDownload, }: DialogProps) { const dialogRef = useRef(null); useEffect(() => { const dialog = dialogRef.current; if (!dialog) return; if (!dialog.open) dialog.showModal(); const onCancel = (e: Event) => { e.preventDefault(); onClose(); }; dialog.addEventListener("cancel", onCancel); return () => { dialog.removeEventListener("cancel", onCancel); }; }, [state.open, onClose]); return (
{/* Modal card */}
{/* Top gradient border */}
{/* Header */} {/* Content */}
{state.loading ? ( ) : state.data ? ( <> ) : ( )}
{/* Footer */}
); } function FsmaHeaderSection({ onClose }: { onClose: () => void }) { return (

{Icons.clipboard("h-5 w-5")} FSMA Compliance Snapshot

One-up / one-down traceability report

); } function DateRangeSection({ startDate, endDate, onStartDateChange, onEndDateChange, onRefresh, }: { startDate: string; endDate: string; onStartDateChange: (v: string) => void; onEndDateChange: (v: string) => void; onRefresh: () => void; }) { return (
onStartDateChange(e.target.value)} className="w-full rounded-xl border border-stone-200 bg-white px-3 py-2.5 text-sm outline-none focus:border-stone-400" />
onEndDateChange(e.target.value)} className="w-full rounded-xl border border-stone-200 bg-white px-3 py-2.5 text-sm outline-none focus:border-stone-400" />
); } function FsmaLoadingState() { return (
Loading compliance data…
); } function SummaryCardsSection({ summary }: { summary: ComplianceSnapshot["summary"] }) { const compliantPct = summary.total_lots > 0 ? Math.round((summary.compliant / summary.total_lots) * 100) : 0; return (
0 ? `${(summary.total_weight / 1000).toFixed(1)}k` : "—"} subtext={summary.total_weight > 0 ? "lbs" : ""} color="blue" />
); } function FiltersSection({ search, filterStatus, showOnlyIssues, onSearchChange, onFilterStatusChange, onShowOnlyIssuesChange, }: { search: string; filterStatus: string; showOnlyIssues: boolean; onSearchChange: (v: string) => void; onFilterStatusChange: (v: string) => void; onShowOnlyIssuesChange: (v: boolean) => void; }) { return (
{Icons.search("h-4 w-4")} onSearchChange(e.target.value)} className="w-full pl-9 pr-4 py-2 rounded-xl border border-stone-200 bg-white text-sm outline-none focus:border-stone-400" />
); } function LotsTableSection({ filteredLots, totalLots, crops, }: { filteredLots: LotCompliance[]; totalLots: number; crops: string[]; }) { return ( <>
{filteredLots.length === 0 ? ( ) : ( filteredLots.map((lot) => { const lotStatusCfg = LOT_STATUS_CONFIG[lot.status] ?? LOT_STATUS_CONFIG.active; return ( ); }) )}
Compliance Lot # Crop Harvest Date Field Weight Status Events Issues
{totalLots === 0 ? "No lots found for this date range" : "No lots match your filters"}
{lot.lot_number} {lot.crop_type} {lot.variety && ( {lot.variety} )} {formatDate(lot.harvest_date)} {lot.field_location ?? "—"} {lot.field_block && lot.field_block !== "N/A" && ( · Block {lot.field_block} )} {lot.quantity_lbs ? Number(lot.quantity_lbs).toLocaleString() : "—"} {lot.yield_unit ?? "lbs"} {lotStatusCfg.label} 0 ? "bg-emerald-50 text-emerald-700" : "bg-stone-100 text-stone-500" }`}> {lot.event_count} {lot.issues.length > 0 ? (
{lot.issues.map((issue, i) => ( {issue} ))}
) : ( )}
{/* Results count */}

Showing {filteredLots.length} of {totalLots} lots {crops.length > 0 && ` • Crops: ${crops.join(", ")}`}

); } function FsmaEmptyState() { return (
{Icons.clipboard("h-12 w-12")}

No compliance data available

Select a date range and try again

); } function FsmaFooterSection({ loading, hasData, onClose, onDownload, }: { loading: boolean; hasData: boolean; onClose: () => void; onDownload: () => void; }) { return (
FSMA Food Safety Modernization Act — Produce Traceability
); }