@ -1,13 +1,15 @@
import { useEffect , useMemo , useState } from 'react'
import { formatINR , fromRupees } from '@sims/domain'
import {
Badge , Button , DataTable, Dialog , EmptyState , ErrorState , Field , FormField , FormGrid , Notice ,
PageHeader, Skeleton , Toolbar , useToast ,
Badge , Button , ConfirmDialog, DataTable, Dialog , EmptyState , ErrorState , Field , FormField ,
FormGrid, Notice , PageHeader, Skeleton , Toolbar , useToast ,
} from '@sims/ui'
import {
addPrice , createModule , downloadModuleClientsCsv , getModuleClients , getModules , getPrices ,
notifyModuleClients , patchModule , previewSample , role ,
KIND_LABEL , type Kind , type Module ,
addPrice , assignClientModule , createModule , downloadModuleClientsCsv , getClients ,
getModuleClients , getModules , getPrices , notifyModuleClients , patchClientModule , patchModule ,
previewSample , role ,
CLIENT_MODULE_STATUSES , KIND_LABEL ,
type Client , type ClientModuleStatus , type Kind , type Module ,
} from '../api'
import { LivePreview } from '../components/LivePreview'
import { useData } from './Clients'
@ -85,6 +87,44 @@ function ModuleClients(props: { module: Module }) {
const [ result , setResult ] = useState < string | undefined > ( )
const [ err , setErr ] = useState < string | undefined > ( )
// D18 WS-G full-edit: one row in edit mode at a time; writes go through the SAME
// audited repo calls Client 360 uses (patchClientModule / assignClientModule).
const [ edit , setEdit ] = useState < {
cmId : string ; kind : Kind ; edition : string ; renewal : string ; status : ClientModuleStatus
} | undefined > ( )
const [ unassign , setUnassign ] = useState < { cmId : string ; clientName : string } | undefined > ( )
const [ assigning , setAssigning ] = useState ( false )
const [ assignQ , setAssignQ ] = useState ( '' )
const [ assignHits , setAssignHits ] = useState < Client [ ] > ( [ ] )
const [ assignPick , setAssignPick ] = useState < Client | undefined > ( )
const [ assignKind , setAssignKind ] = useState < Kind > ( props . module .allowedKinds [ 0 ] ? ? 'yearly' )
const [ assignEdition , setAssignEdition ] = useState ( 'standard' )
const saveEdit = ( ) = > {
if ( edit === undefined || busy ) return
setBusy ( true ) ; setErr ( undefined )
patchClientModule ( edit . cmId , {
kind : edit.kind , edition : edit.edition ,
nextRenewal : edit.renewal === '' ? null : edit . renewal , status : edit.status ,
} )
. then ( ( ) = > { setEdit ( undefined ) ; roster . reload ( ) } )
. catch ( ( e : Error ) = > setErr ( e . message ) )
. finally ( ( ) = > setBusy ( false ) )
}
const searchAssign = ( q : string ) = > {
setAssignQ ( q ) ; setAssignPick ( undefined )
if ( q . trim ( ) . length < 2 ) { setAssignHits ( [ ] ) ; return }
getClients ( q ) . then ( ( cs ) = > setAssignHits ( cs . slice ( 0 , 6 ) ) ) . catch ( ( ) = > setAssignHits ( [ ] ) )
}
const doAssign = ( ) = > {
if ( assignPick === undefined || busy ) return
setBusy ( true ) ; setErr ( undefined )
assignClientModule ( assignPick . id , { module Id : props . module .id , kind : assignKind , edition : assignEdition } )
. then ( ( ) = > { setAssigning ( false ) ; setAssignQ ( '' ) ; setAssignPick ( undefined ) ; roster . reload ( ) } )
. catch ( ( e : Error ) = > setErr ( e . message ) )
. finally ( ( ) = > setBusy ( false ) )
}
const doNotify = ( ) = > {
setBusy ( true ) ; setErr ( undefined ) ; setResult ( undefined )
notifyModuleClients ( props . module .id , { subject , body : bodyText } )
@ -103,6 +143,9 @@ function ModuleClients(props: { module: Module }) {
< section >
< h2 > Clients on this module < / h2 >
< Toolbar >
< Button onClick = { ( ) = > setAssigning ( ( v ) = > ! v ) } >
{ assigning ? 'Close assign' : 'Assign client…' }
< / Button >
{ managerial && (
< Button onClick = { ( ) = > setNotifying ( ( v ) = > ! v ) } >
{ notifying ? 'Close composer' : ` Notify all ${ data !== undefined ? ` ( ${ data . total } ) ` : '' } ` }
@ -112,6 +155,32 @@ function ModuleClients(props: { module: Module }) {
Export CSV
< / Button >
< / Toolbar >
{ assigning && (
< div className = "wf-form" >
< Field label = "Client (search by name/code)" >
< input value = { assignPick !== undefined ? ` ${ assignPick . code } · ${ assignPick . name } ` : assignQ }
onChange = { ( e ) = > searchAssign ( e . target . value ) } / >
< / Field >
{ assignPick === undefined && assignHits . length > 0 && (
< div style = { { display : 'flex' , gap : 6 , flexWrap : 'wrap' , marginBottom : 8 } } >
{ assignHits . map ( ( c ) = > (
< Button key = { c . id } onClick = { ( ) = > setAssignPick ( c ) } > { c . code } · { c . name } < / Button >
) ) }
< / div >
) }
< Field label = "Kind" >
< select className = "wf" value = { assignKind } onChange = { ( e ) = > setAssignKind ( e . target . value as Kind ) } >
{ props . module .allowedKinds.map ( ( k ) = > < option key = { k } value = { k } > { KIND_LABEL [ k ] } < / option > ) }
< / select >
< / Field >
< Field label = "Edition / pack" >
< input value = { assignEdition } onChange = { ( e ) = > setAssignEdition ( e . target . value ) } / >
< / Field >
< Button tone = "primary" onClick = { ( ) = > { if ( assignPick !== undefined && ! busy ) doAssign ( ) } } >
{ busy ? 'Assigning…' : 'Assign to module' }
< / Button >
< / div >
) }
{ notifying && managerial && (
< div className = "wf-form" >
< Field label = "Subject" > < input value = { subject } onChange = { ( e ) = > setSubject ( e . target . value ) } / > < / Field >
@ -137,15 +206,64 @@ function ModuleClients(props: { module: Module }) {
{ key : 'client' , label : 'Client' } , { key : 'code' , label : 'Code' , mono : true } ,
{ key : 'status' , label : 'Status' } , { key : 'kindEdition' , label : 'Kind · Edition' } ,
{ key : 'pays' , label : 'Pays' } , { key : 'renewal' , label : 'Next renewal' } ,
{ key : 'actions' , label : '' } ,
] }
rows = { data . clients . map ( ( c ) = > ( {
client : c.clientName , code : c.clientCode ,
status : < Badge tone = { c . status === 'live' ? 'ok' : undefined } > { c . status } < / Badge > ,
kindEdition : ` ${ KIND_LABEL [ c . kind ] } · ${ c . edition } ` ,
pays : c.pricePaise !== null ? formatINR ( c . pricePaise ) : '—' ,
renewal : c.nextRenewal ? ? '—' ,
} ) ) }
rows = { data . clients . map ( ( c ) = > {
const editing = edit !== undefined && edit . cmId === c . cmId
if ( ! editing ) {
return {
client : c.clientName , code : c.clientCode ,
status : < Badge tone = { c . status === 'live' ? 'ok' : undefined } > { c . status } < / Badge > ,
kindEdition : ` ${ KIND_LABEL [ c . kind ] } · ${ c . edition } ` ,
pays : c.pricePaise !== null ? formatINR ( c . pricePaise ) : '—' ,
renewal : c.nextRenewal ? ? '—' ,
actions : (
< span style = { { display : 'flex' , gap : 6 } } >
< Button onClick = { ( ) = > setEdit ( { cmId : c.cmId , kind : c.kind , edition : c.edition , renewal : c.nextRenewal ? ? '' , status : c.status } ) } > Edit < / Button >
< Button tone = "danger" onClick = { ( ) = > setUnassign ( { cmId : c.cmId , clientName : c.clientName } ) } > ✕ < / Button >
< / span >
) ,
}
}
return {
client : c.clientName , code : c.clientCode ,
status : (
< select className = "wf" value = { edit . status } onChange = { ( e ) = > setEdit ( ( p ) = > p && ( { . . . p , status : e.target.value as ClientModuleStatus } ) ) } >
{ CLIENT_MODULE_STATUSES . map ( ( s ) = > < option key = { s } value = { s } > { s } < / option > ) }
< / select >
) ,
kindEdition : (
< span style = { { display : 'flex' , gap : 6 } } >
< select className = "wf" value = { edit . kind } onChange = { ( e ) = > setEdit ( ( p ) = > p && ( { . . . p , kind : e.target.value as Kind } ) ) } >
{ props . module .allowedKinds.map ( ( k ) = > < option key = { k } value = { k } > { KIND_LABEL [ k ] } < / option > ) }
< / select >
< input className = "wf" style = { { width : 90 } } value = { edit . edition } onChange = { ( e ) = > setEdit ( ( p ) = > p && ( { . . . p , edition : e.target.value } ) ) } / >
< / span >
) ,
pays : c.pricePaise !== null ? formatINR ( c . pricePaise ) : '—' ,
renewal : < input className = "wf" type = "date" value = { edit . renewal } onChange = { ( e ) = > setEdit ( ( p ) = > p && ( { . . . p , renewal : e.target.value } ) ) } / > ,
actions : (
< span style = { { display : 'flex' , gap : 6 } } >
< Button tone = "primary" onClick = { saveEdit } > { busy ? 'Saving…' : 'Save' } < / Button >
< Button onClick = { ( ) = > setEdit ( undefined ) } > Cancel < / Button >
< / span >
) ,
}
} ) }
/ >
{ unassign !== undefined && (
< ConfirmDialog
open
onClose = { ( ) = > setUnassign ( undefined ) }
onConfirm = { ( ) = > patchClientModule ( unassign . cmId , { active : false } )
. then ( ( ) = > { setUnassign ( undefined ) ; roster . reload ( ) } )
. catch ( ( e : Error ) = > setErr ( e . message ) ) }
title = "Unassign client"
body = { ` Remove ${ unassign . clientName } from ${ props . module .code } ? The link is deactivated (history kept), and the roster total shrinks. ` }
confirmLabel = "Unassign"
tone = "danger"
/ >
) }
< div className = "wf-pager" >
< span > { data . total } client ( s ) · total { formatINR ( data . totalPricePaise ) } at current price book < / span >
{ pages > 1 && (