diff --git a/app/api/payments/create/route.ts b/app/api/payments/create/route.ts index 91f1b39..a361048 100644 --- a/app/api/payments/create/route.ts +++ b/app/api/payments/create/route.ts @@ -22,6 +22,10 @@ export async function POST(req: NextRequest) { const paymentDate = String(form.get("payment_date") || "").trim(); const invoiceIdsRaw = String(form.get("invoice_ids") || ""); const popFile = form.get("pop_file"); + // Optional ZAR amount actually paid for this batch. Empty / missing -> 0. + // PB "number" field accepts numeric strings or numbers via the SDK. + const zarPaidRaw = String(form.get("zar_paid") || "").trim(); + const zarPaid = zarPaidRaw === "" ? 0 : Number(zarPaidRaw); // 1. invoice_ids let invoiceIds: string[]; @@ -115,6 +119,7 @@ export async function POST(req: NextRequest) { batch_ref: batchRef, payment_date: paymentDate, pop_file: popFile, + zar_paid: Number.isFinite(zarPaid) ? zarPaid : 0, }); batchId = created.id; } catch (e: any) { diff --git a/app/lib/types.ts b/app/lib/types.ts index b5efb61..fadf430 100644 --- a/app/lib/types.ts +++ b/app/lib/types.ts @@ -54,6 +54,7 @@ export interface PaymentBatch { payment_date: string; // ISO date "YYYY-MM-DD" pop_file: string; // PB file URL (relative path) notes: string; + zar_paid?: number; // optional ZAR amount actually paid; 0/omitted when paid from a £ bank account created: string; updated: string; } diff --git a/app/remittance/RemittanceClient.tsx b/app/remittance/RemittanceClient.tsx index 0da0145..73f1343 100644 --- a/app/remittance/RemittanceClient.tsx +++ b/app/remittance/RemittanceClient.tsx @@ -14,7 +14,7 @@ type Invoice = { }; type BatchWithInvoices = { - batch: { id: string; batch_ref: string; payment_date: string; pop_file: string }; + batch: { id: string; batch_ref: string; payment_date: string; pop_file: string; zar_paid?: number }; invoices: Invoice[]; popUrl: string | null; }; @@ -99,6 +99,9 @@ export default function RemittanceClient({ batches }: { batches: BatchWithInvoic
{fmtDate(batch.payment_date)} · {invoices.length} invoice{invoices.length === 1 ? "" : "s"} ·{" "} {fmtGBP(total)} + {batch.zar_paid && batch.zar_paid > 0 ? ( + · ZAR Paid R{batch.zar_paid.toLocaleString("en-ZA", { minimumFractionDigits: 2, maximumFractionDigits: 2 })} + ) : null}
diff --git a/app/remittance/page.tsx b/app/remittance/page.tsx index ed71291..67fcd35 100644 --- a/app/remittance/page.tsx +++ b/app/remittance/page.tsx @@ -23,6 +23,7 @@ export default async function RemittancePage() { try { const r = await pb.collection("JEI_payment_batches").getList(1, 500, { sort: "-payment_date", + fields: "id,batch_ref,payment_date,pop_file,zar_paid", }); batches = r.items; } catch (e: any) { diff --git a/app/unpaid/UnpaidClient.tsx b/app/unpaid/UnpaidClient.tsx index e3762fe..6a7d591 100644 --- a/app/unpaid/UnpaidClient.tsx +++ b/app/unpaid/UnpaidClient.tsx @@ -38,6 +38,7 @@ export default function UnpaidClient({ initialRecords }: { initialRecords: Invoi 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); @@ -108,6 +109,11 @@ export default function UnpaidClient({ initialRecords }: { initialRecords: Invoi 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) { @@ -241,6 +247,19 @@ export default function UnpaidClient({ initialRecords }: { initialRecords: Invoi 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 && (
diff --git a/scripts/add_payment_batch_field.py b/scripts/add_payment_batch_field.py index dae9a14..756ec97 100644 --- a/scripts/add_payment_batch_field.py +++ b/scripts/add_payment_batch_field.py @@ -115,6 +115,16 @@ def payment_batches_schema(): "max": 1000, "pattern": "", }, + { + # Optional ZAR amount actually paid for this batch. Defaults to 0 + # if not entered (e.g. paid from a £ UK bank account). + "name": "zar_paid", + "type": "number", + "required": False, + "min": 0, + "max": None, + "noDecimal": False, + }, ], **SUPERUSER_RULES, } @@ -182,6 +192,30 @@ def main(): sys.exit(f"ERROR: failed to add payment_batch field: {err}") print(f"ADDED: JEI_invoices.payment_batch relation -> {batches_id}") + # 3b. Ensure JEI_payment_batches has the zar_paid field (added 2026-09-18). + batches = pb_get(f"/api/collections/{batches_id}") + batches_field_names = {f.get("name") for f in batches.get("fields", [])} + if "zar_paid" in batches_field_names: + print("EXISTS: JEI_payment_batches.zar_paid field") + else: + new_zp = { + "name": "zar_paid", + "type": "number", + "required": False, + "min": 0, + "max": None, + "noDecimal": False, + } + patched_fields = list(batches.get("fields", [])) + [new_zp] + clean_fields = [{k: v for k, v in f.items() if k != "options"} for f in patched_fields] + resp, err = pb_patch( + f"/api/collections/{batches_id}", + {"fields": clean_fields}, + ) + if err is not None: + sys.exit(f"ERROR: failed to add zar_paid field: {err}") + print(f"ADDED: JEI_payment_batches.zar_paid number field") + # 4. Wake-up round-trip: PATCH each collection's rule fields. Even though # the values are already empty strings, the round-trip activates the # collection's rule machinery in PB v0.39.8.