"use client"; import { useState, useRef, useTransition } from "react"; import { useRouter } from "next/navigation"; import { Upload, FileText, ChevronDown, ChevronRight, Loader2, TriangleAlert, Check, ShoppingCart, X, } from "lucide-react"; import { ZAR, ZAR2 } from "@/lib/utils"; const ACCENT = "#30638E"; export type PurchaseLineView = { id: string; line_no: number; material_code: string; cost_code: string; description: string; qty: number | null; rate: number | null; amount_excl: number; vat: number | null; total_incl: number | null; assetCode: string; assetName: string; }; export type PurchaseView = { id: string; po_no: string; supplier: string; supplier_code: string; po_date: string; required_by: string; req_no: string; currency: string; subtotal: number; vat: number; total: number; source_file: string; lines: PurchaseLineView[]; }; export function PurchaseIntake({ purchases }: { purchases: PurchaseView[] }) { const router = useRouter(); const inputRef = useRef(null); const [pending, startTransition] = useTransition(); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); const [notice, setNotice] = useState(null); const [open, setOpen] = useState(null); async function upload(files: FileList | null) { if (!files || !files.length) return; setBusy(true); setError(null); setNotice(null); try { for (const f of Array.from(files)) { const fd = new FormData(); fd.append("file", f); const res = await fetch("/api/purchases/intake", { method: "POST", body: fd }); const body = await res.json(); if (!body.ok) { setError(`${f.name}: ${body.error}`); break; } // A line that found no machine is money that exists but is not yet on one. // Named, not buried — that is the whole point of reading the cost code. setNotice( body.unallocated?.length ? `${body.purchase.po_no}: ${body.lines} line(s) imported, but no machine matched ${body.unallocated.join(", ")} — left unallocated.` : `${body.purchase.po_no}: ${body.lines} line(s) imported and allocated.`, ); } startTransition(() => router.refresh()); } catch { setError("Upload failed — check your connection"); } finally { setBusy(false); if (inputRef.current) inputRef.current.value = ""; } } const totalSpend = purchases.reduce((s, p) => s + p.total, 0); const unallocatedLines = purchases.reduce( (s, p) => s + p.lines.filter((l) => !l.assetCode).length, 0, ); return (
{purchases.length} purchase order{purchases.length !== 1 && "s"} {ZAR(totalSpend)} total {unallocatedLines > 0 && ( {unallocatedLines} line{unallocatedLines !== 1 && "s"} with no machine )}
upload(e.target.files)} />
{error && (
Could not import
{error}
)} {notice && (
{notice}
)} {purchases.length === 0 ? (
No purchase orders yet.
Upload a PO PDF above. Each line is allocated to a machine by its cost code.
) : (
{purchases.map((p) => { const isOpen = open === p.id; const unalloc = p.lines.filter((l) => !l.assetCode).length; return (
{isOpen && (
{p.lines.map((l) => ( ))}
# Description Cost code Machine Amount Incl VAT
{l.line_no} {l.description || no description} {l.cost_code || "—"} {l.assetCode ? ( {l.assetCode} {l.assetName} ) : ( no matching machine )} {ZAR2(l.amount_excl)} {l.total_incl == null ? "—" : ZAR2(l.total_incl)}
)}
); })}
)}
); } function Meta({ label, value }: { label: string; value: string }) { return (
{label}
{value}
); }