mirror of
https://github.com/temetro/temetro.git
synced 2026-08-27 19:06:52 +00:00
frontend: invoice payment flow — pay, per-installment pay, due timeframe
- "Mark paid" button (settles invoice + all installments) when not paid/void - per-installment Pay button + Paid badge; auto-marks invoice paid when the last installment clears - overdue badge on past-due unpaid installments; due-through timeframe hint - installments + split control hidden once the invoice is paid All via the existing PUT /api/invoices/:id (no backend change). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -361,7 +361,16 @@
|
||||
"deleteFailedTitle": "Couldn't delete invoice",
|
||||
"deleteFailedBody": "Please try again.",
|
||||
"paid": "Paid",
|
||||
"unpaid": "Unpaid"
|
||||
"unpaid": "Unpaid",
|
||||
"markPaid": "Mark paid",
|
||||
"paidTitle": "Invoice marked paid",
|
||||
"payFailedTitle": "Couldn't record payment",
|
||||
"payFailedBody": "Please try again.",
|
||||
"payInstallment": "Pay",
|
||||
"installmentPaid": "Paid",
|
||||
"installmentPaidTitle": "Installment paid",
|
||||
"overdue": "Overdue",
|
||||
"timeframe": "{{count}} payments · due through {{date}}"
|
||||
}
|
||||
},
|
||||
"prescriptions": {
|
||||
|
||||
@@ -98,6 +98,55 @@ export function updateInvoice(
|
||||
});
|
||||
}
|
||||
|
||||
// Rebuild the editable input payload from a full invoice, so a partial change
|
||||
// (paying it, paying an installment) round-trips through PUT without dropping
|
||||
// any fields.
|
||||
function invoiceToInput(inv: Invoice): InvoiceInput {
|
||||
return {
|
||||
fileNumber: inv.fileNumber,
|
||||
name: inv.name,
|
||||
initials: inv.initials,
|
||||
number: inv.number,
|
||||
issuedAt: inv.issuedAt,
|
||||
dueAt: inv.dueAt,
|
||||
status: inv.status,
|
||||
lineItems: inv.lineItems,
|
||||
installments: inv.installments,
|
||||
notes: inv.notes,
|
||||
source: inv.source,
|
||||
};
|
||||
}
|
||||
|
||||
// Mark the whole invoice paid: status → paid and every installment settled.
|
||||
export function markInvoicePaid(inv: Invoice): Promise<Invoice> {
|
||||
return updateInvoice(inv.id, {
|
||||
...invoiceToInput(inv),
|
||||
status: "paid",
|
||||
installments: inv.installments.map((it) => ({ ...it, paid: true })),
|
||||
});
|
||||
}
|
||||
|
||||
// Settle a single installment. When that clears the last one, the invoice flips
|
||||
// to paid automatically.
|
||||
export function payInstallment(inv: Invoice, index: number): Promise<Invoice> {
|
||||
const installments = inv.installments.map((it, i) =>
|
||||
i === index ? { ...it, paid: true } : it,
|
||||
);
|
||||
const allPaid = installments.length > 0 && installments.every((it) => it.paid);
|
||||
return updateInvoice(inv.id, {
|
||||
...invoiceToInput(inv),
|
||||
installments,
|
||||
status: allPaid ? "paid" : inv.status,
|
||||
});
|
||||
}
|
||||
|
||||
// True when an unpaid installment's due date has passed.
|
||||
export function isInstallmentOverdue(it: InvoiceInstallment): boolean {
|
||||
if (it.paid || !it.dueAt) return false;
|
||||
const due = new Date(`${it.dueAt}T23:59:59`);
|
||||
return !Number.isNaN(due.getTime()) && due.getTime() < Date.now();
|
||||
}
|
||||
|
||||
export function splitInvoice(id: string, count: number): Promise<Invoice> {
|
||||
return apiFetch<Invoice>(`/api/invoices/${id}/split`, {
|
||||
method: "POST",
|
||||
|
||||
Reference in New Issue
Block a user