Admin AI Tools page: unified design system styling, provider selector with OpenAI turd emoji, free-text model input with exact ID warning
This commit is contained in:
@@ -1,12 +1,169 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { formatDate } from "@/lib/format-date";
|
||||
|
||||
type Props = {
|
||||
brandId: string;
|
||||
// One-color outline icons
|
||||
const Icons = {
|
||||
clipboard: (className: string) => (
|
||||
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<rect x="8" y="2" width="8" height="4" rx="1" ry="1"/>
|
||||
<path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/>
|
||||
<path d="M12 11h4M12 16h4M8 11h.01M8 16h.01"/>
|
||||
</svg>
|
||||
),
|
||||
refresh: (className: string) => (
|
||||
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/>
|
||||
<path d="M21 3v5h-5"/>
|
||||
<path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/>
|
||||
<path d="M8 16H3v5"/>
|
||||
</svg>
|
||||
),
|
||||
package: (className: string) => (
|
||||
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M16.5 9.4 7.55 4.24"/>
|
||||
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/>
|
||||
<polyline points="3.29 7 12 12 20.71 7"/>
|
||||
<line x1="12" x2="12" y1="22" y2="12"/>
|
||||
</svg>
|
||||
),
|
||||
check: (className: string) => (
|
||||
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M20 6 9 17l-5-5"/>
|
||||
</svg>
|
||||
),
|
||||
alert: (className: string) => (
|
||||
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"/>
|
||||
<path d="M12 9v4"/>
|
||||
<path d="M12 17h.01"/>
|
||||
</svg>
|
||||
),
|
||||
clock: (className: string) => (
|
||||
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<polyline points="12 6 12 12 16 14"/>
|
||||
</svg>
|
||||
),
|
||||
scale: (className: string) => (
|
||||
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="m16 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z"/>
|
||||
<path d="m2 16 3-8 3 8c-.87.65-1.92 1-3 1s-2.13-.35-3-1Z"/>
|
||||
<path d="M7 21h10"/>
|
||||
<path d="M12 3v18"/>
|
||||
<path d="M3 7h2c2 0 5-1 7-2 2 1 5 2 7 2h2"/>
|
||||
</svg>
|
||||
),
|
||||
chart: (className: string) => (
|
||||
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<line x1="12" x2="12" y1="20" y2="10"/>
|
||||
<line x1="18" x2="18" y1="20" y2="4"/>
|
||||
<line x1="6" x2="6" y1="20" y2="16"/>
|
||||
</svg>
|
||||
),
|
||||
search: (className: string) => (
|
||||
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<circle cx="11" cy="11" r="8"/>
|
||||
<path d="m21 21-4.3-4.3"/>
|
||||
</svg>
|
||||
),
|
||||
download: (className: string) => (
|
||||
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
||||
<polyline points="7 10 12 15 17 10"/>
|
||||
<line x1="12" x2="12" y1="15" y2="3"/>
|
||||
</svg>
|
||||
),
|
||||
};
|
||||
|
||||
export default function FsmaReportModal({ brandId }: Props) {
|
||||
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<string, { bg: string; text: string; dot: string }> = {
|
||||
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<string, { label: string; bg: string; text: string }> = {
|
||||
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 (
|
||||
<span className={`inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-bold ${cfg.bg} ${cfg.text}`}>
|
||||
<span className={`h-1.5 w-1.5 rounded-full ${cfg.dot}`} />
|
||||
{status === "compliant" ? "Compliant" : status === "non_compliant" ? "Issue" : "Pending"}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function SummaryCard({ icon, label, value, subtext, color }: {
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
value: string | number;
|
||||
subtext?: string;
|
||||
color: "green" | "red" | "amber" | "blue" | "stone";
|
||||
}) {
|
||||
const colors = {
|
||||
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",
|
||||
};
|
||||
return (
|
||||
<div className={`rounded-xl border px-4 py-3 ${colors[color]}`}>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-sm">{icon}</span>
|
||||
<span className="text-[10px] font-bold uppercase tracking-wider opacity-70">{label}</span>
|
||||
</div>
|
||||
<p className="text-2xl font-black mt-0.5">{value}</p>
|
||||
{subtext && <p className="text-[10px] opacity-60 mt-0.5">{subtext}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function FsmaReportModal({ brandId }: { brandId: string }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [startDate, setStartDate] = useState(() => {
|
||||
const d = new Date();
|
||||
@@ -15,77 +172,421 @@ export default function FsmaReportModal({ brandId }: Props) {
|
||||
});
|
||||
const [endDate, setEndDate] = useState(() => new Date().toISOString().split("T")[0]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [data, setData] = useState<ComplianceSnapshot | null>(null);
|
||||
const [search, setSearch] = useState("");
|
||||
const [filterStatus, setFilterStatus] = useState<string>("all");
|
||||
const [showOnlyIssues, setShowOnlyIssues] = useState(false);
|
||||
|
||||
async function handleDownload() {
|
||||
useEffect(() => {
|
||||
if (open && startDate && endDate) {
|
||||
fetchComplianceData();
|
||||
}
|
||||
}, [open, startDate, endDate]);
|
||||
|
||||
async function fetchComplianceData() {
|
||||
setLoading(true);
|
||||
try {
|
||||
const url = `/api/route-trace/fsma-report?brandId=${brandId}&startDate=${startDate}&endDate=${endDate}&format=csv`;
|
||||
window.location.href = url;
|
||||
const res = await fetch(
|
||||
`/api/route-trace/fsma-compliance?brandId=${brandId}&startDate=${startDate}&endDate=${endDate}`
|
||||
);
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
setData(json);
|
||||
} else {
|
||||
setData(null);
|
||||
}
|
||||
} catch {
|
||||
setData(null);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setOpen(false);
|
||||
}
|
||||
}
|
||||
|
||||
function handleDownload() {
|
||||
const url = `/api/route-trace/fsma-report?brandId=${brandId}&startDate=${startDate}&endDate=${endDate}&format=csv`;
|
||||
window.location.href = url;
|
||||
setOpen(false);
|
||||
}
|
||||
|
||||
const filteredLots = data?.lots.filter((lot) => {
|
||||
const matchesSearch =
|
||||
!search ||
|
||||
lot.lot_number.toLowerCase().includes(search.toLowerCase()) ||
|
||||
lot.crop_type.toLowerCase().includes(search.toLowerCase()) ||
|
||||
(lot.field_location ?? "").toLowerCase().includes(search.toLowerCase());
|
||||
const matchesStatus = filterStatus === "all" || lot.status === filterStatus;
|
||||
const matchesIssues = !showOnlyIssues || lot.compliance_status !== "compliant";
|
||||
return matchesSearch && matchesStatus && matchesIssues;
|
||||
}) ?? [];
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
onClick={() => setOpen(true)}
|
||||
className="rounded-xl border border-stone-200 bg-white px-5 py-3 text-sm font-semibold text-stone-600 hover:bg-stone-50 transition-colors"
|
||||
>
|
||||
📋 FSMA Report
|
||||
{Icons.clipboard("h-4 w-4 mr-1.5")} FSMA Report
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40" onClick={() => setOpen(false)}>
|
||||
<div className="w-full max-w-sm rounded-2xl bg-white shadow-xl" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="border-b border-stone-100 px-6 py-4">
|
||||
<h3 className="text-base font-bold text-stone-900">📋 FSMA Compliance Report</h3>
|
||||
<p className="text-xs text-stone-400 mt-0.5">One-up / one-down traceability for a date range</p>
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/30"
|
||||
onClick={(e) => e.target === e.currentTarget && setOpen(false)}
|
||||
style={{ backdropFilter: "blur(4px)" }}
|
||||
>
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
style={{
|
||||
background: "linear-gradient(180deg, rgba(0,0,0,0.3) 0%, rgba(0,0,0,0.2) 100%)",
|
||||
backdropFilter: "blur(60px) saturate(180%)",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Modal card */}
|
||||
<div
|
||||
className="relative w-full max-w-5xl rounded-2xl max-h-[90vh] flex flex-col"
|
||||
style={{
|
||||
background: "rgba(255, 255, 255, 0.92)",
|
||||
backdropFilter: "blur(40px) saturate(200%)",
|
||||
WebkitBackdropFilter: "blur(40px) saturate(200%)",
|
||||
border: "1px solid rgba(255, 255, 255, 0.5)",
|
||||
boxShadow: `
|
||||
0 25px 50px -12px rgba(0, 0, 0, 0.25),
|
||||
0 12px 24px -8px rgba(0, 0, 0, 0.15)
|
||||
`,
|
||||
}}
|
||||
>
|
||||
{/* Top gradient border */}
|
||||
<div
|
||||
className="absolute top-0 left-0 right-0 rounded-t-2xl overflow-hidden"
|
||||
style={{
|
||||
background: "linear-gradient(180deg, rgba(120, 113, 108, 0.1) 0%, transparent 100%)",
|
||||
height: "1px",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Header */}
|
||||
<div
|
||||
className="flex items-center justify-between px-6 py-5"
|
||||
style={{ borderBottom: "1px solid rgba(0, 0, 0, 0.06)" }}
|
||||
>
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-stone-950" style={{ letterSpacing: "-0.02em" }}>
|
||||
<div className="flex items-center gap-2"><span className="text-lg">{Icons.clipboard("h-5 w-5")}</span> FSMA Compliance Snapshot</div>
|
||||
</h2>
|
||||
<p className="mt-1 text-sm text-stone-500">
|
||||
One-up / one-down traceability report
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setOpen(false)}
|
||||
className="flex h-9 w-9 items-center justify-center rounded-full transition-all"
|
||||
style={{ background: "rgba(0, 0, 0, 0.04)", color: "rgba(0, 0, 0, 0.4)" }}
|
||||
>
|
||||
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-6 space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-stone-600 mb-1.5">Start Date</label>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-auto p-6">
|
||||
{/* Date Range Selector */}
|
||||
<div className="flex items-end gap-4 mb-6">
|
||||
<div className="flex-1">
|
||||
<label className="block text-xs font-semibold text-stone-500 mb-1.5 uppercase tracking-wider">
|
||||
Start Date
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
value={startDate}
|
||||
onChange={(e) => setStartDate(e.target.value)}
|
||||
className="w-full rounded-xl border border-stone-200 bg-stone-50 px-3 py-3 text-sm outline-none focus:border-emerald-600"
|
||||
className="w-full rounded-xl border border-stone-200 bg-white px-3 py-2.5 text-sm outline-none focus:border-stone-400"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-stone-600 mb-1.5">End Date</label>
|
||||
<div className="flex-1">
|
||||
<label className="block text-xs font-semibold text-stone-500 mb-1.5 uppercase tracking-wider">
|
||||
End Date
|
||||
</label>
|
||||
<input
|
||||
type="date"
|
||||
value={endDate}
|
||||
onChange={(e) => setEndDate(e.target.value)}
|
||||
className="w-full rounded-xl border border-stone-200 bg-stone-50 px-3 py-3 text-sm outline-none focus:border-emerald-600"
|
||||
className="w-full rounded-xl border border-stone-200 bg-white px-3 py-2.5 text-sm outline-none focus:border-stone-400"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={fetchComplianceData}
|
||||
className="rounded-xl bg-emerald-600 px-4 py-2.5 text-sm font-bold text-white hover:bg-emerald-700 transition-colors"
|
||||
>
|
||||
{Icons.refresh("h-4 w-4 mr-1.5")} Refresh
|
||||
</button>
|
||||
</div>
|
||||
<div className="rounded-xl bg-stone-50 border border-stone-200 px-4 py-3">
|
||||
<p className="text-xs text-stone-500">Includes all lots harvested between the selected dates with full trace events.</p>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-16">
|
||||
<div className="flex items-center gap-3 text-stone-500">
|
||||
<svg className="h-5 w-5 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||
</svg>
|
||||
<span className="text-sm font-medium">Loading compliance data…</span>
|
||||
</div>
|
||||
</div>
|
||||
) : data ? (
|
||||
<>
|
||||
{/* Summary Cards */}
|
||||
<div className="grid grid-cols-3 md:grid-cols-6 gap-3 mb-6">
|
||||
<SummaryCard
|
||||
icon={Icons.package("h-4 w-4")}
|
||||
label="Total Lots"
|
||||
value={data.summary.total_lots}
|
||||
color="stone"
|
||||
/>
|
||||
<SummaryCard
|
||||
icon={Icons.check("h-4 w-4")}
|
||||
label="Compliant"
|
||||
value={data.summary.compliant}
|
||||
subtext={`${data.summary.total_lots > 0 ? Math.round((data.summary.compliant / data.summary.total_lots) * 100) : 0}%`}
|
||||
color="green"
|
||||
/>
|
||||
<SummaryCard
|
||||
icon={Icons.alert("h-4 w-4")}
|
||||
label="Issues"
|
||||
value={data.summary.non_compliant}
|
||||
color="red"
|
||||
/>
|
||||
<SummaryCard
|
||||
icon={Icons.clock("h-4 w-4")}
|
||||
label="Pending"
|
||||
value={data.summary.pending}
|
||||
color="amber"
|
||||
/>
|
||||
<SummaryCard
|
||||
icon={Icons.scale("h-4 w-4")}
|
||||
label="Total Weight"
|
||||
value={data.summary.total_weight > 0 ? `${(data.summary.total_weight / 1000).toFixed(1)}k` : "—"}
|
||||
subtext={data.summary.total_weight > 0 ? "lbs" : ""}
|
||||
color="blue"
|
||||
/>
|
||||
<SummaryCard
|
||||
icon={Icons.chart("h-4 w-4")}
|
||||
label="Crops"
|
||||
value={data.summary.crops.length}
|
||||
color="stone"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="relative flex-1">
|
||||
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-stone-400">{Icons.search("h-4 w-4")}</span>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search lot number, crop, field…"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(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"
|
||||
/>
|
||||
</div>
|
||||
<select
|
||||
value={filterStatus}
|
||||
onChange={(e) => setFilterStatus(e.target.value)}
|
||||
className="rounded-xl border border-stone-200 bg-white px-3 py-2 text-sm outline-none focus:border-stone-400"
|
||||
>
|
||||
<option value="all">All Status</option>
|
||||
<option value="active">Active</option>
|
||||
<option value="in_transit">In Transit</option>
|
||||
<option value="at_shed">At Shed</option>
|
||||
<option value="packed">Packed</option>
|
||||
<option value="delivered">Delivered</option>
|
||||
</select>
|
||||
<label className="flex items-center gap-2 rounded-xl border border-red-200 bg-red-50 px-3 py-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={showOnlyIssues}
|
||||
onChange={(e) => setShowOnlyIssues(e.target.checked)}
|
||||
className="rounded"
|
||||
/>
|
||||
<span className="text-xs font-semibold text-red-700 flex items-center gap-1">{Icons.alert("h-3.5 w-3.5")} Issues only</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<div className="rounded-xl border border-stone-200 bg-white overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="border-b border-stone-100 bg-stone-50/50">
|
||||
<th className="px-4 py-3 text-left text-[10px] font-bold text-stone-500 uppercase tracking-widest">
|
||||
Compliance
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-[10px] font-bold text-stone-500 uppercase tracking-widest">
|
||||
Lot #
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-[10px] font-bold text-stone-500 uppercase tracking-widest">
|
||||
Crop
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-[10px] font-bold text-stone-500 uppercase tracking-widest">
|
||||
Harvest Date
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-[10px] font-bold text-stone-500 uppercase tracking-widest">
|
||||
Field
|
||||
</th>
|
||||
<th className="px-4 py-3 text-right text-[10px] font-bold text-stone-500 uppercase tracking-widest">
|
||||
Weight
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-[10px] font-bold text-stone-500 uppercase tracking-widest">
|
||||
Status
|
||||
</th>
|
||||
<th className="px-4 py-3 text-center text-[10px] font-bold text-stone-500 uppercase tracking-widest">
|
||||
Events
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-[10px] font-bold text-stone-500 uppercase tracking-widest">
|
||||
Issues
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredLots.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={9} className="px-4 py-12 text-center text-sm text-stone-400">
|
||||
{data.summary.total_lots === 0
|
||||
? "No lots found for this date range"
|
||||
: "No lots match your filters"}
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
filteredLots.map((lot) => {
|
||||
const lotStatusCfg = LOT_STATUS_CONFIG[lot.status] ?? LOT_STATUS_CONFIG.active;
|
||||
return (
|
||||
<tr
|
||||
key={lot.lot_id}
|
||||
className="border-b border-stone-50 last:border-0 hover:bg-stone-50/40 transition-colors"
|
||||
>
|
||||
<td className="px-4 py-3">
|
||||
<ComplianceBadge status={lot.compliance_status} />
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className="font-mono text-sm font-bold text-stone-900">
|
||||
{lot.lot_number}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className="text-sm font-semibold text-stone-700">
|
||||
{lot.crop_type}
|
||||
</span>
|
||||
{lot.variety && (
|
||||
<span className="ml-1 text-xs text-stone-400">{lot.variety}</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className="text-sm text-stone-600">
|
||||
{formatDate(lot.harvest_date)}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className="text-sm text-stone-600">
|
||||
{lot.field_location ?? "—"}
|
||||
</span>
|
||||
{lot.field_block && lot.field_block !== "N/A" && (
|
||||
<span className="ml-1 text-xs text-stone-400">
|
||||
· Block {lot.field_block}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right">
|
||||
<span className="text-sm font-semibold text-stone-700">
|
||||
{lot.quantity_lbs
|
||||
? Number(lot.quantity_lbs).toLocaleString()
|
||||
: "—"}
|
||||
</span>
|
||||
<span className="text-[10px] text-stone-400 ml-1">
|
||||
{lot.yield_unit ?? "lbs"}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className={`inline-flex items-center gap-1 rounded-full px-2.5 py-0.5 text-[10px] font-bold ${lotStatusCfg.bg} ${lotStatusCfg.text}`}>
|
||||
<span className="h-1 w-1 rounded-full bg-current" />
|
||||
{lotStatusCfg.label}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-center">
|
||||
<span className={`inline-flex items-center justify-center rounded-full h-6 w-6 text-xs font-bold ${
|
||||
lot.event_count > 0 ? "bg-emerald-50 text-emerald-700" : "bg-stone-100 text-stone-500"
|
||||
}`}>
|
||||
{lot.event_count}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
{lot.issues.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{lot.issues.map((issue, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className="inline-flex items-center rounded bg-red-50 px-1.5 py-0.5 text-[10px] font-medium text-red-600"
|
||||
>
|
||||
{issue}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-xs text-stone-400">—</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Results count */}
|
||||
<p className="mt-3 text-xs text-stone-400 text-center">
|
||||
Showing {filteredLots.length} of {data.summary.total_lots} lots
|
||||
{data.summary.crops.length > 0 && ` • Crops: ${data.summary.crops.join(", ")}`}
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center py-16">
|
||||
<div className="text-stone-300 mb-3">{Icons.clipboard("h-12 w-12")}</div>
|
||||
<p className="text-sm text-stone-500">No compliance data available</p>
|
||||
<p className="text-xs text-stone-400 mt-1">Select a date range and try again</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex justify-end gap-3 border-t border-stone-100 px-6 py-4">
|
||||
<button
|
||||
onClick={() => setOpen(false)}
|
||||
className="rounded-xl border border-stone-200 px-4 py-2 text-sm font-semibold text-stone-600 hover:bg-stone-50"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleDownload}
|
||||
disabled={loading}
|
||||
className="rounded-xl bg-emerald-600 px-5 py-2 text-sm font-bold text-white hover:bg-emerald-700 disabled:opacity-50 flex items-center gap-2"
|
||||
>
|
||||
{loading ? "Preparing..." : "📥 Download CSV"}
|
||||
</button>
|
||||
|
||||
{/* Footer */}
|
||||
<div
|
||||
className="flex items-center justify-between px-6 py-4"
|
||||
style={{ borderTop: "1px solid rgba(0, 0, 0, 0.06)" }}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-stone-400">
|
||||
FSMA Food Safety Modernization Act — Produce Traceability
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={() => setOpen(false)}
|
||||
className="rounded-xl border border-stone-200 px-4 py-2 text-sm font-semibold text-stone-600 hover:bg-stone-50 transition-colors"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
<button
|
||||
onClick={handleDownload}
|
||||
disabled={loading || !data}
|
||||
className="rounded-xl bg-blue-600 px-5 py-2 text-sm font-bold text-white hover:bg-blue-700 disabled:opacity-50 transition-colors flex items-center gap-2"
|
||||
>
|
||||
<span className="flex items-center gap-1.5 text-sm font-semibold">{Icons.download("h-4 w-4")} Download CSV</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user