@ -26,9 +26,10 @@ export function Modules() {
const isOwner = role ( ) === 'owner'
const module s = useData ( getModules , [ ] )
const [ creating , setCreating ] = useState ( false )
const [ selected , setSelected ] = useState < Module | undefined > ( )
const [ editingId , setEditingId ] = useState < string | undefined > ( ) // inline "Edit module" form
const editingModule = module s.data?.find ( ( m ) = > m . id === editingId )
// Master → detail: selecting a module opens its own screen (derive from fresh data so the
// header + fields reflect edits immediately after a reload).
const [ selectedId , setSelectedId ] = useState < string | undefined > ( )
const selected = module s.data?.find ( ( m ) = > m . id === selectedId )
// How many clients sit on each module — one lightweight roster count per module,
// best-effort so a slow/failed one never blocks the table (cell shows '…' until then).
const [ clientCounts , setClientCounts ] = useState < Record < string , number > > ( { } )
@ -44,11 +45,34 @@ export function Modules() {
return ( ) = > { live = false }
} , [ module s.data ] )
// --- Detail screen: one module at a time, clearly headed, with a Back button. ---
if ( selected !== undefined ) {
return (
< div className = "wf-page" >
< div className = "wf-crumbs" >
< button type = "button" className = "wf" style = { { border : 'none' , background : 'none' , cursor : 'pointer' , padding : 0 } }
onClick = { ( ) = > setSelectedId ( undefined ) } > ← All module s < / button >
< span > / < / span > < span className = "mono" > { selected . code } < / span >
< / div >
< PageHeader
title = { ` ${ selected . name } · ${ selected . code } ` }
desc = { ` SAC ${ selected . sac } · ${ selected . allowedKinds . map ( ( k ) = > KIND_LABEL [ k ] ) . join ( ' · ' ) } · ${ selected . active ? 'active' : 'inactive' } ` }
actions = { < Button onClick = { ( ) = > setSelectedId ( undefined ) } > ← Back to all module s < / Button > }
/ >
< ModuleDetails key = { ` md- ${ selected . id } ` } module = { selected } isOwner = { isOwner } onSaved = { ( ) = > module s.reload ( ) } / >
< ModuleClients key = { ` mc- ${ selected . id } ` } module = { selected } / >
< FieldSpecEditor key = { ` fs- ${ selected . id } ` } module = { selected } isOwner = { isOwner } onSaved = { ( ) = > module s.reload ( ) } / >
< QuoteContent key = { selected . id } module = { selected } isOwner = { isOwner } onSaved = { ( ) = > module s.reload ( ) } / >
< PriceBook module = { selected } isOwner = { isOwner } / >
< / div >
)
}
return (
< div className = "wf-page" >
< PageHeader
title = "Modules"
desc = "What we sell — each with a dated price book. Use Edit to change a module's details; click a row to see its clients, fields, quote content and prices."
desc = "What we sell — each with a dated price book. Click a module to open it: its clients, install detail s, fields, quote content and prices."
actions = { isOwner
? < Button tone = "primary" onClick = { ( ) = > setCreating ( true ) } > New module < / Button >
: < Badge > read - only ( staff ) < / Badge > }
@ -64,31 +88,17 @@ export function Modules() {
{ key : 'multi' , label : 'Multi-sub' } , { key : 'active' , label : 'Active' } ,
{ key : 'act' , label : '' } ,
] }
onRowClick = { ( _row , i ) = > setSelected ( module s.data ! [ i ] ) }
onRowClick = { ( _row , i ) = > setSelected Id ( module s.data ! [ i ] ! . id ) }
rows = { module s.data.map ( ( m ) = > ( {
code : m.code , name : m.name , sac : m.sac ,
clients : clientCounts [ m . id ] !== undefined ? clientCounts [ m . id ] : '…' ,
kinds : m.allowedKinds.map ( ( k ) = > KIND_LABEL [ k ] ) . join ( ' · ' ) ,
multi : m.multiSubscription ? < Badge tone = "accent" > yes < / Badge > : '—' ,
active : m.active ? < Badge tone = "ok" > active < / Badge > : < Badge > inactive < / Badge > ,
act : isOwner
? < span onClick = { ( e ) = > e . stopPropagation ( ) } > < Button onClick = { ( ) = > setEditingId ( m . id ) } > Edit < / Button > < / span >
: null ,
act : < span onClick = { ( e ) = > e . stopPropagation ( ) } > < Button onClick = { ( ) = > setSelectedId ( m . id ) } > Open → < / Button > < / span > ,
} ) ) }
/ >
) }
{ editingModule !== undefined && (
< ModuleDetails key = { ` md- ${ editingModule . id } ` } module = { editingModule } isOwner = { isOwner }
onSaved = { ( ) = > module s.reload ( ) } onClose = { ( ) = > setEditingId ( undefined ) } / >
) }
{ selected !== undefined && (
< >
< ModuleClients key = { ` mc- ${ selected . id } ` } module = { selected } / >
< QuoteContent key = { selected . id } module = { selected } isOwner = { isOwner } onSaved = { ( ) = > module s.reload ( ) } / >
< FieldSpecEditor key = { ` fs- ${ selected . id } ` } module = { selected } isOwner = { isOwner } onSaved = { ( ) = > module s.reload ( ) } / >
< PriceBook module = { selected } isOwner = { isOwner } / >
< / >
) }
{ isOwner && (
< NewModuleDialog
open = { creating }
@ -328,7 +338,7 @@ function ModuleClients(props: { module: Module }) {
/ * * E d i t a m o d u l e ' s c o r e d e t a i l s ( o w n e r o n l y ) : n a m e , S A C , b i l l i n g k i n d s , m u l t i - s u b , a c t i v e .
* Code is the identity and is not editable . Field - spec / quote - content / prices have their
* own panels below . * /
function ModuleDetails ( props : { module : Module ; isOwner : boolean ; onSaved : ( ) = > void ; onClose : ( ) = > void } ) {
function ModuleDetails ( props : { module : Module ; isOwner : boolean ; onSaved : ( ) = > void ; onClose ? : ( ) = > void } ) {
const toast = useToast ( )
const m = props . module
const [ name , setName ] = useState ( m . name )
@ -348,7 +358,7 @@ function ModuleDetails(props: { module: Module; isOwner: boolean; onSaved: () =>
if ( kinds . length === 0 ) { setError ( 'Pick at least one billing kind' ) ; return }
setError ( undefined ) ; setBusy ( true )
patchModule ( m . id , { name : name.trim ( ) , sac : sac.trim ( ) , allowedKinds : kinds , multiSubscription : multi , active } )
. then ( ( ) = > { toast . ok ( 'Module saved' ) ; props . onSaved ( ) ; props . onClose ( ) } )
. then ( ( ) = > { toast . ok ( 'Module saved' ) ; props . onSaved ( ) } )
. catch ( ( e : Error ) = > setError ( e . message ) )
. finally ( ( ) = > setBusy ( false ) )
}
@ -356,7 +366,7 @@ function ModuleDetails(props: { module: Module; isOwner: boolean; onSaved: () =>
if ( ! props . isOwner ) return null
return (
< >
< h3 style = { { marginTop : 20 } } > Edit module — < span className = "mono" > { m . code } < / span > < / h3 >
< h3 style = { { marginTop : 20 } } > Details < / h3 >
< div className = "wf-card" style = { { padding : 14 } } >
< div style = { { display : 'flex' , gap : 14 , flexWrap : 'wrap' , alignItems : 'flex-end' } } >
< Field label = "Name" > < input className = "wf" style = { { width : 260 } } value = { name } onChange = { ( e ) = > setName ( e . target . value ) } / > < / Field >
@ -378,7 +388,7 @@ function ModuleDetails(props: { module: Module; isOwner: boolean; onSaved: () =>
< / div >
< Toolbar >
< Button tone = "primary" disabled = { busy } onClick = { save } > { busy ? 'Saving…' : 'Save module' } < / Button >
<Button onClick = { props . onClose } > Cancel < / Button >
{props . onClose !== undefined && <Button onClick = { props . onClose } > Cancel < / Button > }
< / Toolbar >
{ error !== undefined && < Notice tone = "err" > { error } < / Notice > }
< / div >
@ -402,7 +412,7 @@ function QuoteContent(props: { module: Module; isOwner: boolean; onSaved: () =>
return (
< >
< h3 style = { { marginTop : 20 } } > Quote content — { props . module .name } < / h3 >
< h3 style = { { marginTop : 20 } } > Quote content < span style = { { fontWeight : 400 , fontSize : 13 , opacity : 0.65 } } > — the “ what ’ s included ” lines printed on quotes < / span > < / h3 >
{ props . isOwner ? (
< div style = { { display : 'flex' , gap : 16 , alignItems : 'flex-start' , flexWrap : 'wrap' } } >
< div style = { { flex : '0 0 560px' , minWidth : 0 } } >
@ -483,7 +493,7 @@ function FieldSpecEditor(props: { module: Module; isOwner: boolean; onSaved: ()
return (
< >
< h3 style = { { marginTop : 20 } } > Fields — { props . module .name } < / h3 >
< h3 style = { { marginTop : 20 } } > Fields < span style = { { fontWeight : 400 , fontSize : 13 , opacity : 0.65 } } > — the details you record for each client on this module < / span > < / h3 >
{ ! props . isOwner ? (
props . module .fieldSpec.length === 0 ? < EmptyState > No fields defined . < / EmptyState > : (
< DataTable