@ -18,6 +18,8 @@ export interface Client {
anydesk? : string ; os? : string ; district? : string ; sector? : string
/** D35: 'PACS' (co-op society) or 'Store' (retail). */
clientType : string
/** D37: society category (Service Bank / Vanitha / Housing / Employees / …). */
category : string
/** True when an encrypted DB password is stored (the UI shows •••• + Reveal). */
hasDbPassword : boolean
}
@ -26,11 +28,27 @@ interface ClientRow {
id : string ; code : string ; name : string ; gstin : string | null ; state_code : string
address : string ; contacts : string ; status : string ; owner_id : string | null ; notes : string
anydesk : string | null ; os : string | null ; district : string | null ; sector : string | null
client_type : string | null
client_type : string | null ; category : string | null
db_password_enc : string | null
source : string ; created_at : string
}
/** Derive the society category from a name (same taxonomy as the DB backfill; most specific first). */
export function categoryFromName ( name : string ) : string {
const n = name . toUpperCase ( )
if ( /STORE[S]?$/ . test ( name . trim ( ) . toUpperCase ( ) ) ) return 'Store'
if ( n . includes ( 'VANITHA' ) ) return 'Vanitha'
if ( n . includes ( 'HOUSING' ) ) return 'Housing'
if ( n . includes ( 'URBAN' ) ) return 'Urban Bank'
if ( /EMPLOYEE|TEACHER|STAFF/ . test ( n ) ) return 'Employees'
if ( n . includes ( 'MARKETING' ) ) return 'Marketing'
if ( /AGRICULTUR|RURAL DEVELOPMENT|RUBBER/ . test ( n ) ) return 'Agricultural'
if ( /SERVICE CO-OPERATIVE|SERVICE BANK|SCB/ . test ( n ) ) return 'Service Bank'
if ( n . includes ( 'BANK' ) ) return 'Bank (other)'
if ( /SOCIET|CO-OPERATIVE|AICOS/ . test ( n ) ) return 'Society (other)'
return 'Other'
}
function toClient ( r : ClientRow ) : Client {
return {
id : r.id , code : r.code , name : r.name ,
@ -44,6 +62,7 @@ function toClient(r: ClientRow): Client {
. . . ( r . district !== null ? { district : r.district } : { } ) ,
. . . ( r . sector !== null ? { sector : r.sector } : { } ) ,
clientType : r.client_type ? ? 'PACS' ,
category : r.category ? ? 'Other' ,
hasDbPassword : r.db_password_enc !== null , // the ciphertext itself never leaves the repo
}
}
@ -54,7 +73,7 @@ function assertGstin(gstin: string): void {
}
export async function listClients (
db : DB , q? : string , filters : { district? : string ; sector? : string ; clientType? : string } = { } ,
db : DB , q? : string , filters : { district? : string ; sector? : string ; clientType? : string ; category? : string } = { } ,
) : Promise < Client [ ] > {
let where = ' WHERE 1=1'
const args : unknown [ ] = [ ]
@ -77,6 +96,9 @@ export async function listClients(
if ( filters . clientType !== undefined && filters . clientType !== '' ) {
where += ` AND client_type=? ` ; args . push ( filters . clientType )
}
if ( filters . category !== undefined && filters . category !== '' ) {
where += ` AND category=? ` ; args . push ( filters . category )
}
const rows = await db . all < ClientRow > ( ` SELECT * FROM client ${ where } ORDER BY name ` , . . . args )
return rows . map ( toClient )
}
@ -89,7 +111,7 @@ export async function getClient(db: DB, id: string): Promise<Client | null> {
export interface ClientInput {
code? : string ; name : string ; gstin? : string ; stateCode : string ; address? : string
contacts? : ClientContact [ ] ; status? : Client [ 'status' ] ; notes? : string
anydesk? : string ; os? : string ; district? : string ; sector? : string ; clientType? : string
anydesk? : string ; os? : string ; district? : string ; sector? : string ; clientType? : string ; category? : string
}
/** New clients default their type from the name (…STORE → Store), like the backfill. */
@ -104,13 +126,14 @@ export async function createClient(db: DB, userId: string, input: ClientInput):
const code = input . code ? ? ` CL ${ String ( n + 1 ) . padStart ( 4 , '0' ) } `
await db . run (
` INSERT INTO client (id, code, name, gstin, state_code, address, contacts, status,
anydesk , os , district , sector , client_type , notes, created_at )
VALUES ( ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? )` ,
anydesk , os , district , sector , client_type , category, notes, created_at )
VALUES ( ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? )` ,
id , code , input . name , input . gstin ? ? null , input . stateCode ,
input . address ? ? '' , JSON . stringify ( input . contacts ? ? [ ] ) ,
input . status ? ? 'active' ,
input . anydesk ? ? null , input . os ? ? null , input . district ? ? null , input . sector ? ? null ,
input . clientType ? ? typeFromName ( input . name ) ,
input . category ? ? categoryFromName ( input . name ) ,
input . notes ? ? '' , new Date ( ) . toISOString ( ) ,
)
const client = ( await getClient ( db , id ) ) !
@ -123,7 +146,7 @@ export interface ClientPatch {
contacts? : ClientContact [ ] ; status? : Client [ 'status' ] ; notes? : string
/** '' clears the field (stored NULL). */
anydesk? : string ; os? : string ; district? : string ; sector? : string
clientType? : string
clientType? : string ; category? : string
}
export async function updateClient ( db : DB , userId : string , id : string , patch : ClientPatch ) : Promise < Client > {
@ -140,6 +163,7 @@ export async function updateClient(db: DB, userId: string, id: string, patch: Cl
if ( patch . status !== undefined ) { sets . push ( 'status=?' ) ; args . push ( patch . status ) }
if ( patch . notes !== undefined ) { sets . push ( 'notes=?' ) ; args . push ( patch . notes ) }
if ( patch . clientType !== undefined ) { sets . push ( 'client_type=?' ) ; args . push ( patch . clientType ) }
if ( patch . category !== undefined ) { sets . push ( 'category=?' ) ; args . push ( patch . category ) }
const optText = ( col : string , v : string | undefined ) = > {
if ( v !== undefined ) { sets . push ( ` ${ col } =? ` ) ; args . push ( v . trim ( ) === '' ? null : v . trim ( ) ) }
}