Files
Mohamed Mathar Irfan ed6610d5d8 Initial project upload
2026-07-28 17:57:02 +05:30

889 lines
30 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useEffect, useMemo, useState } from "react";
import {
Package, CheckCircle2, Clock, AlertTriangle, Calendar, Search,
Plus, Download, Eye, Pencil, Upload, MoreVertical, X,Trash2,
} from "lucide-react";
import { api, apiBase } from "@/lib/api";
import ComplianceDetailModal from "@/components/ComplianceDetailModal";
import jsPDF from "jspdf";
import autoTable from "jspdf-autotable";
function imgSrc(url?: string) {
if (!url) return "";
if (url.startsWith("data:") || url.startsWith("http")) return url;
return `${apiBase}${url}`;
}
const TYPES = ["FSSAI License", "Drug License", "GMP Certificate", "ISO Certificate",
"EPR Registration", "Factory License", "Import Export Code", "Trademark Registration"];
const STATUS: Record<string, string> = {
complete: "bg-emerald-50 text-emerald-700",
pending: "bg-amber-50 text-amber-700",
expiring: "bg-amber-50 text-amber-700",
expired: "bg-rose-50 text-rose-700",
};
export default function Compliance() {
const [rows, setRows] = useState<any[]>([]);
const [s, setS] = useState<any>(null);
const [products, setProducts] = useState<any[]>([]);
const [filters, setFilters] = useState({ search: "", status_filter: "", compliance_type: "", country: "" });
const [detailId, setDetailId] = useState<string | null>(null);
const [menuFor, setMenuFor] = useState<string | null>(null);
const [form, setForm] = useState<any>(null); // add/edit form state (null = closed)
const [page, setPage] = useState(1);
const pageSize = 25;
const [showAlerts, setShowAlerts] = useState(true);
const [deleteRow, setDeleteRow] = useState<any>(null);
const [deleting, setDeleting] = useState(false);
const [selectedRows, setSelectedRows] = useState<string[]>([]);
const [bulkDeleteOpen, setBulkDeleteOpen] = useState(false);
function load() {
const qs = new URLSearchParams(
Object.entries(filters).filter(([, v]) => v) as any
).toString();
api(`/compliance${qs ? "?" + qs : ""}`)
.then(setRows)
.catch(() => setRows([]));
api("/compliance/summary")
.then((data) => {
setS(data);
setShowAlerts(true);
})
.catch(() => {});
}
useEffect(() => { load(); }, [filters]);
useEffect(() => { api("/products").then((d) => setProducts(d.items ?? [])).catch(() => {}); }, []);
const paged = useMemo(() => rows.slice((page - 1) * pageSize, page * pageSize), [rows, page]);
const totalPages = Math.max(1, Math.ceil(rows.length / pageSize));
const set = (k: string, v: string) => { setPage(1); setFilters((f) => ({ ...f, [k]: v })); };
function handleExport() {
const exportRows =
selectedRows.length > 0
? rows.filter((r) => selectedRows.includes(r.id))
: rows;
const headers = [
"Product",
"SKU",
"Compliance Type",
"License Number",
"License Name",
"Issuing Authority",
"Issue Date",
"Expiry Date",
"Days Left",
"Status",
"Country",
"State",
"Category",
"Applicable To",
"Manufacturing Plant",
"Remarks",
"Created By",
"Created On",
"Last Updated",
"Document URL",
];
const csv = [
headers.join(","),
...exportRows.map((r) =>
[
`"${r.product_name ?? ""}"`,
`"${r.sku ?? ""}"`,
`"${r.compliance_type ?? ""}"`,
`"${r.license_number ?? ""}"`,
`"${r.license_name ?? ""}"`,
`"${r.issuing_authority ?? ""}"`,
`"${r.issue_date ?? ""}"`,
`"${r.expiry_date ?? ""}"`,
`"${r.days_left == null
? "-"
: r.days_left < 0
? "Expired"
: r.days_left === 0
? "Today"
: `${r.days_left} Days`
}"`,
`"${r.status ?? ""}"`,
`"${r.country ?? ""}"`,
`"${r.state ?? ""}"`,
`"${r.category ?? ""}"`,
`"${r.applicable_to ?? ""}"`,
`"${r.plant ?? ""}"`,
`"${r.remarks ?? ""}"`,
`"${r.created_by ?? ""}"`,
`"${r.created_at ? new Date(r.created_at).toLocaleString("en-IN") : ""}"`,
`"${r.updated_at ? new Date(r.updated_at).toLocaleString("en-IN") : ""}"`,
`"${r.document_url ?? ""}"`,
].join(",")
),
].join("\n");
const blob = new Blob([csv], {
type: "text/csv;charset=utf-8;",
});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download =
selectedRows.length > 0
? `selected_compliance_${selectedRows.length}.csv`
: `compliance_${new Date().toISOString().slice(0, 10)}.csv`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
async function handleImport(
e: React.ChangeEvent<HTMLInputElement>
) {
const file = e.target.files?.[0];
if (!file) return;
const formData = new FormData();
formData.append("file", file);
try {
await fetch(`${apiBase}/compliance/import`, {
method: "POST",
body: formData,
headers: {
Authorization: `Bearer ${localStorage.getItem("vp_token")}`,
},
});
load();
alert("Import successful");
} catch {
alert("Import failed");
}
e.target.value = "";
}
async function handlePdfExport() {
const exportRows =
selectedRows.length > 0
? rows.filter((r) => selectedRows.includes(r.id))
: rows;
const doc = new jsPDF({
orientation: "landscape",
unit: "mm",
format: "a4",
});
// ===========================
// Logo
// ===========================
try {
const img = new Image();
img.src = "/logo.png";
await new Promise((resolve) => {
img.onload = resolve;
img.onerror = resolve;
});
doc.addImage(img, "PNG", 14, 10, 18, 18);
} catch {}
// ===========================
// Company Header
// ===========================
doc.setFont("helvetica", "bold");
doc.setFontSize(18);
doc.text("VerifyPack", 38, 18);
doc.setFont("helvetica", "normal");
doc.setFontSize(10);
doc.setTextColor(100);
doc.text("Digital Trust for Every Product", 38, 24);
// ===========================
// Report Title
// ===========================
doc.setDrawColor(220);
doc.line(14, 32, 283, 32);
doc.setTextColor(0);
doc.setFont("helvetica", "bold");
doc.setFontSize(15);
doc.text("Compliance Report", 14, 40);
doc.setFont("helvetica", "normal");
doc.setFontSize(10);
doc.text(
`Generated On : ${new Date().toLocaleString("en-IN")}`,
14,
47
);
doc.text(
`Total Records : ${exportRows.length}`,
250,
47,
{ align: "right" }
);
// ===========================
// Table
// ===========================
autoTable(doc, {
startY: 55,
head: [[
"Product",
"SKU",
"Compliance Type",
"License No",
"Issue Date",
"Expiry Date",
"Days Left",
"Status",
"Country",
"Authority",
]],
body: exportRows.map((r) => [
r.product_name || "",
r.sku || "",
r.compliance_type || "",
r.license_number || "",
r.issue_date
? new Date(r.issue_date).toLocaleDateString("en-IN")
: "-",
r.expiry_date
? new Date(r.expiry_date).toLocaleDateString("en-IN")
: "-",
r.days_left == null
? "-"
: r.days_left < 0
? "Expired"
: r.days_left === 0
? "Today"
: `${r.days_left} Days`,
r.days_left == null
? "Pending"
: r.days_left < 0
? "Expired"
: r.days_left <= 30
? "Expiring"
: "Complete",
r.country || "-",
r.issuing_authority || "-",
]),
styles: {
fontSize: 8,
cellPadding: 2,
},
headStyles: {
fillColor: [37, 99, 235],
textColor: 255,
fontStyle: "bold",
},
alternateRowStyles: {
fillColor: [245, 245, 245],
},
margin: {
left: 14,
right: 14,
},
didDrawPage: (data) => {
doc.setFontSize(9);
doc.setTextColor(120);
doc.text(
`Page ${data.pageNumber}`,
doc.internal.pageSize.getWidth() - 20,
doc.internal.pageSize.getHeight() - 8,
{
align: "right",
}
);
doc.text(
"Generated by VerifyPack",
14,
doc.internal.pageSize.getHeight() - 8
);
},
});
// ===========================
// Save PDF
// ===========================
doc.save(
selectedRows.length
? `selected_compliance_report.pdf`
: `compliance_report.pdf`
);
}
async function confirmDelete() {
if (!deleteRow) return;
try {
setDeleting(true);
await api(`/compliance/${deleteRow.id}`, {
method: "DELETE",
});
setDeleteRow(null);
load();
} catch (err) {
alert("Failed to delete compliance record.");
} finally {
setDeleting(false);
}
}
function toggleRow(id: string) {
setSelectedRows((prev) =>
prev.includes(id)
? prev.filter((x) => x !== id)
: [...prev, id]
);
}
const allSelected =
paged.length > 0 &&
paged.every((r) => selectedRows.includes(r.id));
function toggleAll() {
if (allSelected) {
setSelectedRows((prev) =>
prev.filter((id) => !paged.some((r) => r.id === id))
);
} else {
setSelectedRows((prev) => [
...new Set([...prev, ...paged.map((r) => r.id)]),
]);
}
}
async function deleteSelected() {
try {
setDeleting(true);
await Promise.all(
selectedRows.map((id) =>
api(`/compliance/${id}`, {
method: "DELETE",
})
)
);
setSelectedRows([]);
setBulkDeleteOpen(false);
load();
} finally {
setDeleting(false);
}
}
function openAdd() {
setForm({ product_id: "", compliance_type: TYPES[0], license_number: "", license_name: "",
issuing_authority: "", issue_date: "", expiry_date: "", country: "India", state: "",
category: "", applicable_to: "Manufacturing Plant", plant: "", remarks: "", _id: "" });
}
function openEdit(r: any) {
setDetailId(null);
setForm({ ...r, _id: r.id, product_id: r.product_id ?? "",
issue_date: r.issue_date ? r.issue_date.slice(0, 10) : "",
expiry_date: r.expiry_date ? r.expiry_date.slice(0, 10) : "" });
}
async function saveForm() {
const { _id, ...payload } = form;
if (_id) await api(`/compliance/${_id}`, { method: "PUT", body: JSON.stringify(payload) });
else await api("/compliance", { method: "POST", body: JSON.stringify(payload) });
setForm(null); load();
}
return (
<div className="space-y-5" onClick={() => setMenuFor(null)}>
<div className="flex items-center justify-between">
<h2 className="text-xl font-bold">Compliance Center</h2>
</div>
{/* KPIs */}
<div className="grid grid-cols-2 lg:grid-cols-5 gap-4">
<Kpi label="Total Products" value={s?.total_products ?? 0} sub="All products" icon={<Package className="h-5 w-5" />} tint="bg-blue-50 text-blue-600" />
<Kpi label="Complete" value={s?.complete ?? 0} sub={`${s?.complete_pct ?? 0}% of total`} subColor="text-emerald-600" icon={<CheckCircle2 className="h-5 w-5" />} tint="bg-emerald-50 text-emerald-600" />
<Kpi label="Pending" value={s?.pending ?? 0} sub={`${s?.pending_pct ?? 0}% of total`} subColor="text-amber-600" icon={<Clock className="h-5 w-5" />} tint="bg-amber-50 text-amber-600" />
<Kpi label="Expired" value={s?.expired ?? 0} sub={`${s?.expired_pct ?? 0}% of total`} subColor="text-rose-600" icon={<AlertTriangle className="h-5 w-5" />} tint="bg-rose-50 text-rose-500" />
<Kpi label="Upcoming Expiry" value={s?.expiring ?? 0} sub="Next 30 days" icon={<Calendar className="h-5 w-5" />} tint="bg-violet-50 text-violet-600" />
</div>
{/* Filters */}
<div className="card p-4 flex flex-wrap items-end gap-3">
<div className="relative">
<Search className="absolute left-3 top-2.5 h-4 w-4 text-slate-400" />
<input value={filters.search} onChange={(e) => set("search", e.target.value)} placeholder="Search compliance…"
className="pl-9 pr-3 py-2 w-52 rounded-lg border border-slate-200 text-sm" />
</div>
<Filter label="Status" value={filters.status_filter} onChange={(v) => set("status_filter", v)} opts={["complete", "pending", "expiring", "expired"].map((x) => ({ value: x, label: x }))} all="All Status" />
<Filter label="Type" value={filters.compliance_type} onChange={(v) => set("compliance_type", v)} opts={TYPES.map((t) => ({ value: t, label: t }))} all="All Types" />
<Filter label="Country" value={filters.country} onChange={(v) => set("country", v)} opts={[{ value: "India", label: "India" }]} all="All Countries" />
</div>
{/* Alerts */}
{/* Alerts */}
{showAlerts && s?.alerts?.length > 0 && (
<div className="card p-4 bg-amber-50 border border-amber-200 rounded-xl relative">
<button
onClick={() => setShowAlerts(false)}
className="absolute top-3 right-3 rounded-md p-1 text-amber-700 hover:bg-amber-100"
>
<X className="h-4 w-4" />
</button>
<div className="font-semibold text-amber-800 text-sm flex items-center gap-2 mb-2">
<AlertTriangle className="h-4 w-4" />
Attention needed
</div>
<div className="space-y-1">
{s.alerts.map((a: any, i: number) => (
<div key={i} className="text-sm text-amber-700">
{a.type}
{a.number ? ` (${a.number})` : ""}
{" — "}
{a.status}
{a.expiry
? ` · expires ${new Date(a.expiry).toLocaleDateString()}`
: ""}
</div>
))}
</div>
</div>
)}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="text-sm text-slate-500">
Total: {rows.length}
</div>
{/* Import */}
<label className="flex items-center gap-2 px-4 py-2 rounded-lg border border-slate-200 bg-white hover:bg-slate-50 cursor-pointer text-sm font-medium">
<Upload className="h-4 w-4" />
Import
<input
type="file"
accept=".csv,.xlsx,.xls,.json"
className="hidden"
onChange={handleImport}
/>
</label>
{/* Export */}
<button
onClick={handleExport}
className="flex items-center gap-2 px-4 py-2 rounded-lg border border-slate-200 bg-white hover:bg-slate-50 text-sm font-medium"
>
<Download className="h-4 w-4" />
{selectedRows.length > 0
? `Export Selected (${selectedRows.length})`
: "Export"}
</button>
<button
onClick={handlePdfExport}
className="flex items-center gap-2 rounded-lg border border-slate-200 bg-white px-4 py-2 text-sm font-medium hover:bg-slate-50"
>
<Download className="h-4 w-4" />
{selectedRows.length
? `PDF (${selectedRows.length})`
: "PDF Report"}
</button>
{/* Add */}
<button
onClick={openAdd}
className="flex items-center gap-2 bg-brand-600 text-white px-4 py-2 rounded-lg text-sm font-semibold"
>
<Plus className="h-4 w-4" />
Add Compliance
</button>
</div>
{selectedRows.length > 0 && (
<div className="flex items-center gap-3">
<span className="text-sm font-medium text-brand-600">
{selectedRows.length} Selected
</span>
<button
onClick={() => setBulkDeleteOpen(true)}
className="rounded-lg bg-red-600 px-4 py-2 text-sm text-white"
>
Delete Selected
</button>
</div>
)}
</div>
{/* Table */}
<div className="card overflow-x-auto">
<table className="w-full text-sm">
<thead className="bg-slate-50 text-slate-500 text-left">
<tr>
<th className="px-4 py-3">
<input
type="checkbox"
checked={allSelected}
onChange={toggleAll}
/>
</th>
<th className="px-4 py-3 font-medium">Product</th>
<th className="px-4 py-3 font-medium">Type</th>
<th className="px-4 py-3 font-medium">License / Reg.</th>
<th className="px-4 py-3 font-medium">Issue Date</th>
<th className="px-4 py-3 font-medium">Expiry Date</th>
<th className="px-4 py-3 font-medium">Status</th>
<th className="px-4 py-3 font-medium">Country</th>
<th className="px-4 py-3 font-medium">Last Updated</th>
<th className="px-4 py-3 font-medium">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{paged.map((c) => (
<tr key={c.id} className="hover:bg-slate-50">
<td className="px-4 py-3">
<input
type="checkbox"
checked={selectedRows.includes(c.id)}
onChange={() => toggleRow(c.id)}
/>
</td>
<td className="px-4 py-3">
<div className="flex items-center gap-3">
{c.product_image
? <img src={imgSrc(c.product_image)} className="h-10 w-10 rounded object-contain border border-slate-100" />
: <div className="h-10 w-10 rounded bg-slate-100" />}
<div>
<div className="font-medium text-slate-800">{c.product_name ?? "—"}</div>
<div className="text-xs text-slate-400">{c.sku ?? ""}</div>
</div>
</div>
</td>
<td className="px-4 py-3">{c.compliance_type}</td>
<td className="px-4 py-3 font-mono text-slate-600">{c.license_number ?? "—"}</td>
<td className="px-4 py-3 text-slate-500">{c.issue_date ? new Date(c.issue_date).toLocaleDateString() : "—"}</td>
<td className="px-4 py-3">
{c.expiry_date ? (
<div>
<div className={c.days_left != null && c.days_left < 0 ? "text-rose-600" : c.days_left != null && c.days_left < 30 ? "text-amber-600" : "text-slate-700"}>
{new Date(c.expiry_date).toLocaleDateString()}
</div>
{c.days_left != null && <div className={`text-xs ${c.days_left < 0 ? "text-rose-500" : "text-slate-400"}`}>
{c.days_left < 0 ? "(Expired)" : `(In ${c.days_left} days)`}
</div>}
</div>
) : "—"}
</td>
<td className="px-4 py-3"><span className={`text-xs font-semibold px-2 py-0.5 rounded-full capitalize ${STATUS[c.status] ?? "bg-slate-100"}`}>{c.status}</span></td>
<td className="px-4 py-3">{c.country ?? "—"}</td>
<td className="px-4 py-3 text-slate-500">
{c.updated_at ? (
<div>
<div>
{new Date(c.updated_at).toLocaleDateString("en-IN")}
</div>
<div className="text-xs text-slate-400">
{new Date(c.updated_at).toLocaleTimeString("en-IN", {
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
})}
</div>
</div>
) : (
"—"
)}
</td>
<td className="px-4 py-3 relative">
<div className="flex items-center gap-1">
<button title="View" onClick={() => setDetailId(c.id)} className="h-8 w-8 grid place-items-center rounded-lg border border-slate-200 text-slate-500 hover:bg-slate-50"><Eye className="h-4 w-4" /></button>
<button onClick={(e) => { e.stopPropagation(); setMenuFor(menuFor === c.id ? null : c.id); }} className="h-8 w-8 grid place-items-center rounded-lg border border-slate-200 text-slate-500 hover:bg-slate-50"><MoreVertical className="h-4 w-4" /></button>
</div>
{menuFor === c.id && (
<div onClick={(e) => e.stopPropagation()} className="absolute right-4 top-12 z-20 w-44 bg-white rounded-xl border border-slate-200 shadow-lg py-1 text-left">
<button onClick={() => { openEdit(c); setMenuFor(null); }} className="w-full flex items-center gap-2 px-3 py-2 text-sm text-slate-700 hover:bg-slate-50"><Pencil className="h-4 w-4" /> Edit</button>
<button onClick={() => { setDetailId(c.id); setMenuFor(null); }} className="w-full flex items-center gap-2 px-3 py-2 text-sm text-slate-700 hover:bg-slate-50"><Upload className="h-4 w-4" /> Upload Document</button>
<div className="my-1 border-t border-slate-100"></div>
<button
onClick={() => {
setDeleteRow(c);
setMenuFor(null);
}}
className="w-full flex items-center gap-2 px-3 py-2 text-sm text-red-600 hover:bg-red-50"
>
<Trash2 className="h-4 w-4" />
Delete
</button>
</div>
)}
</td>
</tr>
))}
{paged.length === 0 && <tr><td colSpan={8} className="px-4 py-12 text-center text-slate-400">No compliance records.</td></tr>}
</tbody>
</table>
<div className="flex items-center justify-between px-4 py-3 text-sm text-slate-500">
<span>Showing {rows.length === 0 ? 0 : (page - 1) * pageSize + 1} to {Math.min(page * pageSize, rows.length)} of {rows.length}</span>
<div className="flex gap-1">
<button disabled={page === 1} onClick={() => setPage(page - 1)} className="px-3 py-1 rounded border border-slate-200 disabled:opacity-40"></button>
{Array.from({ length: totalPages }).slice(0, 5).map((_, i) => (
<button key={i} onClick={() => setPage(i + 1)} className={`px-3 py-1 rounded ${page === i + 1 ? "bg-brand-600 text-white" : "border border-slate-200"}`}>{i + 1}</button>
))}
<button disabled={page === totalPages} onClick={() => setPage(page + 1)} className="px-3 py-1 rounded border border-slate-200 disabled:opacity-40"></button>
</div>
</div>
</div>
{/* Detail modal */}
{detailId && (
<ComplianceDetailModal cid={detailId}
onClose={() => setDetailId(null)}
onEdit={() => { const r = rows.find((x) => x.id === detailId); if (r) openEdit(r); }}
onChanged={load} />
)}
{deleteRow && (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4"
onClick={() => setDeleteRow(null)}
>
<div
onClick={(e) => e.stopPropagation()}
className="w-full max-w-md rounded-2xl bg-white shadow-xl"
>
<div className="border-b px-6 py-4">
<h2 className="text-lg font-bold text-slate-900">
Delete Compliance
</h2>
</div>
<div className="p-6 space-y-4">
<div className="flex items-center gap-4">
{deleteRow.product_image ? (
<img
src={imgSrc(deleteRow.product_image)}
className="h-16 w-16 rounded-lg border object-contain"
/>
) : (
<div className="h-16 w-16 rounded-lg bg-slate-100" />
)}
<div>
<div className="font-semibold text-slate-900">
{deleteRow.product_name}
</div>
<div className="text-sm text-slate-500">
{deleteRow.compliance_type}
</div>
<div className="text-xs text-slate-400">
{deleteRow.license_number || "No License Number"}
</div>
</div>
</div>
<div className="rounded-lg border border-red-200 bg-red-50 p-4">
<p className="text-sm text-red-700">
Are you sure you want to delete this compliance record?
</p>
<p className="mt-2 text-xs text-red-600">
This action cannot be undone.
</p>
</div>
</div>
<div className="flex justify-end gap-3 border-t px-6 py-4">
<button
onClick={() => setDeleteRow(null)}
className="rounded-lg border border-slate-200 px-4 py-2 text-sm"
>
Cancel
</button>
<button
onClick={confirmDelete}
disabled={deleting}
className="rounded-lg bg-red-600 px-5 py-2 text-sm font-semibold text-white hover:bg-red-700 disabled:opacity-50"
>
{deleting ? "Deleting..." : "Delete"}
</button>
</div>
</div>
</div>
)}
{bulkDeleteOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div className="w-full max-w-md rounded-xl bg-white shadow-xl">
<div className="border-b px-6 py-4">
<h2 className="text-lg font-bold">
Delete Compliance Records
</h2>
</div>
<div className="p-6">
<p className="text-slate-700">
Are you sure you want to delete
<span className="font-bold text-red-600">
{" "}
{selectedRows.length} compliance record
{selectedRows.length > 1 ? "s" : ""}
</span>
?
</p>
<p className="mt-2 text-sm text-slate-500">
This action cannot be undone.
</p>
</div>
<div className="flex justify-end gap-3 border-t px-6 py-4">
<button
onClick={() => setBulkDeleteOpen(false)}
className="rounded-lg border px-4 py-2"
>
Cancel
</button>
<button
onClick={deleteSelected}
disabled={deleting}
className="rounded-lg bg-red-600 px-5 py-2 text-white"
>
{deleting
? "Deleting..."
: `Delete (${selectedRows.length})`}
</button>
</div>
</div>
</div>
)}
{/* Add/Edit form modal */}
{form && (
<div className="fixed inset-0 bg-black/40 z-50 grid place-items-center p-4 overflow-y-auto" onClick={() => setForm(null)}>
<div className="bg-white rounded-2xl w-full max-w-lg my-4 max-h-[92vh] overflow-y-auto" onClick={(e) => e.stopPropagation()}>
<div className="flex items-center justify-between px-5 py-4 border-b border-slate-100 sticky top-0 bg-white rounded-t-2xl">
<h3 className="font-bold">{form._id ? "Edit Compliance" : "Add Compliance"}</h3>
<button onClick={() => setForm(null)}><X className="h-5 w-5 text-slate-400" /></button>
</div>
<div className="p-5 grid grid-cols-2 gap-3">
<Fld label="Product" full>
<select value={form.product_id} onChange={(e) => setForm({ ...form, product_id: e.target.value })} className="input">
<option value="">Select product</option>
{products.map((p) => <option key={p.id} value={p.id}>{p.name}</option>)}
</select>
</Fld>
<Fld label="Compliance Type">
<select value={form.compliance_type} onChange={(e) => setForm({ ...form, compliance_type: e.target.value })} className="input">
{TYPES.map((t) => <option key={t}>{t}</option>)}
</select>
</Fld>
<Inp label="License / Reg. No." v={form.license_number} on={(x) => setForm({ ...form, license_number: x })} />
<Inp label="License Name" v={form.license_name} on={(x) => setForm({ ...form, license_name: x })} />
<Inp label="Issuing Authority" v={form.issuing_authority} on={(x) => setForm({ ...form, issuing_authority: x })} />
<Inp label="Issue Date" type="date" v={form.issue_date} on={(x) => setForm({ ...form, issue_date: x })} />
<Inp label="Expiry Date" type="date" v={form.expiry_date} on={(x) => setForm({ ...form, expiry_date: x })} />
<Inp label="Country" v={form.country} on={(x) => setForm({ ...form, country: x })} />
<Inp label="State" v={form.state} on={(x) => setForm({ ...form, state: x })} />
<Inp label="Category" v={form.category} on={(x) => setForm({ ...form, category: x })} />
<Inp label="Applicable To" v={form.applicable_to} on={(x) => setForm({ ...form, applicable_to: x })} />
<Inp label="Manufacturing Plant" v={form.plant} on={(x) => setForm({ ...form, plant: x })} />
<Inp label="Remarks" full v={form.remarks} on={(x) => setForm({ ...form, remarks: x })} />
</div>
<div className="flex justify-end gap-3 px-5 py-4 border-t border-slate-100 sticky bottom-0 bg-white">
<button onClick={() => setForm(null)} className="px-5 py-2 rounded-lg border border-slate-200 text-sm font-medium">Cancel</button>
<button onClick={saveForm} className="px-6 py-2 rounded-lg bg-brand-600 text-white text-sm font-semibold">Save</button>
</div>
</div>
</div>
)}
<style>{`.input{margin-top:.25rem;width:100%;padding:.5rem .75rem;border-radius:.5rem;border:1px solid #e2e8f0;font-size:.875rem}`}</style>
</div>
);
}
function Kpi({ label, value, sub, subColor = "text-slate-400", icon, tint }: any) {
return (
<div className="card p-5">
<div className="flex items-start gap-4">
<div className={`h-12 w-12 rounded-xl grid place-items-center shrink-0 ${tint}`}>{icon}</div>
<div className="min-w-0">
<div className="text-sm text-slate-500">{label}</div>
<div className="text-2xl font-extrabold text-slate-900 mt-0.5">{value}</div>
<div className={`text-xs font-medium mt-1 ${subColor}`}>{sub}</div>
</div>
</div>
</div>
);
}
function Filter({ label, value, onChange, opts, all }: {
label: string; value: string; onChange: (v: string) => void; opts: { value: string; label: string }[]; all: string;
}) {
return (
<label className="block">
<span className="text-xs text-slate-500">{label}</span>
<select value={value} onChange={(e) => onChange(e.target.value)} className="mt-1 block px-3 py-2 rounded-lg border border-slate-200 text-sm min-w-[130px] capitalize">
<option value="">{all}</option>
{opts.map((o) => <option key={o.value} value={o.value}>{o.label}</option>)}
</select>
</label>
);
}
function Fld({ label, children, full }: any) {
return <label className={`block ${full ? "col-span-2" : ""}`}><span className="text-sm font-medium">{label}</span>{children}</label>;
}
function Inp({ label, v, on, type = "text", full }: { label: string; v: any; on: (x: string) => void; type?: string; full?: boolean }) {
return (
<label className={`block ${full ? "col-span-2" : ""}`}>
<span className="text-sm font-medium">{label}</span>
<input type={type} value={v ?? ""} onChange={(e) => on(e.target.value)} className="input" />
</label>
);
}