@ -46,7 +46,7 @@ export function Modules() {
< div className = "wf-page" >
< PageHeader
title = "Modules"
desc = "What we sell — each with a dated price book (later rows win from their effective date) ."
desc = "What we sell — each with a dated price book . Click a module to edit its details, fields, quote content and prices ."
actions = { isOwner
? < Button tone = "primary" onClick = { ( ) = > setCreating ( true ) } > New module < / Button >
: < Badge > read - only ( staff ) < / Badge > }
@ -73,6 +73,7 @@ export function Modules() {
) }
{ selected !== undefined && (
< >
< ModuleDetails key = { ` md- ${ selected . id } ` } module = { selected } isOwner = { isOwner } onSaved = { ( ) = > module s.reload ( ) } / >
< 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 ( ) } / >
@ -315,6 +316,66 @@ function ModuleClients(props: { module: Module }) {
}
/** Per-module "what's included" lines — prefilled into the composer, printed under the quote line. */
/ * * 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 } ) {
const toast = useToast ( )
const m = props . module
const [ name , setName ] = useState ( m . name )
const [ sac , setSac ] = useState ( m . sac )
const [ kinds , setKinds ] = useState < Kind [ ] > ( [ . . . m . allowedKinds ] )
const [ multi , setMulti ] = useState ( m . multiSubscription )
const [ active , setActive ] = useState ( m . active )
const [ error , setError ] = useState < string | undefined > ( )
const [ busy , setBusy ] = useState ( false )
const toggleKind = ( k : Kind ) = >
setKinds ( ( ks ) = > ( ks . includes ( k ) ? ks . filter ( ( x ) = > x !== k ) : [ . . . ks , k ] ) )
const save = ( ) = > {
if ( busy ) return
if ( name . trim ( ) === '' ) { setError ( 'Name is required' ) ; return }
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 ( ) } )
. catch ( ( e : Error ) = > setError ( e . message ) )
. finally ( ( ) = > setBusy ( false ) )
}
if ( ! props . isOwner ) return null
return (
< >
< h3 style = { { marginTop : 20 } } > Edit module — < span className = "mono" > { m . code } < / span > < / 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 >
< Field label = "SAC" > < input className = "wf mono" style = { { width : 120 } } value = { sac } onChange = { ( e ) = > setSac ( e . target . value ) } / > < / Field >
< / div >
< div style = { { marginTop : 12 } } >
< div style = { { fontSize : 12 , opacity : 0.7 , marginBottom : 4 } } > Billing kinds < / div >
< div style = { { display : 'flex' , gap : 14 , flexWrap : 'wrap' } } >
{ ALL_KINDS . map ( ( k ) = > (
< label key = { k } style = { { display : 'inline-flex' , gap : 6 , alignItems : 'center' } } >
< input type = "checkbox" checked = { kinds . includes ( k ) } onChange = { ( ) = > toggleKind ( k ) } / > { KIND_LABEL [ k ] }
< / label >
) ) }
< / div >
< / div >
< div style = { { display : 'flex' , gap : 20 , marginTop : 12 } } >
< label style = { { display : 'inline-flex' , gap : 6 , alignItems : 'center' } } > < input type = "checkbox" checked = { multi } onChange = { ( e ) = > setMulti ( e . target . checked ) } / > Multi - subscription < / label >
< label style = { { display : 'inline-flex' , gap : 6 , alignItems : 'center' } } > < input type = "checkbox" checked = { active } onChange = { ( e ) = > setActive ( e . target . checked ) } / > Active < / label >
< / div >
< Toolbar >
< Button tone = "primary" disabled = { busy } onClick = { save } > { busy ? 'Saving…' : 'Save module' } < / Button >
< / Toolbar >
{ error !== undefined && < Notice tone = "err" > { error } < / Notice > }
< / div >
< / >
)
}
function QuoteContent ( props : { module : Module ; isOwner : boolean ; onSaved : ( ) = > void } ) {
const toast = useToast ( )
const [ text , setText ] = useState ( props . module .quoteContent.join ( '\n' ) )