@ -3,7 +3,7 @@ import { fyOf, validateGstin } from '@sims/domain'
import { isManagerial , login , requireAuth , requireManagerial , requireOwner } from './auth'
import { isManagerial , login , requireAuth , requireManagerial , requireOwner } from './auth'
import type { DB } from './db'
import type { DB } from './db'
import {
import {
createClient, getClient , listClients , revealClientDbPassword , setClientDbPassword ,
bulkUpdateClients, createClient, getClient , listClients , revealClientDbPassword , setClientDbPassword ,
setClientOwner , updateClient ,
setClientOwner , updateClient ,
type Client , type ClientInput , type ClientPatch ,
type Client , type ClientInput , type ClientPatch ,
} from './repos-clients'
} from './repos-clients'
@ -58,11 +58,11 @@ import {
costRanking , listAwsUsage , previousMonth , upsertAwsUsage , type UpsertAwsUsageInput ,
costRanking , listAwsUsage , previousMonth , upsertAwsUsage , type UpsertAwsUsageInput ,
} from './repos-aws'
} from './repos-aws'
import { pullMonthlyCosts } from './aws-costs'
import { pullMonthlyCosts } from './aws-costs'
import { clientProfitability , duesAging , module Revenue , type DateRange } from './repos-reports'
import { clientProfitability , duesAging , gstSummary , module Revenue , type DateRange } from './repos-reports'
import { listUsageRateCards , setUsageRateCard } from './repos-rate-card'
import { listUsageRateCards , setUsageRateCard } from './repos-rate-card'
import { makeRateLimiter } from './rate-limit'
import { makeRateLimiter } from './rate-limit'
import {
import {
addProjectMilestone , ensureProjectMilestones, listProjectMilestones , milestoneBoard ,
addProjectMilestone , bulkSetMilestone, ensureProjectMilestones, listProjectMilestones , milestoneBoard ,
projectsPendingMilestone , setMilestone ,
projectsPendingMilestone , setMilestone ,
} from './repos-milestones'
} from './repos-milestones'
import { commitImport , stageCsv , verificationReport } from './import-apex'
import { commitImport , stageCsv , verificationReport } from './import-apex'
@ -873,6 +873,7 @@ export function apiRouter(
if ( typeof req . query [ 'type' ] === 'string' ) filter . type = req . query [ 'type' ]
if ( typeof req . query [ 'type' ] === 'string' ) filter . type = req . query [ 'type' ]
if ( typeof req . query [ 'status' ] === 'string' ) filter . status = req . query [ 'status' ]
if ( typeof req . query [ 'status' ] === 'string' ) filter . status = req . query [ 'status' ]
if ( typeof req . query [ 'clientId' ] === 'string' ) filter . clientId = req . query [ 'clientId' ]
if ( typeof req . query [ 'clientId' ] === 'string' ) filter . clientId = req . query [ 'clientId' ]
if ( typeof req . query [ 'q' ] === 'string' && req . query [ 'q' ] !== '' ) filter . q = req . query [ 'q' ]
const page = Number ( req . query [ 'page' ] )
const page = Number ( req . query [ 'page' ] )
if ( Number . isInteger ( page ) && page >= 1 ) filter . page = page
if ( Number . isInteger ( page ) && page >= 1 ) filter . page = page
const pageSize = Number ( req . query [ 'pageSize' ] )
const pageSize = Number ( req . query [ 'pageSize' ] )
@ -1501,6 +1502,41 @@ export function apiRouter(
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 ) } )
}
}
} )
} )
// D26: GST filing summary (per month; invoices add, credit notes subtract).
r . get ( '/reports/gst-summary' , requireAuth , async ( req , res ) = > {
try {
res . json ( { ok : true , . . . await gstSummary ( db , rangeOf ( req ) ) } )
} catch ( err ) {
res . status ( 400 ) . json ( { ok : false , error : err instanceof Error ? err.message : String ( err ) } )
}
} )
// ---------- bulk operations (D26 — cleanup at scale, owner-only) ----------
r . post ( '/clients/bulk' , requireAuth , requireOwner , async ( req , res ) = > {
try {
const body = req . body as { ids? : unknown ; patch? : unknown }
if ( ! Array . isArray ( body . ids ) || typeof body . patch !== 'object' || body . patch === null ) {
throw new Error ( 'Provide ids [] and a patch {}' )
}
const out = await bulkUpdateClients ( db , staffId ( res ) , body . ids as string [ ] , body . patch as ClientPatch )
res . json ( { ok : true , . . . out } )
} catch ( err ) {
res . status ( 400 ) . json ( { ok : false , error : err instanceof Error ? err.message : String ( err ) } )
}
} )
r . post ( '/projects/bulk-milestone' , requireAuth , requireOwner , async ( req , res ) = > {
try {
const body = req . body as { clientModuleIds? : unknown ; key? : unknown ; done? : unknown ; doneOn? : unknown }
if ( ! Array . isArray ( body . clientModuleIds ) || typeof body . key !== 'string' || typeof body . done !== 'boolean' ) {
throw new Error ( 'Provide clientModuleIds [], key, done' )
}
const out = await bulkSetMilestone ( db , staffId ( res ) , body . clientModuleIds as string [ ] , body . key , body . done ,
typeof body . doneOn === 'string' ? body.doneOn : undefined )
res . json ( { ok : true , . . . out } )
} catch ( err ) {
res . status ( 400 ) . json ( { ok : false , error : err instanceof Error ? err.message : String ( err ) } )
}
} )
// ---------- company profile (owner-editable; PDFs read these live) ----------
// ---------- company profile (owner-editable; PDFs read these live) ----------
// D18: the composer autofills invoice due dates from this (issue also stamps with it).
// D18: the composer autofills invoice due dates from this (issue also stamps with it).