KLEINUILTIES-WEBAPP/js/invoice.js

278 lines
11 KiB
JavaScript
Raw Normal View History

/* 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 `<span class="invoice-status-pill ${s.cls}">${s.label}</span>`;
}
function paymentBlock(payments) {
if (!payments || payments.length === 0) return '';
const rows = payments.map((p) => `
<div class="invoice-total-row">
<span>${fmtDate(p.date)} · ${(p.method || '').toUpperCase()}</span>
<span>${fmtCurrency(p.amount)}</span>
</div>
`).join('');
const total = payments.reduce((s, p) => s + Number(p.amount || 0), 0);
return `
<div class="invoice-payment-info">
<div class="label">Payments received</div>
${rows}
<div class="invoice-total-row grand" style="border-top-color:#1F8A4D">
<span>Total received</span>
<span>${fmtCurrency(total)}</span>
</div>
</div>
`;
}
function renderInvoiceHTML({ invoice, parent, child, payments, business, type = 'invoice' }) {
const items = (invoice.items || []).map((it) => `
<tr>
<td class="desc">${escape(it.description)}</td>
<td style="text-align:center">${it.quantity || 1}</td>
<td class="amount">${fmtCurrency(it.unitPrice, business.currency)}</td>
<td class="amount">${fmtCurrency(it.total, business.currency)}</td>
</tr>
`).join('');
const dueText = invoice.dueDate ? `Due ${fmtDate(invoice.dueDate)}` : '';
const logoHtml = business.logoDataUrl
? `<div class="invoice-logo" style="background-image:url('${business.logoDataUrl.replace(/'/g, "\\'")}');background-size:cover;background-position:center"></div>`
: `<div class="invoice-logo">${window.SUIcons.brandOwl(92)}</div>`;
return `
<div class="invoice-doc">
<div class="invoice-header">
<div class="invoice-brand">
<div class="invoice-brand-info">
${logoHtml}
<div>
<h1 class="invoice-business-name">${escape(business.name)}</h1>
<span class="invoice-tagline">"${escape(business.tagline || 'A safe place to learn, grow and shine!')}"</span>
</div>
</div>
<div class="invoice-meta">
<div class="invoice-no">${escape(invoice.number || 'INVOICE')}</div>
<div class="invoice-meta-row"><strong>Issued:</strong> ${fmtDate(invoice.issueDate)}</div>
${dueText ? `<div class="invoice-meta-row"><strong>${escape(dueText)}</strong></div>` : ''}
${statusBadge(invoice.status)}
</div>
</div>
</div>
<h2 class="invoice-title">${type === 'statement' ? 'STATEMENT OF PAYMENTS RECEIVED' : 'INVOICE'}</h2>
<div class="invoice-body">
<div class="invoice-info-grid">
<div class="invoice-info-block">
<div class="label">Parent / Guardian</div>
<div class="value">${escape(parent.name)}</div>
${parent.phone ? `<div style="font-size:12px;color:var(--ink-soft);margin-top:2px">${escape(parent.phone)}</div>` : ''}
${parent.email ? `<div style="font-size:12px;color:var(--ink-soft)">${escape(parent.email)}</div>` : ''}
${parent.address ? `<div style="font-size:12px;color:var(--ink-soft)">${escape(parent.address)}</div>` : ''}
</div>
<div class="invoice-info-block right">
<div class="label">Child & Period</div>
<div class="value">${escape(child.name)}</div>
<div style="font-size:12px;color:var(--ink-soft);margin-top:2px">${child.plan === 'full' ? 'Full Day' : (child.plan === 'half' ? 'Half Day' : escape(child.plan))}</div>
<div style="font-size:12px;color:var(--ink-soft)">${formatPeriod(invoice.period)}</div>
</div>
</div>
${paymentBlock(payments)}
<table class="invoice-table">
<thead>
<tr>
<th style="width:55%">Description</th>
<th style="width:10%;text-align:center">Qty</th>
<th style="width:17.5%" class="amount">Unit</th>
<th style="width:17.5%" class="amount">Total</th>
</tr>
</thead>
<tbody>${items}</tbody>
</table>
<div class="invoice-totals">
<div class="invoice-totals-box">
<div class="invoice-total-row">
<span>Subtotal</span>
<span>${fmtCurrency(invoice.subtotal, business.currency)}</span>
</div>
${invoice.paid > 0 ? `
<div class="invoice-total-row">
<span>Paid</span>
<span style="color:#1F8A4D">− ${fmtCurrency(invoice.paid, business.currency)}</span>
</div>` : ''}
<div class="invoice-total-row grand">
<span>${invoice.balance > 0 ? 'Balance Due' : 'Total Paid'}</span>
<span>${fmtCurrency(invoice.balance > 0 ? invoice.balance : invoice.total, business.currency)}</span>
</div>
</div>
</div>
${invoice.notes ? `
<div class="invoice-notes">
<div class="label">Notes</div>
<div class="value">${escape(invoice.notes)}</div>
</div>` : ''}
${business.bankName || business.accountNumber ? `
<div class="invoice-notes">
<div class="label">Banking Details</div>
<div class="value">
${business.bankName ? `<div>Bank: <strong>${escape(business.bankName)}</strong></div>` : ''}
${business.accountHolder ? `<div>Account holder: <strong>${escape(business.accountHolder)}</strong></div>` : ''}
${business.accountNumber ? `<div>Account no: <strong>${escape(business.accountNumber)}</strong></div>` : ''}
${business.branchCode ? `<div>Branch code: <strong>${escape(business.branchCode)}</strong></div>` : ''}
${business.referenceNote ? `<div style="margin-top:4px">${escape(business.referenceNote)}</div>` : ''}
</div>
</div>` : ''}
</div>
<div class="invoice-footer">
${business.footerNote ? business.footerNote.split('\n').map((l) => `<div class="invoice-footer-row">${escape(l)}</div>`).join('') : ''}
<div class="invoice-footer-row" style="margin-top:8px">
${business.address ? escape(business.address) + ' · ' : ''}
${business.phone ? escape(business.phone) : ''}
${business.email ? ' · ' + escape(business.email) : ''}
</div>
${business.facebook ? `<div class="invoice-footer-row">${escape(business.facebook)}</div>` : ''}
</div>
</div>`;
}
// 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) => `
<tr>
<td>${fmtDate(p.date)}</td>
<td>${escape(p.childName || '')}</td>
<td>${escape((p.method || '').toUpperCase())}</td>
<td class="right">${fmtCurrency(p.amount, business.currency)}</td>
<td style="font-style:italic">${escape(p.receivedBy || '')}</td>
</tr>
`).join('');
return `
<div class="invoice-doc">
<div class="invoice-header">
<div class="invoice-brand">
<div class="invoice-brand-info">
<div class="invoice-logo">${window.SUIcons.brandOwl(92)}</div>
<div>
<h1 class="invoice-business-name">${escape(business.name)}</h1>
<span class="invoice-tagline">"${escape(business.tagline || 'A safe place to learn, grow and shine!')}"</span>
</div>
</div>
<div class="invoice-meta">
<div class="invoice-meta-label">Statement Period</div>
<div style="font-weight:700;font-size:13px">${formatPeriod(periodStart)} — ${formatPeriod(periodEnd)}</div>
</div>
</div>
</div>
<h2 class="invoice-title">STATEMENT OF THE PROOFS OF<br/><span class="invoice-title-pink">PAYMENTS RECEIVED</span></h2>
<div class="invoice-body">
<div class="invoice-info-grid">
<div class="invoice-info-block">
<div class="label">Parent / Guardian</div>
<div class="value">${escape(parent.name)}</div>
${parent.phone ? `<div style="font-size:12px;color:var(--ink-soft);margin-top:2px">${escape(parent.phone)}</div>` : ''}
${parent.email ? `<div style="font-size:12px;color:var(--ink-soft)">${escape(parent.email)}</div>` : ''}
</div>
<div class="invoice-info-block right">
<div class="label">Children</div>
${children.map((c) => `<div style="font-weight:700">${escape(c.name)}</div>`).join('')}
</div>
</div>
<table class="statement-table">
<thead>
<tr>
<th style="width:15%">Date</th>
<th style="width:25%">Child</th>
<th style="width:15%">Method</th>
<th style="width:20%" class="right">Amount</th>
<th style="width:25%">Received by</th>
</tr>
</thead>
<tbody>${rowsHtml || '<tr><td colspan="5" style="text-align:center;color:var(--ink-soft);padding:20px">No payments in this period</td></tr>'}</tbody>
</table>
<div style="text-align:right;margin-top:8px">
<span class="statement-total">TOTAL RECEIVED: ${fmtCurrency(total, business.currency)}</span>
</div>
</div>
<div class="invoice-footer">
${business.footerNote ? business.footerNote.split('\n').map((l) => `<div class="invoice-footer-row">${escape(l)}</div>`).join('') : ''}
<div class="invoice-footer-row" style="margin-top:8px">
${business.phone ? escape(business.phone) : ''}
${business.facebook ? ' · ' + escape(business.facebook) : ''}
</div>
</div>
</div>`;
}
function escape(s) {
return String(s ?? '').replace(/[&<>"']/g, (c) => ({
'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;'
}[c]));
}
window.SUInvoice = {
renderInvoiceHTML,
renderStatementHTML,
fmtCurrency,
fmtDate,
formatPeriod,
escape,
};
})();