/* Slim Uiltjies — Invoice & Statement rendering
Builds the on-brand invoice / statement HTML that matches the original receipt.
*/
(function () {
'use strict';
const months = ['January','February','March','April','May','June','July','August','September','October','November','December'];
function fmtCurrency(n, currency = 'R') {
const v = Number(n) || 0;
return `${currency}${v.toLocaleString('en-ZA', { minimumFractionDigits: 0, maximumFractionDigits: 2 })}`;
}
function fmtDate(d) {
if (!d) return '';
const [y, m, day] = String(d).split('-').map(Number);
return `${day} ${months[m - 1]} ${y}`;
}
function formatPeriod(p) {
if (!p) return '';
const [y, m] = String(p).split('-').map(Number);
return `${months[m - 1]} ${y}`;
}
function statusBadge(status) {
const map = {
paid: { cls: 'invoice-status-paid', label: 'PAID' },
unpaid: { cls: 'invoice-status-unpaid', label: 'UNPAID' },
partial: { cls: 'invoice-status-partial', label: 'PARTIAL' },
};
const s = map[status] || map.unpaid;
return `${s.label}`;
}
function paymentBlock(payments) {
if (!payments || payments.length === 0) return '';
const rows = payments.map((p) => `
${fmtDate(p.date)} · ${(p.method || '').toUpperCase()}
${fmtCurrency(p.amount)}
`).join('');
const total = payments.reduce((s, p) => s + Number(p.amount || 0), 0);
return `
Payments received
${rows}
Total received
${fmtCurrency(total)}
`;
}
function renderInvoiceHTML({ invoice, parent, child, payments, business, type = 'invoice' }) {
const items = (invoice.items || []).map((it) => `
| ${escape(it.description)} |
${it.quantity || 1} |
${fmtCurrency(it.unitPrice, business.currency)} |
${fmtCurrency(it.total, business.currency)} |
`).join('');
const dueText = invoice.dueDate ? `Due ${fmtDate(invoice.dueDate)}` : '';
const logoHtml = business.logoDataUrl
? ``
: `${window.SUIcons.brandOwl(92)}
`;
return `
${type === 'statement' ? 'STATEMENT OF PAYMENTS RECEIVED' : 'INVOICE'}
Parent / Guardian
${escape(parent.name)}
${parent.phone ? `
${escape(parent.phone)}
` : ''}
${parent.email ? `
${escape(parent.email)}
` : ''}
${parent.address ? `
${escape(parent.address)}
` : ''}
Child & Period
${escape(child.name)}
${child.plan === 'full' ? 'Full Day' : (child.plan === 'half' ? 'Half Day' : escape(child.plan))}
${formatPeriod(invoice.period)}
${paymentBlock(payments)}
| Description |
Qty |
Unit |
Total |
${items}
Subtotal
${fmtCurrency(invoice.subtotal, business.currency)}
${invoice.paid > 0 ? `
Paid
− ${fmtCurrency(invoice.paid, business.currency)}
` : ''}
${invoice.balance > 0 ? 'Balance Due' : 'Total Paid'}
${fmtCurrency(invoice.balance > 0 ? invoice.balance : invoice.total, business.currency)}
${invoice.notes ? `
Notes
${escape(invoice.notes)}
` : ''}
${business.bankName || business.accountNumber ? `
Banking Details
${business.bankName ? `
Bank: ${escape(business.bankName)}
` : ''}
${business.accountHolder ? `
Account holder: ${escape(business.accountHolder)}
` : ''}
${business.accountNumber ? `
Account no: ${escape(business.accountNumber)}
` : ''}
${business.branchCode ? `
Branch code: ${escape(business.branchCode)}
` : ''}
${business.referenceNote ? `
${escape(business.referenceNote)}
` : ''}
` : ''}
`;
}
// Statement of payments received — modeled on the original receipt
function renderStatementHTML({ parent, children, payments, business, periodStart, periodEnd }) {
// Aggregate payments by child + period for display
const rows = [];
let total = 0;
const childMap = {};
children.forEach((c) => { childMap[c.id] = c; });
const sorted = payments.slice().sort((a, b) => (a.date || '').localeCompare(b.date || ''));
for (const p of sorted) {
const child = childMap[p.childName ? -1 : (p.invoiceChildId || 0)]; // we'll fall back below
// We need the child for each payment — we'll resolve by looking up the invoice
rows.push(p);
total += Number(p.amount) || 0;
}
const rowsHtml = rows.map((p) => `
| ${fmtDate(p.date)} |
${escape(p.childName || '')} |
${escape((p.method || '').toUpperCase())} |
${fmtCurrency(p.amount, business.currency)} |
${escape(p.receivedBy || '')} |
`).join('');
return `
STATEMENT OF THE PROOFS OF
PAYMENTS RECEIVED
Parent / Guardian
${escape(parent.name)}
${parent.phone ? `
${escape(parent.phone)}
` : ''}
${parent.email ? `
${escape(parent.email)}
` : ''}
Children
${children.map((c) => `
${escape(c.name)}
`).join('')}
| Date |
Child |
Method |
Amount |
Received by |
${rowsHtml || '| No payments in this period |
'}
TOTAL RECEIVED: ${fmtCurrency(total, business.currency)}
`;
}
function escape(s) {
return String(s ?? '').replace(/[&<>"']/g, (c) => ({
'&': '&', '<': '<', '>': '>', '"': '"', "'": '''
}[c]));
}
window.SUInvoice = {
renderInvoiceHTML,
renderStatementHTML,
fmtCurrency,
fmtDate,
formatPeriod,
escape,
};
})();