feat(client-detail): per-module documents list + bulk SMS purchase action

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
main
Thomas Joise 2 days ago
parent e5ed3517dc
commit e8f00503a4

@ -965,6 +965,11 @@ export const getRenewals = (from?: string, to?: string): Promise<RenewalRow[]> =
} }
export const generateModuleRenewalQuote = (clientModuleId: string): Promise<Doc> => export const generateModuleRenewalQuote = (clientModuleId: string): Promise<Doc> =>
apiFetch<{ document: Doc }>(`/client-modules/${clientModuleId}/renewal-quote`, { method: 'POST', body: '{}' }).then((r) => r.document) 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<Doc> =>
apiFetch<{ document: Doc }>(`/client-modules/${clientModuleId}/bulk-sms-quote`, {
method: 'POST', body: JSON.stringify({ smsQty }),
}).then((r) => r.document)
// D27: owner MIS cockpit. // D27: owner MIS cockpit.
export interface MisCockpit { export interface MisCockpit {

@ -16,9 +16,10 @@ import {
getClientAwsUsage, getCompanyProfile, getClientAwsUsage, getCompanyProfile,
getBranches, createBranch, patchBranch, getTickets, getBranches, createBranch, patchBranch, getTickets,
getMilestones, setMilestone, addMilestone, getMilestones, setMilestone, addMilestone,
generateBulkSmsPurchase,
CLIENT_STATUSES, KIND_LABEL, CLIENT_STATUSES, KIND_LABEL,
type AmcContract, type AmcPaidStatus, type Branch, type Client, type ClientModule, 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, type Payment, type RecurringPlan, type ServiceDetail,
} from '../api' } from '../api'
import { buildStatement } from '../statement' import { buildStatement } from '../statement'
@ -327,6 +328,12 @@ export function ClientDetail() {
refreshToken={milestonesVersion} refreshToken={milestonesVersion}
onTickPayment={(key, label) => { setLinkError(undefined); setPaymentPicker({ clientModuleId: cm.id, key, label }) }} onTickPayment={(key, label) => { setLinkError(undefined); setPaymentPicker({ clientModuleId: cm.id, key, label }) }}
/> />
<ModuleDocuments
cm={cm}
docs={ledger.data?.documents ?? []}
moduleCode={modules.data?.find((m) => m.id === cm.moduleId)?.code ?? ''}
onCreated={() => ledger.reload()}
/>
</div> </div>
</details> </details>
))} ))}
@ -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<string, string> = {
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 (
<div className="wf-card" style={{ padding: '10px 14px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 6 }}>
<span className="lbl" style={{ fontWeight: 700 }}>Documents</span>
<Badge>{mine.length}</Badge>
<span style={{ flex: 1 }} />
{isSms && <Button disabled={busy} onClick={bulk}>Bulk SMS purchase</Button>}
</div>
{mine.length === 0 ? <div style={{ color: 'var(--text-dim)', fontSize: 12.5 }}>No documents for this module yet.</div>
: mine.map((d) => (
<div key={d.id} onClick={() => nav(`/documents/${d.id}`)}
style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '7px 0', borderTop: '1px solid var(--border)', cursor: 'pointer', fontSize: 12.5 }}>
<span className="mono" style={{ minWidth: 74 }}>{d.docNo ?? 'draft'}</span>
<span className="doc-pill">{docLabel(d)}</span>
<span className="mono" style={{ color: 'var(--text-dim)' }}>{d.docDate}</span>
<span className="mono" style={{ marginLeft: 'auto' }}>{inr(d.payablePaise)}</span>
<Badge tone={DOC_TONE[d.status]}>{d.status}</Badge>
</div>
))}
</div>
)
}
/** /**
* Payment-step picker (D38): ticking the advance/balance payment status step opens this * 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 * instead of stamping a bare date it links a real payment row so the milestone and the

Loading…
Cancel
Save