/* TMD Portal — money screens + settings. */ const uSt = React.useState; /* ================= Expenses ================= */ function Expenses({ app }) { const [add, setAdd] = uSt(false); const [cat, setCat] = uSt('all'); const [cl, setCl] = uSt('all'); const [paid, setPaid] = uSt('all'); const [when, setWhen] = uSt('all'); const cats = ['Ad spend', 'Production', 'Software', 'Freelance', 'Print', 'Travel']; const list = app.expenses.filter(e => (cat === 'all' || e.cat === cat) && (cl === 'all' || e.client === cl) && (paid === 'all' || e.paidBy === paid) && (when === 'all' || (when === '7' ? dayDiff(e.date) >= -7 : when === 'aug' ? e.date >= monthStart(0) : e.date >= monthStart(-1) && e.date <= monthEnd(-1)))); const total = list.filter(e => e.paidBy === 'TMD').reduce((s, e) => s + e.amount, 0); const billable = list.filter(e => e.billable).reduce((s, e) => s + e.amount, 0); return (
setAdd(true)}>Add expense} />
({ v: c, l: c })))} /> ({ v: c.id, l: c.name })))} />
{!list.length ? setAdd(true)}>Add expense} /> : app.mobile ? (
{list.map(e => (
{fmtDate(e.date)}{money(e.amount, { bare: true })} {e.desc} {e.cat}{e.client ? client(e.client).name : 'Overhead'}{e.billable ? Rebill : null}
))}
) : ( {list.map(e => ( ))}
DateExpenseCategoryClientPaid byRebillAmount
{fmtDate(e.date)}{e.desc}{e.cat} {e.client ? client(e.client).name : Overhead}{e.paidBy} {e.billable ? Rebill : —}{money(e.amount, { bare: true })}
)} setAdd(false)} app={app} />
); } function AddExpense({ open, onClose, app }) { const [amt, setAmt] = uSt(''); const [cat, setCat] = uSt(''); const [cl, setCl] = uSt(''); const [paidBy, setPaidBy] = uSt('TMD'); const [bill, setBill] = uSt(true); const [date, setDate] = uSt(T.TODAY); const cats = ['Ad spend', 'Production', 'Software', 'Freelance', 'Print', 'Travel']; const ready = amt && cat; return ( Cancel { app.addExpense({ amount: Number(amt), cat, client: cl || null, paidBy, billable: bill, date }); setAmt(''); setCat(''); setCl(''); setDate(T.TODAY); onClose(); }}>Log expense}>
Category
{cats.map(c => )}
Client
{T.clients.map(c => )}
Paid by
Date
{[{ v: T.TODAY, l: 'Today' }, { v: isoShift(-1), l: 'Yesterday' }].map(o => )} setDate(e.target.value)} />

Nothing is stored here but the link.

); } /* ================= Invoices ================= */ const invTotal = i => i.lines.reduce((s, l) => s + l.amount, 0); const invPaid = i => i.payments.reduce((s, p) => s + p.amount, 0); function Invoices({ app }) { const [batch, setBatch] = uSt(false); const list = app.invoices; const out = list.reduce((s, i) => s + (i.status === 'draft' ? 0 : invTotal(i) - invPaid(i)), 0); const drafts = list.filter(i => i.status === 'draft').length; return (
setBatch(true)}>Review {monthLabel(0)} drafts{T.clients.filter(c => c.status === 'Active').length}} /> {drafts ?
{drafts} draft{drafts > 1 ? 's' : ''} from {monthLabel(-1)} still unissued.Drafts are editable. Issued invoices are not.
: null} {list.map(i => ( app.go('inv:' + i.id)} tabIndex={0}> ))}
NumberClientPeriodStatusDueTotalOutstanding
{i.no}{client(i.client).name}{i.period} {i.status} {i.due ? : —} {money(invTotal(i), { bare: true })} {i.status === 'paid' ? — : money(invTotal(i) - invPaid(i), { bare: true })}
setBatch(false)} app={app} />
); } function InvoiceDetail({ app, id }) { const i = byId(app.invoices, id); const [confirm, setConfirm] = uSt(false); const [pay, setPay] = uSt(false); if (!i) return ; const draft = i.status === 'draft'; const total = invTotal(i), paid = invPaid(i); return (
Edit lines setConfirm(true)}>Review and issue : <> setPay(true)}>Record paymentIssue credit noteVoid} /> {!draft ?
Issued {fmtDate(i.issued, true)} — this invoice cannot be edited.Corrections are made with a credit note, so the client's copy and yours never disagree.
: null}

The Marketing Desk (Pvt) Ltd · Colombo 05 · Sri Lanka

{draft ? 'Draft' : i.no}
Issued {i.issued ? fmtDate(i.issued, true) : '—'}
Due {i.due ? fmtDate(i.due, true) : '—'}
{i.lines.map((l, n) => )} {paid ? : null}
DescriptionAmount
{l.desc}{money(l.amount, { bare: true })}
Total{money(total, { bare: true })}
Paid− {money(paid, { bare: true })}
{paid ? 'Balance' : 'Due'}{money(total - paid, { bare: true })}

No tax is charged. TMD is not tax-registered. Payment to Sampath Bank · 0071 2345 6789.

Payments {i.payments.length ? {i.payments.map((p, n) => )}
DateReferenceAmount
{fmtDate(p.date, true)}{p.ref}{money(p.amount, { bare: true })}
: }

{client(i.client).name} · {i.period} · {money(total)}

Once issued, this invoice is locked. No line can be changed, no amount corrected, no date moved. A mistake after this point is fixed with a credit note, and both documents stay on the record.

} onClose={() => setConfirm(false)} onConfirm={() => { setConfirm(false); app.issue([i.id]); }} /> setPay(false)} title="Record payment" footer={<> setPay(false)}>Cancel { setPay(false); app.toast('Payment recorded'); }}>Record payment}>
); } function BatchIssue({ open, onClose, app }) { const [step, setStep] = uSt(1); const [sel, setSel] = uSt(T.clients.map(c => c.id)); const [confirm, setConfirm] = uSt(false); const rows = T.clients.map(c => { const rebill = T.expenses.filter(e => e.client === c.id && e.billable).reduce((s, e) => s + e.amount, 0); return { c, retainer: c.retainer, rebill, total: c.retainer + rebill }; }); const chosen = rows.filter(r => sel.includes(r.c.id)); const sum = chosen.reduce((s, r) => s + r.total, 0); const toggle = id => setSel(s => s.includes(id) ? s.filter(x => x !== id) : s.concat(id)); return ( { setStep(1); onClose(); }} title={step === 1 ? monthLabel(0) + ' drafts — review' : 'Issue ' + chosen.length + ' invoices'} footer={step === 1 ? <>Close setStep(2)}>Continue — {chosen.length} selected : <> setStep(1)}>Back setConfirm(true)}>Issue {chosen.length} invoices}> {step === 1 ? ( <>

The desk drafted one invoice per active client for {monthLabel(0)}: the retainer in force on the last day of the month, plus every expense marked rebillable. Uncheck anything that should not go out.

{rows.map(r => ( ))}
Total to issue{money(sum)}
) : ( <>
This is the door.

Issuing sends {chosen.length} invoices worth {money(sum)} into the record. Each one is locked from that moment: no edits, no deletions, no quiet corrections. Only payments, voids and credit notes afterwards.

Numbers are never reused.

)} This cannot be undone. {money(sum)} across {chosen.length} clients becomes permanent record.

} onClose={() => setConfirm(false)} onConfirm={() => { setConfirm(false); setStep(1); onClose(); app.toast(chosen.length + ' invoices issued'); }} />
); } /* ================= Client P&L ================= */ function PnL({ app }) { const rows = T.clients.map(c => { const rev = c.retainer; const exp = T.expenses.filter(e => e.client === c.id && e.paidBy === 'TMD').reduce((s, e) => s + e.amount, 0); const share = app.dels.filter(d => d.client === c.id).length; const totalWork = app.dels.length; const peopleCost = Math.round((T.people.reduce((s, p) => s + (p.cost || 0), 0)) * (share / totalWork)); const margin = rev - exp - peopleCost; return { c, rev, exp, peopleCost, margin, share }; }); const tot = rows.reduce((a, r) => ({ rev: a.rev + r.rev, exp: a.exp + r.exp, pc: a.pc + r.peopleCost, m: a.m + r.margin }), { rev: 0, exp: 0, pc: 0, m: 0 }); return (
{rows.map(r => ( app.go('client:' + r.c.id)} tabIndex={0}> ))}
ClientRetainerTMD-paid expensesPeople cost est.Margin%
{r.c.name} {money(r.rev, { bare: true })} {money(r.exp, { bare: true })} {money(r.peopleCost, { bare: true })} {money(r.margin, { bare: true })} {Math.round(r.margin / r.rev * 100)}%
All clients{money(tot.rev, { bare: true })}{money(tot.exp, { bare: true })}{money(tot.pc, { bare: true })}{money(tot.m, { bare: true })}{Math.round(tot.m / tot.rev * 100)}%

People cost is an estimate. It splits salaries by share of deliverables this month, not by hours — the desk does not track hours. Treat the ranking as real and the exact figure as approximate.

); } /* ================= Settings — pipeline editor ================= */ function Pipelines({ app }) { const [tid, setTid] = uSt('reel'); const t = typeOf(tid); const [stages, setStages] = uSt(t.stages); React.useEffect(() => { setStages(typeOf(tid).stages); }, [tid]); const move = (i, dir) => setStages(s => { const n = s.slice(); const j = i + dir; if (j < 0 || j >= n.length) return s; const x = n[i]; n[i] = n[j]; n[j] = x; return n; }); const setSem = (i, sem) => setStages(s => s.map((x, n) => n === i ? Object.assign({}, x, { sem }) : x)); const inUse = app.dels.filter(d => d.type === tid).length; return (
{T.types.map(x => )}
{inUse} live deliverables use this pipeline.Renaming a stage renames it on their cards. Deleting one asks where its work should go first.
StageMaps toApprovalOrder
{stages.map((s, i) => (
{['todo', 'doing', 'waiting', 'done', 'blocked'].map(k => ( ))}
{s.approval ? {s.approval === 'client' ? 'Client' : 'Internal'} : None}
))}
Add stage app.toast('Pipeline saved', true)}>Save pipeline
The five states
{['todo', 'doing', 'waiting', 'done', 'blocked'].map(k => (

{{ todo: 'Requested, queued, briefed — real work has not started.', doing: 'Someone at the desk is working on it right now.', waiting: 'Nothing can move until a person outside the work responds. The loudest state on purpose — this is where agency work dies.', done: 'Published, delivered, live. It leaves the board.', blocked: 'Stopped by something that is not a queue: no consent, no budget, no rate card.' }[k]}

))}
); } Object.assign(window, { Expenses, AddExpense, Invoices, InvoiceDetail, BatchIssue, PnL, Pipelines, invTotal, invPaid });