@ -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 = { module Name ( cm . module Id ) }
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 module s 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 >
)
}