A production-quality front end for a non-profit financial-management module whose entire job is to produce five trustworthy, auditable headline numbers — Total Raised, Expensed, Blocked, Reserved and Free Cash — plus the workflows and drill-downs that make those numbers true.
Built to the NPO Financial Management Module — Design & Development Specification.
The original mockup computed Free Cash = Raised − Expenses. That is wrong —
it overstates spendable cash by ignoring money that is legally or strategically
off-limits. The only correct derivation, implemented everywhere here, is:
Free Cash = Total Raised − Expensed − Blocked − Reserved
- Blocked = unspent donor-restricted balances (
restricted donations − restricted expenses) - Reserved = board-designated amounts (
designations + allocations − un-reserves − reserve spend) - Non-cash = in-kind fair value (an extra
− NonCashterm so in-kind, which is raised but never cash, can't inflate Free Cash; it is0in the baseline)
No number on screen is hand-keyed or stored denormalised. Every headline, project
and liquidity figure is a pure function over one ledger. Each journal line is
tagged with a fund_bucket and a cash_account_id, so the bucket view and the
liquidity view are just different group-bys of the same data — no parallel
bookkeeping (§7). The derivation logic lives in a framework-agnostic module
(src/domain) with the UI talking to it only through a swappable DataProvider.
Free Cash is derived two independent ways and they must agree:
- Formula:
Raised − Expensed − Blocked − Reserved − NonCash - Cash identity:
(Σ cash-account current balances) − Blocked − Reserved
If they disagree the app shows a loud “ledger out of balance” alert instead of silently displaying a number. Flip the Out-of-balance ledger scenario on the dashboard to see it fire.
Every headline number is clickable and lands on the exact filtered list that produces it — e.g. Blocked → unspent restricted balances by project. This is what makes the dashboard trustworthy to an accountant.
Pending/bounced donations excluded until cleared · refunds that reverse a donation and restore bucket + account · restricted overspend that warns and requires explicit approval · negative Free Cash shown in red with an explanation (never hidden) · in-kind donations recorded at fair value but flagged and excluded from cash · multi-currency modelled in the types (PKR rendered in v1).
The first four are switchable live via the scenario switcher on the dashboard; pending/bounced/refunded/draft records are present in the baseline (and correctly excluded) without moving the headline numbers.
- React 19 + TypeScript (strict) + Vite
- Tailwind CSS v4
- Vitest for the derivation & reconciliation tests
- All monetary math in integer minor units (no floats), currency-aware types
src/
domain/ # pure, framework-agnostic — the source of truth
money.ts integer minor-unit money (multi-currency-ready)
types.ts the six entities (§3) + buckets (§4) + journal (§7)
predicates.ts the single definition of what "counts" (§9)
ledger.ts double-entry journal + cash balances (§7, §6.2)
derive.ts §8 formulas + per-project + drill-down breakdowns
reconcile.ts §8/§10 reconciliation guard
seed.ts mock data reconciling exactly to §6.1, + scenarios
*.test.ts 31 unit tests of the correctness claims
data/ # the swappable service seam
DataProvider.ts interface
InMemoryDataProvider.ts implementation over the seed
context.tsx React context + scenario switch
components/ # UI surfaces (§6)
ui/ # formatting helpers + design primitives
npm install
npm run dev # start the app
npm test # run the derivation/reconciliation suite
npm run build # typecheck + production buildThe seed reconciles exactly to the spec example:
| Total Raised | Expensed | Blocked | Reserved | Free Cash |
|---|---|---|---|---|
| 800,000 | 320,000 | 150,000 | 80,000 | 250,000 |
Liquidity: lifetime raised Cash 200,000 + Bank 600,000 = 800,000; current
balances 480,000 after 320,000 spent — and 480,000 − 150,000 − 80,000 = 250,000,
matching the formula.