feat: add ZAR Paid field to JEI_payment_batches (optional)

This commit is contained in:
Ysterbaard 2026-09-18 23:10:54 +02:00
parent ca1c6cfedd
commit 551baeeb80
6 changed files with 64 additions and 1 deletions

View file

@ -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) {

View file

@ -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;
}

View file

@ -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
<div className="text-xs text-slate-500">
{fmtDate(batch.payment_date)} · {invoices.length} invoice{invoices.length === 1 ? "" : "s"} ·{" "}
<span className="tabular-nums">{fmtGBP(total)}</span>
{batch.zar_paid && batch.zar_paid > 0 ? (
<span className="ml-1">· ZAR Paid <span className="tabular-nums font-semibold text-slate-700">R{batch.zar_paid.toLocaleString("en-ZA", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</span></span>
) : null}
</div>
</div>
<div className="flex gap-2 flex-wrap">

View file

@ -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) {

View file

@ -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<File | null>(null);
const [zarPaid, setZarPaid] = useState<string>(""); // optional ZAR amount actually paid; "" means omitted
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState<string | null>(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"
/>
</div>
<div>
<label className="text-xs text-slate-500">ZAR Paid <span className="text-slate-400">(optional)</span></label>
<input
type="number"
step="0.01"
min="0"
inputMode="decimal"
value={zarPaid}
onChange={(e) => setZarPaid(e.target.value)}
placeholder="e.g. 12500.00"
className="w-full border rounded px-3 py-2 tabular-nums"
/>
</div>
{error && (
<div className="bg-red-50 border border-red-200 text-red-800 rounded p-3 text-sm">

View file

@ -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.