feat(client-detail): drop mandatory assign creds, add per-module status control, trim summary table

- AssignModuleForm: remove SMS-forced username/password gate at assign time
  (creds now captured later via module Access -> Edit); reword to "Add a
  module" with a one-line hint on what assigning starts.
- Modules summary table: drop Installed/Completed/Trained columns (dates
  live in the per-module status line); keep Module/Kind/Status/Billed/Settled.
- Each module's collapsible block now has an inline ClientModuleStatusSelect
  next to its status badge, so status is editable without opening the table.
- Payments tab: remove the Outstanding panel's duplicate "Advance on
  account" footer line -- the pre-existing StatCard is the single source.
- Remove RowDate, now unused after the summary-table trim.

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

@ -29,7 +29,7 @@ import {
} from './Tickets'
import {
AccountOwnerSelect, AmcDialog, AssignModuleForm, ClientModuleStatusSelect,
InteractionDialog, PaymentForm, PlanDialog, RowDate,
InteractionDialog, PaymentForm, PlanDialog,
} from './client-forms'
import { ClientFormDialog } from '../components/ClientFormDialog'
import { PulseRibbon } from '../components/PulseRibbon'
@ -268,11 +268,8 @@ export function ClientDetail() {
{modules.data !== undefined && (
<AssignModuleForm
modules={modules.data}
onAssign={(moduleId, kind, creds) => {
onAssign={(moduleId, kind) => {
assignClientModule(id, { moduleId, kind })
.then((cm) => creds !== undefined
? patchClientModule(cm.id, { username: creds.username, password: creds.password })
: cm)
.then(() => { toast.ok('Module assigned'); cms.reload(); ledger.reload() })
.catch((err: Error) => toast.err(err.message))
}}
@ -285,8 +282,6 @@ export function ClientDetail() {
columns={[
{ key: 'module', label: 'Module' }, { key: 'kind', label: 'Kind' },
{ key: 'status', label: 'Status' },
{ key: 'installed', label: 'Installed' }, { key: 'completed', label: 'Completed' },
{ key: 'trained', label: 'Trained' },
{ key: 'billed', label: 'Billed', numeric: true },
{ key: 'settled', label: 'Settled', numeric: true },
]}
@ -296,9 +291,6 @@ export function ClientDetail() {
module: moduleName(cm.moduleId),
kind: KIND_LABEL[cm.kind],
status: <ClientModuleStatusSelect cm={cm} onPatched={() => cms.reload()} onError={toast.err} />,
installed: <RowDate cm={cm} field="installedOn" onPatched={() => cms.reload()} onError={toast.err} />,
completed: <RowDate cm={cm} field="completedOn" onPatched={() => cms.reload()} onError={toast.err} />,
trained: <RowDate cm={cm} field="trainedOn" onPatched={() => cms.reload()} onError={toast.err} />,
billed: paid !== undefined ? inr(paid.billedPaise) : '—',
settled: paid !== undefined ? inr(paid.settledPaise) : '—',
}
@ -314,6 +306,9 @@ export function ClientDetail() {
<span className="mod-name">{moduleName(cm.moduleId)}</span>
<Badge tone={cm.status === 'live' ? 'ok' : undefined}>{cm.status}</Badge>
<Badge>{KIND_LABEL[cm.kind]}</Badge>
<span onClick={(e) => e.stopPropagation()}>
<ClientModuleStatusSelect cm={cm} onPatched={() => cms.reload()} onError={toast.err} />
</span>
</summary>
<div className="mod-section-body">
<ServiceDataCard
@ -439,8 +434,6 @@ export function ClientDetail() {
<span className="due">{inr(o.outstandingPaise)}</span>
</div>
))}
<div className="ofoot"><span style={{ color: 'var(--text-dim)', fontSize: 12 }}>Advance on account</span>
<span className="mono" style={{ marginLeft: 'auto', fontWeight: 600 }}>{formatINR(ledger.data.advancePaise)}</span></div>
</div>
</div>
)}

@ -284,25 +284,21 @@ export function InteractionDialog(props: {
export function AssignModuleForm(props: {
modules: Module[]
onAssign: (moduleId: string, kind: Kind, creds?: { username: string; password: string }) => void
onAssign: (moduleId: string, kind: Kind) => void
}) {
const [moduleId, setModuleId] = useState('')
const mod = props.modules.find((m) => m.id === moduleId)
const [kind, setKind] = useState<Kind | ''>('')
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
// SMS is a gateway service — its login is required at assign time so the balance pull works.
const needsCreds = mod?.code === 'SMS'
const ready = moduleId !== '' && kind !== '' && (!needsCreds || (username.trim() !== '' && password.trim() !== ''))
const reset = () => { setModuleId(''); setKind(''); setUsername(''); setPassword('') }
const ready = moduleId !== '' && kind !== ''
const reset = () => { setModuleId(''); setKind('') }
return (
<div className="wf-card" style={{ padding: '10px 14px', marginBottom: 10 }}>
<Toolbar>
<select
className="wf" value={moduleId}
onChange={(e) => { setModuleId(e.target.value); setKind(''); setUsername(''); setPassword('') }}
onChange={(e) => { setModuleId(e.target.value); setKind('') }}
>
<option value="">Assign module</option>
<option value="">Add a module</option>
{props.modules.filter((m) => m.active).map((m) => <option key={m.id} value={m.id}>{m.name}</option>)}
</select>
{mod !== undefined && (
@ -311,30 +307,20 @@ export function AssignModuleForm(props: {
{mod.allowedKinds.map((k) => <option key={k} value={k}>{KIND_LABEL[k]}</option>)}
</select>
)}
{needsCreds && (
<>
<input className="wf mono" style={{ width: 150 }} placeholder="SMS username *" aria-label="SMS username"
value={username} onChange={(e) => setUsername(e.target.value)} />
<input className="wf mono" style={{ width: 150 }} placeholder="SMS password *" aria-label="SMS password"
value={password} onChange={(e) => setPassword(e.target.value)} />
</>
)}
<Button
tone="primary" disabled={!ready}
onClick={() => {
if (!ready) return
props.onAssign(moduleId, kind as Kind, needsCreds ? { username: username.trim(), password: password.trim() } : undefined)
props.onAssign(moduleId, kind as Kind)
reset()
}}
>
Assign
Add a module
</Button>
</Toolbar>
{needsCreds && (
<span style={{ fontSize: 12, color: 'var(--text-dim)' }}>
SMS needs the provider gateway login username and password are required so the balance can be pulled.
</span>
)}
<span style={{ fontSize: 12, color: 'var(--text-dim)' }}>
Starts the module at "quoted" and opens its onboarding status line.
</span>
</div>
)
}
@ -355,22 +341,6 @@ export function ClientModuleStatusSelect(props: {
)
}
export function RowDate(props: {
cm: ClientModule; field: 'installedOn' | 'completedOn' | 'trainedOn'
onPatched: () => void; onError: (msg: string) => void
}) {
return (
<input
type="date" className="wf" style={{ width: 140 }}
value={props.cm[props.field] ?? ''}
onChange={(e) => {
patchClientModule(props.cm.id, { [props.field]: e.target.value !== '' ? e.target.value : null })
.then(props.onPatched).catch((err: Error) => props.onError(err.message))
}}
/>
)
}
/**
* Record a payment against the client amounts entered in rupees, stored in
* integer paise via fromRupees. Shared with DocumentView's "Record payment".

Loading…
Cancel
Save