From 2acca18fc9eb480ab58f0015bde3ed948c392338 Mon Sep 17 00:00:00 2001 From: Thomas Joise Date: Fri, 10 Jul 2026 21:55:37 +0530 Subject: [PATCH] =?UTF-8?q?feat(hq-web):=20reports=20page=20=E2=80=94=20du?= =?UTF-8?q?es=20aging,=20module=20revenue,=20profitability,=20aws=20cost?= =?UTF-8?q?=20chart?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- apps/hq-web/src/Layout.tsx | 1 + apps/hq-web/src/api.ts | 49 +++++++++++++++ apps/hq-web/src/main.tsx | 2 + apps/hq-web/src/pages/Reports.tsx | 99 +++++++++++++++++++++++++++++++ 4 files changed, 151 insertions(+) create mode 100644 apps/hq-web/src/pages/Reports.tsx diff --git a/apps/hq-web/src/Layout.tsx b/apps/hq-web/src/Layout.tsx index ddde654..ed01b30 100644 --- a/apps/hq-web/src/Layout.tsx +++ b/apps/hq-web/src/Layout.tsx @@ -7,6 +7,7 @@ const NAV = [ { to: '/', label: 'Dashboard' }, { to: '/clients', label: 'Clients' }, { to: '/modules', label: 'Modules' }, + { to: '/reports', label: 'Reports' }, { to: '/documents/new', label: 'New Document' }, ] diff --git a/apps/hq-web/src/api.ts b/apps/hq-web/src/api.ts index 8535c4c..5e09ec3 100644 --- a/apps/hq-web/src/api.ts +++ b/apps/hq-web/src/api.ts @@ -276,6 +276,55 @@ export const createInteraction = (clientId: string, body: Record): Promise => apiFetch<{ interaction: Interaction }>(`/interactions/${id}`, { method: 'PATCH', body: JSON.stringify(body) }).then((r) => r.interaction) +// ---------- HQ-3a shapes ---------- + +export interface DuesAgingRow { + clientId: string; clientName: string + b0_30: number; b31_60: number; b61_90: number; b90p: number; totalPaise: number +} +export interface ModuleRevenueRow { + moduleId: string; moduleCode: string; moduleName: string; billedPaise: number; settledPaise: number +} +export interface ProfitabilityRow { + clientId: string; clientName: string + billedPaise: number; settledPaise: number; awsCostPaise: number; marginPaise: number +} +export interface CostRankRow { clientId: string; clientName: string; costPaise: number; sharePctBp: number } +export interface AwsUsageRow { + id: string; clientId: string; month: string + storageGb: number; transferGb: number; costPaise: number; source: 'auto' | 'manual'; updatedAt: string +} + +// ---------- HQ-3a calls ---------- + +const rangeQs = (from?: string, to?: string): string => { + const p = new URLSearchParams() + if (from !== undefined && from !== '') p.set('from', from) + if (to !== undefined && to !== '') p.set('to', to) + const q = p.toString() + return q === '' ? '' : `?${q}` +} + +export const getDuesAging = (): Promise => + apiFetch<{ rows: DuesAgingRow[] }>('/reports/dues-aging').then((r) => r.rows) +export const getModuleRevenue = (from?: string, to?: string): Promise => + apiFetch<{ rows: ModuleRevenueRow[] }>(`/reports/module-revenue${rangeQs(from, to)}`).then((r) => r.rows) +export const getProfitability = (from?: string, to?: string): Promise => + apiFetch<{ rows: ProfitabilityRow[] }>(`/reports/profitability${rangeQs(from, to)}`).then((r) => r.rows) +export const getAwsRanking = (month?: string): Promise<{ month: string; total: number; rows: CostRankRow[] }> => + apiFetch<{ month: string; total: number; rows: CostRankRow[] }>(`/aws/ranking${month !== undefined && month !== '' ? `?month=${month}` : ''}`) +export const getClientAwsUsage = (clientId: string): Promise => + apiFetch<{ usage: AwsUsageRow[] }>(`/clients/${clientId}/aws-usage`).then((r) => r.usage) +export const upsertAwsUsage = (body: Record): Promise => + apiFetch<{ usage: AwsUsageRow }>('/aws/usage', { method: 'POST', body: JSON.stringify(body) }).then((r) => r.usage) +export const pullAwsCosts = (month?: string): Promise<{ upserted: number; totalPaise: number; unknownTags: { tag: string; costPaise: number }[] }> => + apiFetch('/aws/pull', { method: 'POST', body: JSON.stringify(month !== undefined ? { month } : {}) }) + +export const getCompanyProfile = (): Promise> => + apiFetch<{ company: Record }>('/settings/company').then((r) => r.company) +export const putCompanyProfile = (body: Record): Promise> => + apiFetch<{ company: Record }>('/settings/company', { method: 'PUT', body: JSON.stringify(body) }).then((r) => r.company) + /** The PDF route needs the bearer header, which an