169 lines
7.7 KiB
TypeScript
169 lines
7.7 KiB
TypeScript
"use client";
|
|
import { useEffect, useRef, useState } from "react";
|
|
import { X, Pencil, Upload, ExternalLink, FileText } from "lucide-react";
|
|
import { api, apiBase } from "@/lib/api";
|
|
|
|
function imgSrc(url?: string) {
|
|
if (!url) return "";
|
|
if (url.startsWith("data:") || url.startsWith("http")) return url;
|
|
return `${apiBase}${url}`;
|
|
}
|
|
|
|
type Tab = "overview" | "documents" | "history";
|
|
|
|
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 ComplianceDetailModal({ cid, onClose, onEdit, onChanged }: {
|
|
cid: string; onClose: () => void; onEdit: () => void; onChanged: () => void;
|
|
}) {
|
|
const [d, setD] = useState<any>(null);
|
|
const [tab, setTab] = useState<Tab>("overview");
|
|
const fileRef = useRef<HTMLInputElement>(null);
|
|
|
|
function load() { api(`/compliance/${cid}/detail`).then(setD).catch(() => {}); }
|
|
useEffect(() => { load(); }, [cid]);
|
|
|
|
async function onUpload(e: React.ChangeEvent<HTMLInputElement>) {
|
|
const file = e.target.files?.[0];
|
|
if (!file) return;
|
|
const r = new FileReader();
|
|
r.onload = async () => {
|
|
await api(`/compliance/${cid}/document`, {
|
|
method: "POST",
|
|
body: JSON.stringify({ data_url: r.result, filename: file.name }),
|
|
});
|
|
load(); onChanged();
|
|
};
|
|
r.readAsDataURL(file);
|
|
}
|
|
|
|
if (!d) return (
|
|
<div className="fixed inset-0 bg-black/40 z-50 grid place-items-center p-4" onClick={onClose}>
|
|
<div className="bg-white rounded-2xl w-full max-w-md p-5" onClick={(e) => e.stopPropagation()}>
|
|
<div className="skeleton h-40 rounded-xl" />
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
const expColor = d.days_left != null && d.days_left < 0 ? "text-rose-600"
|
|
: d.days_left != null && d.days_left < 30 ? "text-amber-600" : "text-slate-700";
|
|
|
|
return (
|
|
<div className="fixed inset-0 bg-black/40 z-50 grid place-items-center p-4 overflow-y-auto" onClick={onClose}>
|
|
<div className="bg-white rounded-2xl w-full max-w-md my-4 flex flex-col 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">Compliance Details</h3>
|
|
<button onClick={onClose}><X className="h-5 w-5 text-slate-400" /></button>
|
|
</div>
|
|
|
|
<div className="p-5 space-y-4">
|
|
<div className="flex items-center gap-3">
|
|
{d.product_image
|
|
? <img src={imgSrc(d.product_image)} className="h-14 w-14 rounded-lg object-contain border border-slate-100" />
|
|
: <div className="h-14 w-14 rounded-lg bg-slate-100" />}
|
|
<div className="flex-1">
|
|
<div className="font-bold">{d.product_name ?? "—"}</div>
|
|
<div className="text-xs text-slate-400">SKU: {d.sku ?? "—"}</div>
|
|
</div>
|
|
<span className={`text-xs font-semibold px-2 py-0.5 rounded-full capitalize ${STATUS[d.status] ?? "bg-slate-100"}`}>{d.status}</span>
|
|
</div>
|
|
|
|
<div className="flex gap-1 border-b border-slate-200 text-sm">
|
|
{([["overview", "Overview"], ["documents", `Documents (${d.documents?.length ?? 0})`], ["history", "History"]] as const).map(([t, l]) => (
|
|
<button key={t} onClick={() => setTab(t)}
|
|
className={`px-3 py-2 border-b-2 -mb-px font-medium ${tab === t ? "border-brand-600 text-brand-700" : "border-transparent text-slate-500"}`}>{l}</button>
|
|
))}
|
|
</div>
|
|
|
|
{tab === "overview" && (
|
|
<div className="space-y-2 text-sm">
|
|
<Row label="Compliance Type" value={d.compliance_type} />
|
|
<Row label="License / Reg. No." value={d.license_number} />
|
|
<Row label="License Name" value={d.license_name} />
|
|
<Row label="Issue Date" value={fmt(d.issue_date)} />
|
|
<div className="flex justify-between border-b border-slate-50 py-1.5">
|
|
<span className="text-slate-400">Expiry Date</span>
|
|
<span className={`font-medium text-right ${expColor}`}>
|
|
{fmt(d.expiry_date)}
|
|
{d.days_left != null && <span className="block text-xs">
|
|
{d.days_left < 0 ? "(Expired)" : `(In ${d.days_left} days)`}
|
|
</span>}
|
|
</span>
|
|
</div>
|
|
<Row label="Status" value={d.status} cap />
|
|
<Row label="Issuing Authority" value={d.issuing_authority} />
|
|
<Row label="Country" value={d.country} />
|
|
<Row label="State" value={d.state} />
|
|
<Row label="Category" value={d.category} />
|
|
<Row label="Applicable To" value={d.applicable_to} />
|
|
<Row label="Manufacturing Plant" value={d.plant} />
|
|
<Row label="Remarks" value={d.remarks} />
|
|
<Row label="Created By" value={d.created_by} />
|
|
<Row label="Created On" value={fmt(d.created_at, true)} />
|
|
<Row label="Last Updated" value={fmt(d.updated_at, true)} />
|
|
</div>
|
|
)}
|
|
|
|
{tab === "documents" && (
|
|
<div className="space-y-2 text-sm">
|
|
{(d.documents ?? []).map((doc: any, i: number) => (
|
|
<a key={i} href={`${apiBase}${doc.url}`} target="_blank" rel="noreferrer"
|
|
className="flex items-center gap-2 border border-slate-100 rounded-lg px-3 py-2 hover:bg-slate-50">
|
|
<FileText className="h-4 w-4 text-slate-400" /> {doc.name}
|
|
</a>
|
|
))}
|
|
{(!d.documents || d.documents.length === 0) && <p className="text-slate-400">No documents uploaded.</p>}
|
|
</div>
|
|
)}
|
|
|
|
{tab === "history" && (
|
|
<div className="space-y-1 text-sm">
|
|
{(d.history ?? []).map((h: any, i: number) => (
|
|
<div key={i} className="flex justify-between border-b border-slate-50 py-1.5">
|
|
<span>{h.action} {h.by ? `· ${h.by}` : ""}</span>
|
|
<span className="text-slate-400">{fmt(h.at, true)}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
<div className="space-y-2 pt-2">
|
|
<div className="grid grid-cols-2 gap-2">
|
|
<button onClick={onEdit} className="flex items-center justify-center gap-1 border border-slate-200 rounded-lg py-2 text-sm font-medium">
|
|
<Pencil className="h-4 w-4" /> Edit
|
|
</button>
|
|
<button onClick={() => fileRef.current?.click()} className="flex items-center justify-center gap-1 bg-brand-600 text-white rounded-lg py-2 text-sm font-semibold">
|
|
<Upload className="h-4 w-4" /> Upload Document
|
|
</button>
|
|
<input ref={fileRef} type="file" accept="application/pdf,image/*" className="hidden" onChange={onUpload} />
|
|
</div>
|
|
<a href="#" onClick={(e) => e.preventDefault()}
|
|
className="flex items-center justify-center gap-1 border border-brand-200 text-brand-700 rounded-lg py-2 text-sm font-medium">
|
|
<ExternalLink className="h-4 w-4" /> View on Verification Page
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function fmt(d?: string, time = false) {
|
|
if (!d) return "—";
|
|
const dt = new Date(d);
|
|
return isNaN(+dt) ? "—" : time ? dt.toLocaleString() : dt.toLocaleDateString();
|
|
}
|
|
function Row({ label, value, cap }: any) {
|
|
return (
|
|
<div className="flex justify-between border-b border-slate-50 py-1.5">
|
|
<span className="text-slate-400">{label}</span>
|
|
<span className={`font-medium text-slate-700 text-right ${cap ? "capitalize" : ""}`}>{value ?? "—"}</span>
|
|
</div>
|
|
);
|
|
}
|