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