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.