diff --git a/apps/hq-web/src/api.ts b/apps/hq-web/src/api.ts index d4fd4db..f78bfe5 100644 --- a/apps/hq-web/src/api.ts +++ b/apps/hq-web/src/api.ts @@ -965,6 +965,11 @@ export const getRenewals = (from?: string, to?: string): Promise = } export const generateModuleRenewalQuote = (clientModuleId: string): Promise => apiFetch<{ document: Doc }>(`/client-modules/${clientModuleId}/renewal-quote`, { method: 'POST', body: '{}' }).then((r) => r.document) +/** Raise a bulk SMS top-up proforma for a client's SMS module (client-detail Task 4). */ +export const generateBulkSmsPurchase = (clientModuleId: string, smsQty: number): Promise => + apiFetch<{ document: Doc }>(`/client-modules/${clientModuleId}/bulk-sms-quote`, { + method: 'POST', body: JSON.stringify({ smsQty }), + }).then((r) => r.document) // D27: owner MIS cockpit. export interface MisCockpit { diff --git a/apps/hq-web/src/pages/ClientDetail.tsx b/apps/hq-web/src/pages/ClientDetail.tsx index e0aa996..e3109fc 100644 --- a/apps/hq-web/src/pages/ClientDetail.tsx +++ b/apps/hq-web/src/pages/ClientDetail.tsx @@ -16,9 +16,10 @@ import { getClientAwsUsage, getCompanyProfile, getBranches, createBranch, patchBranch, getTickets, getMilestones, setMilestone, addMilestone, + generateBulkSmsPurchase, CLIENT_STATUSES, KIND_LABEL, type AmcContract, type AmcPaidStatus, type Branch, type Client, type ClientModule, - type ClientStatus, type FieldDef, type Interaction, type Ledger, type Milestone, type Outcome, + type ClientStatus, type Doc, type FieldDef, type Interaction, type Ledger, type Milestone, type Outcome, type Payment, type RecurringPlan, type ServiceDetail, } from '../api' import { buildStatement } from '../statement' @@ -327,6 +328,12 @@ export function ClientDetail() { refreshToken={milestonesVersion} onTickPayment={(key, label) => { setLinkError(undefined); setPaymentPicker({ clientModuleId: cm.id, key, label }) }} /> + m.id === cm.moduleId)?.code ?? ''} + onCreated={() => ledger.reload()} + /> ))} @@ -1367,6 +1374,61 @@ function StatusLine(props: { ) } +/** Doc.source values that don't map onto DOC_TYPE_LABEL get a friendlier, source-aware label. */ +const DOC_SOURCE_LABEL: Record = { + module_renewal: 'Renewal', bulk_sms_topup: 'Bulk top-up', +} +function docLabel(d: Doc): string { + if (DOC_SOURCE_LABEL[d.source] !== undefined) return DOC_SOURCE_LABEL[d.source]! + return DOC_TYPE_LABEL[d.docType] ?? d.docType +} + +/** + * Task 4: per-module documents — every doc whose line items include this module (quotes, + * renewals, bulk SMS top-ups, invoices, credit notes), newest first. SMS-coded modules get + * a "Bulk SMS purchase" action that raises a proforma and jumps straight to it. + */ +function ModuleDocuments(props: { cm: ClientModule; docs: Doc[]; moduleCode: string; onCreated: () => void }) { + const nav = useNavigate() + const toast = useToast() + const [busy, setBusy] = useState(false) + const mine = props.docs + .filter((d) => d.payload.lines.some((l) => l.itemId === props.cm.moduleId)) + .sort((a, b) => b.docDate.localeCompare(a.docDate)) + const isSms = props.moduleCode.toUpperCase() === 'SMS' + + const bulk = () => { + const qty = Number(window.prompt('Bulk SMS quantity to purchase?', '100000') ?? '') + if (!Number.isInteger(qty) || qty <= 0) return + setBusy(true) + generateBulkSmsPurchase(props.cm.id, qty) + .then((doc) => { toast.ok('Bulk SMS proforma created'); props.onCreated(); nav(`/documents/${doc.id}`) }) + .catch((e: Error) => { toast.err(e.message); setBusy(false) }) + } + + return ( +
+
+ Documents + {mine.length} + + {isSms && } +
+ {mine.length === 0 ?
No documents for this module yet.
+ : mine.map((d) => ( +
nav(`/documents/${d.id}`)} + style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '7px 0', borderTop: '1px solid var(--border)', cursor: 'pointer', fontSize: 12.5 }}> + {d.docNo ?? 'draft'} + {docLabel(d)} + {d.docDate} + {inr(d.payablePaise)} + {d.status} +
+ ))} +
+ ) +} + /** * Payment-step picker (D38): ticking the advance/balance payment status step opens this * instead of stamping a bare date — it links a real payment row so the milestone and the