@ -13,10 +13,16 @@ import {
} from './repos-employees'
} from './repos-employees'
import {
import {
assignModule , createModule , getClientModule , getModule , listClientModules ,
assignModule , createModule , getClientModule , getModule , listClientModules ,
listClientsByModule , listModules , listPrices , setPrice , updateClientModule , updateModule ,
listClientsByModule , listModules , listPrices , revealModulePassword , setModulePassword ,
setPrice , updateClientModule , updateModule ,
type AssignModuleInput , type ClientModulePatch , type ModuleInput , type ModulePatch ,
type AssignModuleInput , type ClientModulePatch , type ModuleInput , type ModulePatch ,
type PriceInput ,
type PriceInput ,
} from './repos-modules'
} from './repos-modules'
import { createBranch , listBranches , updateBranch } from './repos-branches'
import {
createTicket , getTicket , listTickets , ticketCounts , ticketKinds , updateTicket ,
TICKET_STATUSES , type CreateTicketInput , type TicketPatch , type TicketStatus ,
} from './repos-tickets'
import {
import {
cancelDocument , convertDocument , createCreditNote , createDraft , getDocument ,
cancelDocument , convertDocument , createCreditNote , createDraft , getDocument ,
issueDocument , listDocumentEvents , listDocuments , markStatus , prepareDraft ,
issueDocument , listDocumentEvents , listDocuments , markStatus , prepareDraft ,
@ -520,12 +526,121 @@ export function apiRouter(
res . status ( 404 ) . json ( { ok : false , error : 'Client module not found' } ) ; return
res . status ( 404 ) . json ( { ok : false , error : 'Client module not found' } ) ; return
}
}
try {
try {
const cm = await updateClientModule ( db , staffId ( res ) , id , req . body as ClientModulePatch )
// D20: the portal password rides the same PATCH but is its own gated, audited
// write — owner/manager only, encrypted at rest, never in ClientModulePatch.
const { password , . . . rest } = req . body as ClientModulePatch & { password? : unknown }
if ( password !== undefined ) {
if ( typeof password !== 'string' ) throw new Error ( 'password must be a string' )
const viewer = res . locals [ 'staff' ] as { id : string ; role : string }
if ( ! isManagerial ( viewer . role ) ) {
res . status ( 403 ) . json ( { ok : false , error : 'Owner or manager only' } ) ; return
}
}
// One transaction: a bad field elsewhere must not leave a half-applied password.
const cm = await db . transaction ( async ( ) = > {
if ( typeof password === 'string' ) {
await setModulePassword ( db , staffId ( res ) , id , password , gmail ( ) . keyHex )
}
return Object . keys ( rest ) . length > 0
? await updateClientModule ( db , staffId ( res ) , id , rest )
: ( await getClientModule ( db , id ) ) !
} )
res . json ( { ok : true , clientModule : cm } )
res . json ( { ok : true , clientModule : cm } )
} catch ( err ) {
} catch ( err ) {
res . status ( 400 ) . json ( { ok : false , error : err instanceof Error ? err.message : String ( err ) } )
res . status ( 400 ) . json ( { ok : false , error : err instanceof Error ? err.message : String ( err ) } )
}
}
} )
} )
r . post ( '/client-modules/:id/reveal-password' , requireAuth , async ( req , res ) = > {
const viewer = res . locals [ 'staff' ] as { id : string ; role : string }
if ( ! isManagerial ( viewer . role ) ) {
res . status ( 403 ) . json ( { ok : false , error : 'Owner or manager only' } ) ; return
}
try {
const id = String ( req . params [ 'id' ] ? ? '' )
res . json ( { ok : true , password : await revealModulePassword ( db , viewer . id , id , gmail ( ) . keyHex ) } )
} catch ( err ) {
res . status ( 400 ) . json ( { ok : false , error : err instanceof Error ? err.message : String ( err ) } )
}
} )
// ---------- client branches (D20) ----------
r . get ( '/clients/:id/branches' , requireAuth , async ( req , res ) = > {
try {
const id = String ( req . params [ 'id' ] ? ? '' )
if ( ( await getClient ( db , id ) ) === null ) {
res . status ( 404 ) . json ( { ok : false , error : 'Client not found' } ) ; return
}
res . json ( { ok : true , branches : await listBranches ( db , id ) } )
} catch ( err ) {
res . status ( 400 ) . json ( { ok : false , error : err instanceof Error ? err.message : String ( err ) } )
}
} )
r . post ( '/clients/:id/branches' , requireAuth , async ( req , res ) = > {
try {
const body = req . body as { name? : unknown ; code? : unknown }
const branch = await createBranch ( db , staffId ( res ) , {
clientId : String ( req . params [ 'id' ] ? ? '' ) ,
name : typeof body . name === 'string' ? body . name : '' ,
. . . ( typeof body . code === 'string' ? { code : body.code } : { } ) ,
} )
res . json ( { ok : true , branch } )
} catch ( err ) {
res . status ( 400 ) . json ( { ok : false , error : err instanceof Error ? err.message : String ( err ) } )
}
} )
r . patch ( '/branches/:id' , requireAuth , async ( req , res ) = > {
try {
const body = req . body as { name? : unknown ; code? : unknown ; active? : unknown }
const branch = await updateBranch ( db , staffId ( res ) , String ( req . params [ 'id' ] ? ? '' ) , {
. . . ( typeof body . name === 'string' ? { name : body.name } : { } ) ,
. . . ( typeof body . code === 'string' ? { code : body.code } : { } ) ,
. . . ( typeof body . active === 'boolean' ? { active : body.active } : { } ) ,
} )
res . json ( { ok : true , branch } )
} catch ( err ) {
res . status ( 400 ) . json ( { ok : false , error : err instanceof Error ? err.message : String ( err ) } )
}
} )
// ---------- ticket desk (D20 — team-visible workbench) ----------
r . get ( '/tickets' , requireAuth , async ( req , res ) = > {
try {
const q = req . query
const status = typeof q [ 'status' ] === 'string' && ( TICKET_STATUSES as readonly string [ ] ) . includes ( q [ 'status' ] )
? q [ 'status' ] as TicketStatus : undefined
const out = await listTickets ( db , {
. . . ( status !== undefined ? { status } : { } ) ,
. . . ( typeof q [ 'clientId' ] === 'string' && q [ 'clientId' ] !== '' ? { clientId : q [ 'clientId' ] } : { } ) ,
. . . ( q [ 'mine' ] === '1'
? { assignedTo : staffId ( res ) }
: typeof q [ 'assignedTo' ] === 'string' && q [ 'assignedTo' ] !== '' ? { assignedTo : q [ 'assignedTo' ] } : { } ) ,
. . . ( typeof q [ 'module' ] === 'string' && q [ 'module' ] !== '' ? { module Code : q [ 'module' ] } : { } ) ,
. . . ( typeof q [ 'q' ] === 'string' && q [ 'q' ] !== '' ? { q : q [ 'q' ] } : { } ) ,
page : Number ( q [ 'page' ] ? ? 1 ) , pageSize : Number ( q [ 'pageSize' ] ? ? 50 ) ,
} )
res . json ( { ok : true , . . . out , counts : await ticketCounts ( db ) , kinds : await ticketKinds ( db ) } )
} catch ( err ) {
res . status ( 400 ) . json ( { ok : false , error : err instanceof Error ? err.message : String ( err ) } )
}
} )
r . post ( '/tickets' , requireAuth , async ( req , res ) = > {
try {
res . json ( { ok : true , ticket : await createTicket ( db , staffId ( res ) , req . body as CreateTicketInput ) } )
} catch ( err ) {
res . status ( 400 ) . json ( { ok : false , error : err instanceof Error ? err.message : String ( err ) } )
}
} )
r . patch ( '/tickets/:id' , requireAuth , async ( req , res ) = > {
const id = String ( req . params [ 'id' ] ? ? '' )
if ( ( await getTicket ( db , id ) ) === null ) {
res . status ( 404 ) . json ( { ok : false , error : 'Ticket not found' } ) ; return
}
try {
res . json ( { ok : true , ticket : await updateTicket ( db , staffId ( res ) , id , req . body as TicketPatch ) } )
} catch ( err ) {
res . status ( 400 ) . json ( { ok : false , error : err instanceof Error ? err.message : String ( err ) } )
}
} )
// ---------- documents ----------
// ---------- documents ----------
r . post ( '/documents' , requireAuth , async ( req , res ) = > {
r . post ( '/documents' , requireAuth , async ( req , res ) = > {