// FRAZIER Finance OS — Invoices (Phase 2 demo). FRONTEND-ONLY: no
// fin_invoice_* backend exists yet, so this seeds a realistic in-memory
// dummy dataset below (clearly labeled, same pattern as FIN_HOME_MOCK in
// fin-home.jsx) and operates entirely on local React state — no finApi/
// fetch calls. A real backend swap is a later phase; the UI/behavior built
// here should not need to change shape when that lands.
const FIN_INV_CLIENTS_SEED = [
{ id: 'c1', name: 'Kopi Kenangan' },
{ id: 'c2', name: 'Sociolla' },
{ id: 'c3', name: 'Erigo' },
{ id: 'c4', name: 'Wardah Beauty' },
{ id: 'c5', name: 'Ternak Uang Media' },
{ id: 'c6', name: 'Bank Jago Digital' },
];
const FIN_INV_SEED = [
{ id: 'i1', clientId: 'c1', number: 'FRZ/INV/2026/09/0041', service: 'Social Media Management — September', amount: 32_000_000, discount: 0, tax: 11, issueDate: '2026-09-01', dueDate: '2026-09-20', status: 'overdue', notes: '' },
{ id: 'i2', clientId: 'c2', number: 'FRZ/INV/2026/09/0042', service: 'Campaign Production — Ramadan Refresh', amount: 58_500_000, discount: 5, tax: 11, issueDate: '2026-09-05', dueDate: '2026-09-28', status: 'overdue', notes: '' },
{ id: 'i3', clientId: 'c3', number: 'FRZ/INV/2026/09/0043', service: 'Content Shoot — Lookbook Vol. 4', amount: 24_750_000, discount: 0, tax: 11, issueDate: '2026-08-28', dueDate: '2026-09-15', status: 'paid', notes: '' },
{ id: 'i4', clientId: 'c4', number: 'FRZ/INV/2026/08/0039', service: 'Influencer Management — August', amount: 45_000_000, discount: 0, tax: 11, issueDate: '2026-08-10', dueDate: '2026-08-30', status: 'paid', notes: '' },
{ id: 'i5', clientId: 'c5', number: 'FRZ/INV/2026/10/0044', service: 'Podcast Editing Retainer — October', amount: 18_200_000, discount: 0, tax: 11, issueDate: '2026-10-01', dueDate: '2026-10-20', status: 'sent', notes: '' },
{ id: 'i6', clientId: 'c6', number: 'FRZ/INV/2026/10/0045', service: 'Brand Campaign — Q4 Launch', amount: 60_000_000, discount: 10, tax: 11, issueDate: '2026-10-05', dueDate: '2026-10-25', status: 'ready', notes: '' },
{ id: 'i7', clientId: 'c1', number: 'FRZ/INV/2026/10/0046', service: 'TikTok Series — November batch', amount: 15_000_000, discount: 0, tax: 11, issueDate: '2026-10-05', dueDate: '2026-11-05', status: 'draft', notes: '' },
{ id: 'i8', clientId: 'c2', number: 'FRZ/INV/2026/10/0047', service: 'Photo Retouching Batch 12', amount: 21_000_000, discount: 0, tax: 11, issueDate: '2026-10-06', dueDate: '2026-11-06', status: 'draft', notes: '' },
];
const FIN_INV_STATUS = {
draft: { label: 'Draft', tone: 'neutral' },
ready: { label: 'Ready to Send', tone: 'info' },
sent: { label: 'Sent', tone: 'info' },
paid: { label: 'Paid', tone: 'ok' },
overdue: { label: 'Overdue', tone: 'crit' },
};
const FIN_INV_FILTERS = [
{ id: 'needs', label: 'Needs Action', test: (s) => s === 'draft' },
{ id: 'ready', label: 'Ready to Send', test: (s) => s === 'ready' },
{ id: 'sent', label: 'Sent', test: (s) => s === 'sent' },
{ id: 'paid', label: 'Paid', test: (s) => s === 'paid' },
{ id: 'overdue', label: 'Overdue', test: (s) => s === 'overdue' },
{ id: 'all', label: 'All', test: () => true },
];
function finInvDateFmt(iso) {
if (!iso) return '—';
const d = new Date(iso + 'T00:00:00');
if (isNaN(d.getTime())) return iso;
return d.toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' });
}
// Simple sequential number — good enough for the demo dataset; a real
// backend would own the counter + year/month bucketing server-side.
function finInvNextNumber(invoices, issueDateIso) {
const d = issueDateIso ? new Date(issueDateIso + 'T00:00:00') : new Date();
const base = isNaN(d.getTime()) ? new Date() : d;
const y = base.getFullYear();
const m = String(base.getMonth() + 1).padStart(2, '0');
const seq = String(invoices.length + 1).padStart(4, '0');
return `FRZ/INV/${y}/${m}/${seq}`;
}
function finInvTotals(amount, discountPct, taxPct) {
const subtotal = Number(amount) || 0;
const discountAmt = subtotal * (Number(discountPct) || 0) / 100;
const afterDiscount = subtotal - discountAmt;
const taxAmt = afterDiscount * (Number(taxPct) || 0) / 100;
return { subtotal, discountAmt, afterDiscount, taxAmt, total: afterDiscount + taxAmt };
}
function finInvDaysOverdue(dueIso) {
const d = new Date(dueIso + 'T00:00:00');
if (isNaN(d.getTime())) return 0;
return Math.max(0, Math.round((Date.now() - d.getTime()) / 86400000));
}
function finInvNewDraft() {
return {
step: 1,
clientId: null,
clientSearch: '',
addingClient: false,
newClientName: '',
service: '',
amount: '',
discount: '0',
tax: '11',
issueDate: new Date().toISOString().slice(0, 10),
dueDate: '',
notes: '',
};
}
function FinInvPill({ status }) {
const meta = FIN_INV_STATUS[status] || FIN_INV_STATUS.draft;
return
No clients match "{wizard.clientSearch}".
: null}{wizard.notes}
: null}No invoices in this view.