feat(reports): Financial report (Initiative 1 Phase 4)

Builds the Financial report on the canonical payments + expenses tables
(the CRM records money received; it does not invoice — invoices module
is off, dev DB has zero invoice rows). The invoice-centric spec is
reframed onto the payments model: "outstanding AR" → expected-deposit
shortfall on active deals; "AR aging" → outstanding deposits bucketed by
deal age.

Service (financial.service.ts):
- 7 KPIs: revenue collected (net of refunds), deposits, balance,
  pipeline expected, outstanding deposits, expenses, net contribution
- 6 chart datasets: revenue by month (deposit/balance), collection
  funnel (EOI→deposit→contract→won), expected-deposit aging, cash flow
  (inflow vs outflow), expense breakdown by category
- 4 tables: outstanding deposits, recent payments, refund log, expense
  ledger
- every money figure normalised to port currency via a shared
  resolvePortCurrency/normalizeAmount helper (new reports/currency.ts)

UI (financial-report-client.tsx): KPI strip + recharts (stacked bar /
horizontal bar / line / donut) + month/quarter/year toggle + branded
empty states; DateRangePicker + Templates + Export wired. Un-hidden the
Financial card on the reports landing.

Plumbing: added '1y' (trailing 12mo) preset to the shared range system
(financial trends want a year); added 'financial'/'marketing' to the
report-template kind enum for template parity.

TDD: 6 financial-math unit tests (aging buckets, month keys/range, net
contribution). tsc clean; full unit suite green except pre-existing
Redis/storage-dependent integration tests. Browser-verified against live
data: API 200, KPIs correct ($5,849 expenses / -$5,849 net, $0 revenue
correct given 0 payment rows), expense ledger + breakdown populate,
payment-derived sections show graceful empty states.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-02 00:43:36 +02:00
parent 75fdb9fab4
commit b690fb8d56
14 changed files with 1769 additions and 8 deletions

View File

@@ -12,7 +12,7 @@
/**
* Preset date ranges used by the dashboard's quick-pick tabs.
*/
export type PresetDateRange = '7d' | '30d' | '90d' | 'today';
export type PresetDateRange = '7d' | '30d' | '90d' | '1y' | 'today';
/**
* A custom date range expressed as a pair of ISO date strings (YYYY-MM-DD).
@@ -27,7 +27,7 @@ export interface CustomDateRange {
export type DateRange = PresetDateRange | CustomDateRange;
export const ALL_RANGES: readonly PresetDateRange[] = ['today', '7d', '30d', '90d'] as const;
export const ALL_RANGES: readonly PresetDateRange[] = ['today', '7d', '30d', '90d', '1y'] as const;
export function isCustomRange(range: DateRange): range is CustomDateRange {
return typeof range === 'object' && range.kind === 'custom';
@@ -87,6 +87,8 @@ export function rangeToDays(range: PresetDateRange): number {
return 30;
case '90d':
return 90;
case '1y':
return 365;
}
}