"use client"; import { useEffect, useMemo, useState } from "react"; import { useRouter } from "next/navigation"; import { fmtGBP, fmtDate } from "../lib/pb"; type Invoice = { id: string; invoice_no: string; invoice_date: string; buyer_name_variant: string; customer_order_no: string; invoice_total: number; }; const STORAGE_KEY = "jei:selected-invoices"; function todayYYYYMMDD(): string { const d = new Date(); const y = d.getFullYear(); const m = String(d.getMonth() + 1).padStart(2, "0"); const day = String(d.getDate()).padStart(2, "0"); return `${y}${m}${day}`; } function nextBatchRefSuggestion(): string { // We don't fetch today's count server-side; this is a client suggestion only. // Server validates uniqueness on submit. return `BATCH-${todayYYYYMMDD()}-001`; } export default function UnpaidClient({ initialRecords }: { initialRecords: Invoice[] }) { const router = useRouter(); const records = initialRecords; const [selected, setSelected] = useState>(new Set()); const [hydrated, setHydrated] = useState(false); const [modalOpen, setModalOpen] = useState(false); const [batchRef, setBatchRef] = useState(nextBatchRefSuggestion()); const [paymentDate, setPaymentDate] = useState(() => new Date().toISOString().slice(0, 10)); const [popFile, setPopFile] = useState(null); const [zarPaid, setZarPaid] = useState(""); // optional ZAR amount actually paid; "" means omitted const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); // Hydrate selection from localStorage useEffect(() => { try { const raw = localStorage.getItem(STORAGE_KEY); if (raw) { const arr = JSON.parse(raw); if (Array.isArray(arr)) { // Only keep IDs that are still in the unpaid list const validIds = new Set(records.map((r) => r.id)); setSelected(new Set(arr.filter((id: any) => validIds.has(id)))); } } } catch (_) { // ignore parse errors } setHydrated(true); }, [records]); // Persist on change useEffect(() => { if (!hydrated) return; try { localStorage.setItem(STORAGE_KEY, JSON.stringify(Array.from(selected))); } catch (_) { // localStorage may be unavailable (e.g. private mode) } }, [selected, hydrated]); const selectedRecords = useMemo( () => records.filter((r) => selected.has(r.id)), [records, selected], ); const total = useMemo( () => selectedRecords.reduce((s, r) => s + (r.invoice_total || 0), 0), [selectedRecords], ); function toggleAll(checked: boolean) { if (checked) setSelected(new Set(records.map((r) => r.id))); else setSelected(new Set()); } function toggleOne(id: string, checked: boolean) { setSelected((prev) => { const next = new Set(prev); if (checked) next.add(id); else next.delete(id); return next; }); } const allChecked = records.length > 0 && selected.size === records.length; async function submitPayment(e: React.FormEvent) { e.preventDefault(); setError(null); if (!popFile) { setError("POP file is required"); return; } setSubmitting(true); try { const fd = new FormData(); fd.set("batch_ref", batchRef); fd.set("payment_date", paymentDate); fd.set("pop_file", popFile); fd.set("invoice_ids", JSON.stringify(Array.from(selected))); // Optional ZAR paid: only set when the user typed something, so the API // treats missing as "no ZAR amount recorded" rather than as 0. if (zarPaid.trim() !== "") { fd.set("zar_paid", zarPaid.trim()); } const resp = await fetch("/api/payments/create", { method: "POST", body: fd }); const json = await resp.json().catch(() => ({})); if (!resp.ok) { setError(json?.error || `HTTP ${resp.status}`); return; } // Success: clear selection, close modal, refresh setSelected(new Set()); setModalOpen(false); router.refresh(); } catch (e: any) { setError(String(e?.message || e)); } finally { setSubmitting(false); } } return ( <>
{records.length === 0 && ( )} {records.map((r) => ( ))}
toggleAll(e.target.checked)} /> Date Invoice No Buyer PO Total (£)
No unpaid invoices. All caught up 🎉
toggleOne(r.id, e.target.checked)} /> {fmtDate(r.invoice_date)} {r.invoice_no} {r.buyer_name_variant} {r.customer_order_no || "—"} {fmtGBP(r.invoice_total)}
{selected.size > 0 && (
{selected.size} selected ·{" "} {fmtGBP(total)}
)} {selected.size === 0 && records.length > 0 && (
Tick invoices above to create a payment batch.
)} {modalOpen && (

Create Payment Batch

Bundling {selectedRecords.length} invoices for{" "} {fmtGBP(total)}.
setBatchRef(e.target.value)} pattern="[A-Za-z0-9 _\-\/.,:()&#'+]{1,100}" required className="w-full border rounded px-3 py-2 font-mono" />
setPaymentDate(e.target.value)} required className="w-full border rounded px-3 py-2" />
setPopFile(e.target.files?.[0] || null)} className="w-full border rounded px-3 py-2" />
setZarPaid(e.target.value)} placeholder="e.g. 12500.00" className="w-full border rounded px-3 py-2 tabular-nums" />
{error && (
{error}
)}
)} ); }