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:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user