import { useEffect, useMemo, useState } from 'react' import { formatINR, fromRupees } from '@sims/domain' import { Badge, Button, ConfirmDialog, DataTable, Dialog, EmptyState, ErrorState, Field, FormField, FormGrid, Notice, PageHeader, Skeleton, Toolbar, useToast, } from '@sims/ui' import { addPrice, assignClientModule, createModule, downloadModuleClientsCsv, getClients, getModuleClients, getModules, getPrices, notifyModuleClients, patchClientModule, patchModule, previewSample, role, CLIENT_MODULE_STATUSES, KIND_LABEL, type Client, type ClientModuleStatus, type Kind, type Module, } from '../api' import { LivePreview } from '../components/LivePreview' import { useData } from './Clients' const ALL_KINDS: Kind[] = ['one_time', 'monthly', 'yearly', 'usage'] const today = () => new Date().toISOString().slice(0, 10) /** Textarea ↔ string[]: one "what's included" bullet per row, blanks dropped. */ const splitLines = (s: string): string[] => s.split('\n').map((x) => x.trim()).filter((x) => x !== '') /** Module catalog + dated price book. Owner edits; staff read-only. */ export function Modules() { const isOwner = role() === 'owner' const modules = useData(getModules, []) const [creating, setCreating] = useState(false) const [selected, setSelected] = useState() return (
setCreating(true)}>New module : read-only (staff)} /> {modules.error !== undefined ? : modules.data === undefined ? : modules.data.length === 0 ? No modules yet. : ( setSelected(modules.data![i])} rows={modules.data.map((m) => ({ code: m.code, name: m.name, sac: m.sac, kinds: m.allowedKinds.map((k) => KIND_LABEL[k]).join(' · '), multi: m.multiSubscription ? yes : '—', active: m.active ? active : inactive, }))} /> )} {selected !== undefined && ( <> modules.reload()} /> )} {isOwner && ( setCreating(false)} onCreated={() => { setCreating(false); modules.reload() }} /> )}
) } /** * "Who's on this module" roster (spec §10): every ACTIVE client link with what * it pays and its next renewal, plus Notify-all (owner/manager) and CSV export. */ function ModuleClients(props: { module: Module }) { const managerial = role() === 'owner' || role() === 'manager' const [page, setPage] = useState(1) const roster = useData(() => getModuleClients(props.module.id, page), [props.module.id, page]) const [notifying, setNotifying] = useState(false) const [subject, setSubject] = useState('') const [bodyText, setBodyText] = useState('') const [busy, setBusy] = useState(false) const [result, setResult] = useState() const [err, setErr] = useState() // D18 WS-G full-edit: one row in edit mode at a time; writes go through the SAME // audited repo calls Client 360 uses (patchClientModule / assignClientModule). const [edit, setEdit] = useState<{ cmId: string; kind: Kind; edition: string; renewal: string; status: ClientModuleStatus } | undefined>() const [unassign, setUnassign] = useState<{ cmId: string; clientName: string } | undefined>() const [assigning, setAssigning] = useState(false) const [assignQ, setAssignQ] = useState('') const [assignHits, setAssignHits] = useState([]) const [assignPick, setAssignPick] = useState() const [assignKind, setAssignKind] = useState(props.module.allowedKinds[0] ?? 'yearly') const [assignEdition, setAssignEdition] = useState('standard') const saveEdit = () => { if (edit === undefined || busy) return setBusy(true); setErr(undefined) patchClientModule(edit.cmId, { kind: edit.kind, edition: edit.edition, nextRenewal: edit.renewal === '' ? null : edit.renewal, status: edit.status, }) .then(() => { setEdit(undefined); roster.reload() }) .catch((e: Error) => setErr(e.message)) .finally(() => setBusy(false)) } const searchAssign = (q: string) => { setAssignQ(q); setAssignPick(undefined) if (q.trim().length < 2) { setAssignHits([]); return } getClients(q).then((cs) => setAssignHits(cs.slice(0, 6))).catch(() => setAssignHits([])) } const doAssign = () => { if (assignPick === undefined || busy) return setBusy(true); setErr(undefined) assignClientModule(assignPick.id, { moduleId: props.module.id, kind: assignKind, edition: assignEdition }) .then(() => { setAssigning(false); setAssignQ(''); setAssignPick(undefined); roster.reload() }) .catch((e: Error) => setErr(e.message)) .finally(() => setBusy(false)) } const doNotify = () => { setBusy(true); setErr(undefined); setResult(undefined) notifyModuleClients(props.module.id, { subject, body: bodyText }) .then((out) => { setResult(`Sent ${out.sent}/${out.total}` + (out.failed.length > 0 ? ` — unreached: ${out.failed.map((f) => `${f.client} (${f.error})`).join(', ')}` : '')) setNotifying(false) }) .catch((e: Error) => setErr(e.message)) .finally(() => setBusy(false)) } const data = roster.data const pages = data !== undefined ? Math.max(1, Math.ceil(data.total / data.pageSize)) : 1 return (

Clients on this module

{managerial && ( )} {assigning && ( // wf-card, not the dead 'wf-form' class (no rules exist for it anywhere) — // the assign panel gets the same contained-card chrome as every other inline panel.
searchAssign(e.target.value)} /> {assignPick === undefined && assignHits.length > 0 && (
{assignHits.map((c) => ( ))}
)} setAssignEdition(e.target.value)} />
)} {notifying && managerial && (
setSubject(e.target.value)} />