feat(client-detail): prompt for module credentials on account_creation tick

Ticking the "Account creation & registration" onboarding step now opens a
small credentials dialog (mirrors the existing payment-step picker) instead
of a bare toggle. Saving reuses ServiceDataCard's exact patchClientModule
call and managerial password gate, then marks the milestone done; "Skip"
marks it done without touching credentials so the step never hard-blocks.

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

@ -82,6 +82,12 @@ export function ClientDetail() {
const [linkingPayment, setLinkingPayment] = useState(false)
const [linkError, setLinkError] = useState<string | undefined>()
const [milestonesVersion, setMilestonesVersion] = useState(0)
// Ticking the account_creation step opens this credentials dialog instead of a bare
// toggle (mirrors the payment picker above) — `cm` is captured via closure at the JSX
// callback below, exactly like `onTickPayment` captures it for the payment picker.
const [credsDialog, setCredsDialog] = useState<{ cm: ClientModule; key: string; label: string } | undefined>()
const [savingCreds, setSavingCreds] = useState(false)
const [credsError, setCredsError] = useState<string | undefined>()
const rawTab = sp.get('tab') ?? 'overview'
const tab: TabKey = (TAB_KEYS as readonly string[]).includes(rawTab) ? (rawTab as TabKey) : 'overview'
@ -322,6 +328,7 @@ export function ClientDetail() {
name={moduleName(cm.moduleId)}
refreshToken={milestonesVersion}
onTickPayment={(key, label) => { setLinkError(undefined); setPaymentPicker({ clientModuleId: cm.id, key, label }) }}
onTickCreds={(key, label) => { setCredsError(undefined); setCredsDialog({ cm, key, label }) }}
/>
<ModuleDocuments
cm={cm}
@ -670,6 +677,47 @@ export function ClientDetail() {
onRecordNew={() => { setPaymentPicker(undefined); setTab('payments') }}
/>
)}
{credsDialog !== undefined && (
<CredentialsDialog
open
stepLabel={credsDialog.label}
cm={credsDialog.cm}
saving={savingCreds}
error={credsError}
onClose={() => { if (!savingCreds) setCredsDialog(undefined) }}
onSave={(fields) => {
if (savingCreds) return
setSavingCreds(true)
setCredsError(undefined)
patchClientModule(credsDialog.cm.id, {
provider: fields.provider, username: fields.username,
...(canRoute && fields.password.trim() !== '' ? { password: fields.password } : {}),
})
.then(() => setMilestone(credsDialog.cm.id, credsDialog.key, true))
.then(() => {
toast.ok('Credentials saved')
setMilestonesVersion((v) => v + 1)
cms.reload()
setCredsDialog(undefined)
})
.catch((e: Error) => setCredsError(e.message))
.finally(() => setSavingCreds(false))
}}
onSkip={() => {
if (savingCreds) return
setSavingCreds(true)
setCredsError(undefined)
setMilestone(credsDialog.cm.id, credsDialog.key, true)
.then(() => {
toast.ok('Step marked done')
setMilestonesVersion((v) => v + 1)
setCredsDialog(undefined)
})
.catch((e: Error) => setCredsError(e.message))
.finally(() => setSavingCreds(false))
}}
/>
)}
</div>
)
}
@ -1306,6 +1354,7 @@ function SecretField(props: { cm: ClientModule; field: FieldDef; managerial: boo
function StatusLine(props: {
clientModuleId: string; name: string; refreshToken: number
onTickPayment: (key: string, label: string) => void
onTickCreds: (key: string, label: string) => void
}) {
const toast = useToast()
const list = useData(() => getMilestones(props.clientModuleId), [props.clientModuleId, props.refreshToken])
@ -1336,12 +1385,16 @@ function StatusLine(props: {
// current = first not-done step
const currentKey = ms?.find((m) => !m.done)?.key
const isPaymentStep = (key: string) => key === 'advance_payment' || key === 'balance_payment'
const isCredsStep = (key: string) => key === 'account_creation'
// Ticking ON a payment step opens the picker (parent owns that dialog — it needs the
// client's ledger + the Payments tab); un-ticking and every other step toggle directly.
// client's ledger + the Payments tab); ticking ON account_creation opens the parent's
// credentials dialog (it needs the module's provider/username/password + managerial
// gate). Un-ticking and every other step toggle directly.
const onDotClick = (m: Milestone) => {
if (busyKey !== '') return
if (!m.done && isPaymentStep(m.key)) { props.onTickPayment(m.key, m.label); return }
if (!m.done && isCredsStep(m.key)) { props.onTickCreds(m.key, m.label); return }
toggle(m, !m.done)
}
@ -1378,7 +1431,12 @@ function StatusLine(props: {
<span className="bar" />
<button
className="dot" disabled={busyKey === m.key}
title={m.done ? 'Click to un-tick' : isPaymentStep(m.key) ? 'Click to link a payment' : 'Click to mark done (today)'}
title={
m.done ? 'Click to un-tick'
: isPaymentStep(m.key) ? 'Click to link a payment'
: isCredsStep(m.key) ? 'Click to set account credentials'
: 'Click to mark done (today)'
}
onClick={() => onDotClick(m)}
>
{m.done ? '✓' : m.key === currentKey ? '●' : ''}
@ -1491,3 +1549,62 @@ function PaymentPickerDialog(props: {
</Dialog>
)
}
/**
* Account-creation credentials dialog (D38 follow-up): ticking the "Account creation &
* registration" status-line step opens this instead of a bare toggle mirrors
* `PaymentPickerDialog`'s structure/props exactly. Captures the module's Access fields
* right at the point the account actually exists, reusing `ServiceDataCard`'s exact save
* call (`patchClientModule`) and its managerial gate on the password field (blank keeps
* whatever is already stored). Fields pre-fill from the client_module so re-ticking after
* creds already exist is never blocked "Skip" marks the step done without touching
* credentials at all, for modules that don't need one.
*/
function CredentialsDialog(props: {
open: boolean; onClose: () => void; stepLabel: string; cm: ClientModule
saving: boolean; error?: string
onSave: (fields: { provider: string; username: string; password: string }) => void
onSkip: () => void
}) {
const managerial = isManagerial()
const [provider, setProvider] = useState(props.cm.provider ?? '')
const [username, setUsername] = useState(props.cm.username ?? '')
const [password, setPassword] = useState('')
return (
<Dialog
open={props.open}
onClose={() => { if (!props.saving) props.onClose() }}
title={`Account credentials — ${props.stepLabel}`}
size="sm"
footer={
<>
<Button onClick={props.onClose} disabled={props.saving}>Cancel</Button>
<Button disabled={props.saving} onClick={props.onSkip}>Skip mark done</Button>
<Button tone="primary" disabled={props.saving} onClick={() => props.onSave({ provider, username, password })}>
{props.saving ? 'Saving…' : 'Save & mark done'}
</Button>
</>
}
>
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
<Field label="Provider">
<input className="wf" value={provider} onChange={(e) => setProvider(e.target.value)} />
</Field>
<Field label="Username">
<input className="wf mono" value={username} onChange={(e) => setUsername(e.target.value)} />
</Field>
{managerial && (
<Field label="Password">
<input
className="wf mono" type="password"
placeholder={props.cm.hasPassword ? 'leave blank to keep' : 'set password'}
value={password} onChange={(e) => setPassword(e.target.value)}
/>
</Field>
)}
</div>
{props.error !== undefined && <Notice tone="err">{props.error}</Notice>}
</Dialog>
)
}

Loading…
Cancel
Save