From 9e3886d0bef4f5eed2c8b249581a851101f491f1 Mon Sep 17 00:00:00 2001 From: Thomas Joise Date: Fri, 10 Jul 2026 15:55:13 +0530 Subject: [PATCH] feat(hq-web): dashboard money view with manual reminder queue as default route Co-Authored-By: Claude Fable 5 --- apps/hq-web/src/Layout.tsx | 3 +- apps/hq-web/src/api.ts | 82 +++++++++++++++++++ apps/hq-web/src/main.tsx | 4 +- apps/hq-web/src/pages/Dashboard.tsx | 120 ++++++++++++++++++++++++++++ 4 files changed, 207 insertions(+), 2 deletions(-) create mode 100644 apps/hq-web/src/pages/Dashboard.tsx diff --git a/apps/hq-web/src/Layout.tsx b/apps/hq-web/src/Layout.tsx index ecd14b2..ddde654 100644 --- a/apps/hq-web/src/Layout.tsx +++ b/apps/hq-web/src/Layout.tsx @@ -4,7 +4,8 @@ import { Badge, ThemeSwitcher } from '@sims/ui' import { clearSession, displayName, getEmailStatus, hasSession } from './api' const NAV = [ - { to: '/', label: 'Clients' }, + { to: '/', label: 'Dashboard' }, + { to: '/clients', label: 'Clients' }, { to: '/modules', label: 'Modules' }, { to: '/documents/new', label: 'New Document' }, ] diff --git a/apps/hq-web/src/api.ts b/apps/hq-web/src/api.ts index d157cd6..8535c4c 100644 --- a/apps/hq-web/src/api.ts +++ b/apps/hq-web/src/api.ts @@ -194,6 +194,88 @@ export const recordPayment = (body: Record): Promise = export const getLedger = (clientId: string): Promise => apiFetch(`/clients/${clientId}/ledger`) +// ---------- HQ-2 shapes ---------- + +export type Cadence = 'monthly' | 'yearly' +export interface RecurringPlan { + id: string; clientId: string; clientModuleId: string | null; cadence: Cadence + amountPaise: number | null; nextRun: string; policy: 'auto' | 'manual'; active: boolean +} + +export type AmcPaidStatus = 'paid' | 'unpaid' | 'unbilled' +export interface AmcContract { + id: string; clientId: string; coverage: string; periodFrom: string; periodTo: string + amountPaise: number; renewalReminderDays: number; legacyPaid: boolean | null + invoiceDocId: string | null; active: boolean; paidStatus: AmcPaidStatus +} + +export interface InteractionType { code: string; label: string } +export type Outcome = 'positive' | 'neutral' | 'negative' +export interface Interaction { + id: string; clientId: string; typeCode: string; onDate: string; staffId: string + notes: string; outcome: Outcome | null; followUpOn: string | null; createdAt: string +} + +export type ReminderRuleKind = + | 'invoice_overdue' | 'renewal_due' | 'amc_expiring' | 'follow_up' | 'recurring_generated' | 'email_bounced' +export type ReminderStatus = 'queued' | 'sent' | 'failed' | 'dismissed' +export interface Reminder { + id: string; ruleKind: ReminderRuleKind; subjectId: string; duePeriod: string; clientId: string + docId: string | null; status: ReminderStatus; policyApplied: 'auto' | 'manual' + error: string | null; createdAt: string; sentAt: string | null +} + +export interface DashboardView { + today: string + overdue: { docId: string; docNo: string | null; clientId: string; clientName: string; outstandingPaise: number; daysOverdue: number }[] + dueThisWeek: { planId: string; clientId: string; clientName: string; nextRun: string; amountPaise: number | null }[] + renewalsThisMonth: { clientModuleId: string; clientId: string; clientName: string; nextRenewal: string }[] + followUpsToday: { id: string; clientId: string; clientName: string; onDate: string; followUpOn: string; notes: string }[] + recentPayments: { id: string; clientId: string; clientName: string; receivedOn: string; amountPaise: number; mode: string }[] + queue: (Reminder & { clientName: string; docNo: string | null })[] + totals: { overduePaise: number; queued: number; failed: number } +} + +// ---------- HQ-2 calls ---------- + +export const getDashboard = (): Promise => + apiFetch<{ view: DashboardView }>('/dashboard').then((r) => r.view) +export const getReminders = (status?: ReminderStatus): Promise => + apiFetch<{ reminders: Reminder[] }>(`/reminders${status !== undefined ? `?status=${status}` : ''}`).then((r) => r.reminders) +export const sendReminder = (id: string): Promise => + apiFetch<{ reminder: Reminder }>(`/reminders/${id}/send`, { method: 'POST', body: '{}' }).then((r) => r.reminder) +export const dismissReminder = (id: string): Promise => + apiFetch<{ reminder: Reminder }>(`/reminders/${id}/dismiss`, { method: 'POST', body: '{}' }).then((r) => r.reminder) + +export const getRecurringPlans = (clientId: string): Promise => + apiFetch<{ plans: RecurringPlan[] }>(`/recurring?clientId=${clientId}`).then((r) => r.plans) +export const createRecurringPlan = (clientId: string, body: Record): Promise => + apiFetch<{ plan: RecurringPlan }>(`/clients/${clientId}/recurring`, { method: 'POST', body: JSON.stringify(body) }).then((r) => r.plan) +export const updateRecurringPlan = (id: string, body: Record): Promise => + apiFetch<{ plan: RecurringPlan }>(`/recurring/${id}`, { method: 'PATCH', body: JSON.stringify(body) }).then((r) => r.plan) +export const deactivateRecurringPlan = (id: string): Promise => + apiFetch<{ plan: RecurringPlan }>(`/recurring/${id}/deactivate`, { method: 'POST', body: '{}' }).then((r) => r.plan) + +export const getAmc = (clientId: string): Promise => + apiFetch<{ contracts: AmcContract[] }>(`/clients/${clientId}/amc`).then((r) => r.contracts) +export const createAmc = (clientId: string, body: Record): Promise => + apiFetch<{ contract: AmcContract }>(`/clients/${clientId}/amc`, { method: 'POST', body: JSON.stringify(body) }).then((r) => r.contract) +export const updateAmc = (id: string, body: Record): Promise => + apiFetch<{ contract: AmcContract }>(`/amc/${id}`, { method: 'PATCH', body: JSON.stringify(body) }).then((r) => r.contract) +export const deactivateAmc = (id: string): Promise => + apiFetch<{ contract: AmcContract }>(`/amc/${id}/deactivate`, { method: 'POST', body: '{}' }).then((r) => r.contract) +export const generateAmcRenewalInvoice = (id: string): Promise => + apiFetch<{ document: Doc }>(`/amc/${id}/renewal-invoice`, { method: 'POST', body: '{}' }).then((r) => r.document) + +export const getInteractionTypes = (): Promise => + apiFetch<{ types: InteractionType[] }>('/interaction-types').then((r) => r.types) +export const getInteractions = (clientId: string): Promise => + apiFetch<{ interactions: Interaction[] }>(`/clients/${clientId}/interactions`).then((r) => r.interactions) +export const createInteraction = (clientId: string, body: Record): Promise => + apiFetch<{ interaction: Interaction }>(`/clients/${clientId}/interactions`, { method: 'POST', body: JSON.stringify(body) }).then((r) => r.interaction) +export const updateInteraction = (id: string, body: Record): Promise => + apiFetch<{ interaction: Interaction }>(`/interactions/${id}`, { method: 'PATCH', body: JSON.stringify(body) }).then((r) => r.interaction) + /** The PDF route needs the bearer header, which an