From 8c145d5a3b6edb426e6389b0ab50f7fde8fd1eda Mon Sep 17 00:00:00 2001 From: Thomas Joise Date: Fri, 10 Jul 2026 10:00:31 +0530 Subject: [PATCH] feat(hq-web): clients, modules, composer and document pages (HQ-1 task 16) Co-Authored-By: Claude Fable 5 --- apps/hq-web/src/api.ts | 142 ++++++++++++++ apps/hq-web/src/main.tsx | 22 +-- apps/hq-web/src/pages/ClientDetail.tsx | 261 +++++++++++++++++++++++++ apps/hq-web/src/pages/Clients.tsx | 118 +++++++++++ apps/hq-web/src/pages/DocumentView.tsx | 193 ++++++++++++++++++ apps/hq-web/src/pages/Modules.tsx | 157 +++++++++++++++ apps/hq-web/src/pages/NewDocument.tsx | 214 ++++++++++++++++++++ 7 files changed, 1096 insertions(+), 11 deletions(-) create mode 100644 apps/hq-web/src/pages/ClientDetail.tsx create mode 100644 apps/hq-web/src/pages/Clients.tsx create mode 100644 apps/hq-web/src/pages/DocumentView.tsx create mode 100644 apps/hq-web/src/pages/Modules.tsx create mode 100644 apps/hq-web/src/pages/NewDocument.tsx diff --git a/apps/hq-web/src/api.ts b/apps/hq-web/src/api.ts index 5982a5f..167fee3 100644 --- a/apps/hq-web/src/api.ts +++ b/apps/hq-web/src/api.ts @@ -61,3 +61,145 @@ export async function login(email: string, password: string): Promise<{ displayN export interface EmailStatus { connected: boolean; address?: string; dead: boolean } export const getEmailStatus = (): Promise => apiFetch('/email/status') + +// ---------- shared server shapes (mirror apps/hq repos — camelCase JSON) ---------- + +export interface ClientContact { name: string; phone?: string; email?: string; role?: string } + +export type ClientStatus = 'lead' | 'active' | 'dormant' | 'lost' +export const CLIENT_STATUSES: ClientStatus[] = ['lead', 'active', 'dormant', 'lost'] + +export interface Client { + id: string; code: string; name: string; gstin?: string; stateCode: string + address: string; contacts: ClientContact[]; status: ClientStatus; notes: string +} + +export type Kind = 'one_time' | 'monthly' | 'yearly' | 'usage' +export const KIND_LABEL: Record = { + one_time: 'One-time', monthly: 'Monthly', yearly: 'Yearly', usage: 'Usage', +} + +export interface Module { + id: string; code: string; name: string; sac: string + allowedKinds: Kind[]; multiSubscription: boolean; active: boolean +} + +export interface ModulePrice { + id: string; moduleId: string; edition: string; kind: Kind + pricePaise: number; effectiveFrom: string +} + +export type ClientModuleStatus = + | 'quoted' | 'ordered' | 'installing' | 'installed' + | 'trained' | 'live' | 'expired' | 'cancelled' +export const CLIENT_MODULE_STATUSES: ClientModuleStatus[] = [ + 'quoted', 'ordered', 'installing', 'installed', 'trained', 'live', 'expired', 'cancelled', +] + +export interface ClientModule { + id: string; clientId: string; moduleId: string; status: ClientModuleStatus + kind: Kind; edition: string + installedOn: string | null; completedOn: string | null; trainedOn: string | null + nextRenewal: string | null; active: boolean +} + +export type DocType = 'QUOTATION' | 'PROFORMA' | 'INVOICE' | 'RECEIPT' | 'CREDIT_NOTE' +export type DocStatus = + | 'draft' | 'sent' | 'accepted' | 'invoiced' | 'part_paid' | 'paid' | 'lost' | 'cancelled' + +export interface DocLine { + itemId: string; name: string; hsn: string; qty: number; unitCode: string + unitPricePaise: number; taxablePaise: number; lineTotalPaise: number +} + +export interface Doc { + id: string; docType: DocType; docNo: string | null; fy: string; clientId: string + docDate: string; status: DocStatus; refDocId: string | null + taxablePaise: number; cgstPaise: number; sgstPaise: number; igstPaise: number + roundOffPaise: number; payablePaise: number + payload: { lines: DocLine[]; terms?: string } + source: string; createdBy: string; createdAt: string +} + +export interface DocumentEvent { + id: string; documentId: string; atWall: string; kind: string; meta: Record +} + +/** email_log rows come straight off the table (snake_case). */ +export interface EmailLogRow { + id: string; document_id: string | null; to_addr: string; subject: string + status: 'sent' | 'failed'; gmail_message_id: string | null; error: string | null; at_wall: string +} + +export type PaymentMode = 'bank' | 'upi' | 'cheque' | 'cash' | 'other' +export const PAYMENT_MODES: PaymentMode[] = ['bank', 'upi', 'cheque', 'cash', 'other'] + +export interface Payment { + id: string; clientId: string; receivedOn: string; mode: PaymentMode + reference: string; amountPaise: number; tdsPaise: number + createdBy: string; createdAt: string +} + +export interface Ledger { + documents: Doc[]; payments: Payment[]; advancePaise: number + modulePaid: { moduleId: string; billedPaise: number; settledPaise: number }[] +} + +// ---------- typed calls ---------- + +export const getClients = (q?: string): Promise => + apiFetch<{ clients: Client[] }>(`/clients${q !== undefined && q !== '' ? `?q=${encodeURIComponent(q)}` : ''}`) + .then((r) => r.clients) +export const getClient = (id: string): Promise => + apiFetch<{ client: Client }>(`/clients/${id}`).then((r) => r.client) +export const createClient = (body: Record): Promise => + apiFetch<{ client: Client }>('/clients', { method: 'POST', body: JSON.stringify(body) }).then((r) => r.client) +export const patchClient = (id: string, body: Record): Promise => + apiFetch<{ client: Client }>(`/clients/${id}`, { method: 'PATCH', body: JSON.stringify(body) }).then((r) => r.client) + +export const getModules = (): Promise => + apiFetch<{ modules: Module[] }>('/modules').then((r) => r.modules) +export const createModule = (body: Record): Promise => + apiFetch<{ module: Module }>('/modules', { method: 'POST', body: JSON.stringify(body) }).then((r) => r.module) +export const getPrices = (moduleId: string): Promise => + apiFetch<{ prices: ModulePrice[] }>(`/modules/${moduleId}/prices`).then((r) => r.prices) +export const addPrice = (moduleId: string, body: Record): Promise => + apiFetch<{ prices: ModulePrice[] }>(`/modules/${moduleId}/prices`, { method: 'POST', body: JSON.stringify(body) }) + .then((r) => r.prices) + +export const getClientModules = (clientId: string): Promise => + apiFetch<{ clientModules: ClientModule[] }>(`/clients/${clientId}/modules`).then((r) => r.clientModules) +export const assignClientModule = (clientId: string, body: Record): Promise => + apiFetch<{ clientModule: ClientModule }>(`/clients/${clientId}/modules`, { method: 'POST', body: JSON.stringify(body) }) + .then((r) => r.clientModule) +export const patchClientModule = (id: string, body: Record): Promise => + apiFetch<{ clientModule: ClientModule }>(`/client-modules/${id}`, { method: 'PATCH', body: JSON.stringify(body) }) + .then((r) => r.clientModule) + +export const createDocument = (body: Record): Promise => + apiFetch<{ document: Doc }>('/documents', { method: 'POST', body: JSON.stringify(body) }).then((r) => r.document) +export const getDocumentFull = (id: string): Promise<{ document: Doc; events: DocumentEvent[]; emails: EmailLogRow[] }> => + apiFetch(`/documents/${id}`) +/** issue / status / convert / cancel / credit-note / send — all POST → { document }. */ +export const documentAction = (id: string, action: string, body?: Record): Promise => + apiFetch<{ document: Doc }>(`/documents/${id}/${action}`, { + method: 'POST', body: JSON.stringify(body ?? {}), + }).then((r) => r.document) + +export const recordPayment = (body: Record): Promise => + apiFetch<{ payment: Payment }>('/payments', { method: 'POST', body: JSON.stringify(body) }).then((r) => r.payment) +export const getLedger = (clientId: string): Promise => + apiFetch(`/clients/${clientId}/ledger`) + +/** The PDF route needs the bearer header, which an