@ -5,11 +5,11 @@ import {
FormGrid , Notice , PageHeader , Skeleton , Tabs , Toolbar , useToast ,
} from '@sims/ui'
import {
addPrice , assignClientModule , createModule , downloadModuleClientsCsv , getClients ,
getModuleClients , getModule s, getPrices , notifyModuleClients , patchClientModule , patchModule ,
previewSample , role ,
addPrice , assignClientModule , createModule , downloadModuleClientsCsv , getClients , getFrontTemplate ,
getModuleClients , getModule Template, getModule s, getPrices , notifyModuleClients , patchClientModule , patchModule ,
previewSample , role , setFrontTemplate , setModuleTemplate ,
CLIENT_MODULE_STATUSES , KIND_LABEL ,
type Client , type ClientModuleStatus , type FieldDef , type FieldType , type Kind , type Module ,
type Client , type ClientModuleStatus , type FieldDef , type FieldType , type Kind , type Module , type TemplateStep ,
} from '../api'
import { LivePreview } from '../components/LivePreview'
import { useData } from './Clients'
@ -121,6 +121,7 @@ export function Modules() {
onCreated = { ( ) = > { setCreating ( false ) ; module s.reload ( ) } }
/ >
) }
{ isOwner && module s.data !== undefined && < OnboardingTemplatesSection modules = { module s.data } / > }
< / div >
)
}
@ -832,3 +833,126 @@ function AddPriceDialog(props: { open: boolean; onClose: () => void; module: Mod
< / Dialog >
)
}
/ * *
* Owner - only editor for the COMPOSED onboarding checklist template : one shared "front" ( same
* lead - in steps for every module ) plus each module ' s own "tail" . Both are config ( ` setting `
* rows ) resolved by ` milestoneTemplate ` — this just edits the two halves . Staged local edits ;
* nothing is written until Save .
* /
function OnboardingTemplatesSection ( props : { module s : Module [ ] } ) {
const front = useData ( getFrontTemplate , [ ] )
const [ expanded , setExpanded ] = useState < string | undefined > ( )
return (
< section style = { { marginTop : 28 } } >
< h3 > Onboarding steps < / h3 >
< div className = "wf-card" style = { { padding : '10px 14px' , marginBottom : 14 , fontSize : 13 , color : 'var(--text-dim)' , lineHeight : 1.5 } } >
Every project 's onboarding checklist is this shared <strong style={{ color: ' var ( -- text ) ' } } > front < / strong > followed
by that module 's own <strong style={{ color: ' var ( -- text ) ' } } > tail < / strong > below . Changes reach existing projects
additively — new steps appear on projects already in progress , and any step already ticked stays ticked as - is .
Re - ordering or renaming steps that existing projects already have doesn ' t retro - fit those projects — that needs
the onboarding - template migration script .
< / div >
< h4 > Shared front < span style = { { fontWeight : 400 , fontSize : 12 , color : 'var(--text-dim)' } } > — applies to every module < / span > < / h4 >
{ front . error !== undefined ? < ErrorState message = { front . error } onRetry = { front . reload } / >
: front . data === undefined ? < Skeleton rows = { 3 } / > : (
< div className = "wf-card" style = { { padding : 14 , marginBottom : 20 } } >
< StepListEditor
steps = { front . data }
onSave = { ( steps ) = > setFrontTemplate ( steps ) . then ( ( saved ) = > { front . reload ( ) ; return saved } ) }
/ >
< / div >
) }
< h4 > Per - module tail < / h4 >
< div style = { { display : 'flex' , flexDirection : 'column' , gap : 8 } } >
{ props . module s.map ( ( m ) = > (
< div key = { m . id } className = "wf-card" style = { { padding : '10px 14px' } } >
< div style = { { display : 'flex' , alignItems : 'center' , justifyContent : 'space-between' , gap : 10 , flexWrap : 'wrap' } } >
< span > < strong > { m . name } < / strong > < span className = "mono" style = { { fontSize : 12 , color : 'var(--text-dim)' } } > { m . code } < / span > < / span >
< Button onClick = { ( ) = > setExpanded ( ( v ) = > ( v === m . id ? undefined : m . id ) ) } >
{ expanded === m . id ? 'Close' : 'Edit onboarding steps' }
< / Button >
< / div >
{ expanded === m . id && < ModuleTailEditor code = { m . code } / > }
< / div >
) ) }
< / div >
< / section >
)
}
/** Lazily loads and edits one module's tail — only fetched once its row is expanded. */
function ModuleTailEditor ( props : { code : string } ) {
const tail = useData ( ( ) = > getModuleTemplate ( props . code ) , [ props . code ] )
return (
< div style = { { marginTop : 10 } } >
{ tail . error !== undefined ? < ErrorState message = { tail . error } onRetry = { tail . reload } / >
: tail . data === undefined ? < Skeleton rows = { 3 } / > : (
< StepListEditor
steps = { tail . data }
onSave = { ( steps ) = > setModuleTemplate ( props . code , steps ) . then ( ( saved ) = > { tail . reload ( ) ; return saved } ) }
/ >
) }
< / div >
)
}
/ * *
* One editable ordered list of template steps — label - only rows ( the key is auto - derived
* server - side from the label when blank ) ; add / remove / reorder , staged locally until Save .
* Shared by the front editor and every per - module tail editor above .
* /
function StepListEditor ( props : { steps : TemplateStep [ ] ; onSave : ( steps : TemplateStep [ ] ) = > Promise < TemplateStep [ ] > } ) {
const toast = useToast ( )
const [ rows , setRows ] = useState < TemplateStep [ ] > ( ( ) = > props . steps . map ( ( s ) = > ( { . . . s } ) ) )
const [ dirty , setDirty ] = useState ( false )
const [ busy , setBusy ] = useState ( false )
const [ error , setError ] = useState < string | undefined > ( )
useEffect ( ( ) = > { setRows ( props . steps . map ( ( s ) = > ( { . . . s } ) ) ) ; setDirty ( false ) } , [ props . steps ] )
const setLabel = ( i : number , label : string ) = > {
setRows ( ( rs ) = > rs . map ( ( r , j ) = > ( j === i ? { . . . r , label } : r ) ) )
setDirty ( true )
}
const addRow = ( ) = > { setRows ( ( rs ) = > [ . . . rs , { key : '' , label : '' } ] ) ; setDirty ( true ) }
const removeRow = ( i : number ) = > { setRows ( ( rs ) = > rs . filter ( ( _r , j ) = > j !== i ) ) ; setDirty ( true ) }
const move = ( i : number , dir : - 1 | 1 ) = > {
const j = i + dir
if ( j < 0 || j >= rows . length ) return
setRows ( ( rs ) = > { const next = [ . . . rs ] ; [ next [ i ] , next [ j ] ] = [ next [ j ] ! , next [ i ] ! ] ; return next } )
setDirty ( true )
}
const save = ( ) = > {
if ( busy ) return
if ( rows . length === 0 ) { setError ( 'Add at least one step' ) ; return }
if ( rows . some ( ( r ) = > r . label . trim ( ) === '' ) ) { setError ( 'Every step needs a label' ) ; return }
setError ( undefined ) ; setBusy ( true )
props . onSave ( rows . map ( ( r ) = > ( { key : r.key , label : r.label.trim ( ) } ) ) )
. then ( ( saved ) = > { setRows ( saved . map ( ( s ) = > ( { . . . s } ) ) ) ; setDirty ( false ) ; toast . ok ( 'Onboarding steps saved' ) } )
. catch ( ( e : Error ) = > { setError ( e . message ) ; toast . err ( e . message ) } )
. finally ( ( ) = > setBusy ( false ) )
}
return (
< div style = { { display : 'flex' , flexDirection : 'column' , gap : 8 } } >
{ rows . map ( ( r , i ) = > (
< div key = { i } style = { { display : 'flex' , gap : 6 , alignItems : 'center' } } >
< input className = "wf" style = { { flex : 1 } } placeholder = "Step label…"
value = { r . label } onChange = { ( e ) = > setLabel ( i , e . target . value ) } / >
< Button disabled = { busy || i === 0 } aria - label = "Move step up" onClick = { ( ) = > move ( i , - 1 ) } > ↑ < / Button >
< Button disabled = { busy || i === rows . length - 1 } aria - label = "Move step down" onClick = { ( ) = > move ( i , 1 ) } > ↓ < / Button >
< Button tone = "danger" disabled = { busy } aria - label = "Remove step" onClick = { ( ) = > removeRow ( i ) } > ✕ < / Button >
< / div >
) ) }
< Toolbar >
< Button disabled = { busy } onClick = { addRow } > + Add step < / Button >
< Button tone = "primary" disabled = { busy || ! dirty } onClick = { save } > { busy ? 'Saving…' : 'Save' } < / Button >
< / Toolbar >
{ error !== undefined && < Notice tone = "err" > { error } < / Notice > }
< / div >
)
}