|
|
|
|
@ -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 }) }}
|
|
|
|
|
/>
|
|
|
|
|
<ModuleDocuments
|
|
|
|
|
cm={cm}
|
|
|
|
|
docs={ledger.data?.documents ?? []}
|
|
|
|
|
moduleCode={modules.data?.find((m) => m.id === cm.moduleId)?.code ?? ''}
|
|
|
|
|
onCreated={() => ledger.reload()}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</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
|
|
|
|
|
* instead of stamping a bare date — it links a real payment row so the milestone and the
|
|
|
|
|
|