You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
sims-hq/apps/hq/src/repos-mis.ts

94 lines
4.4 KiB
TypeScript

import type { DB } from './db'
import { duesAging } from './repos-reports'
import { ticketCounts } from './repos-tickets'
import { queueCounts } from './repos-reminders'
import { renewalsDue } from './repos-renewals'
import { smsLowCount } from './repos-sms'
/**
* Owner MIS cockpit (D27): one read-only screen of the numbers the owner checks —
* this month vs last for billing + collections, total outstanding, open pipeline value,
* renewals due, open tickets, reminder queue. Composes the SAME repo aggregates the
* Reports/Dashboard pages use, so the figures reconcile exactly. Integer paise throughout.
*/
function monthStart(iso: string): string { return iso.slice(0, 7) + '-01' }
function addMonths(ymd: string, n: number): string {
const [y, m] = ymd.split('-').map(Number)
const d = new Date(Date.UTC(y!, (m! - 1) + n, 1))
return d.toISOString().slice(0, 10)
}
export interface MisCockpit {
today: string; thisMonth: string; lastMonth: string
invoicedThisPaise: number; invoicedLastPaise: number
creditNoteThisPaise: number
collectedThisPaise: number; collectedLastPaise: number
outstandingPaise: number; outstandingClients: number
pipelineCount: number; pipelineValuePaise: number
renewals30Count: number; renewals30Paise: number
tickets: { open: number; inProgress: number; waiting: number }
reminders: { queued: number; failed: number }
smsLow: number; smsThreshold: number
}
const sum = async (db: DB, sql: string, ...a: unknown[]): Promise<number> =>
((await db.get<{ n: number }>(sql, ...a))!).n
export async function misCockpit(db: DB, today: string): Promise<MisCockpit> {
const thisMonth = monthStart(today)
const lastMonth = addMonths(thisMonth, -1)
const nextMonth = addMonths(thisMonth, 1)
// Billed (issued invoices, non-cancelled) by doc_date month.
const invoicedThis = await sum(db,
`SELECT COALESCE(SUM(payable_paise),0) AS n FROM document
WHERE doc_type='INVOICE' AND doc_no IS NOT NULL AND status<>'cancelled' AND doc_date>=? AND doc_date<?`,
thisMonth, nextMonth)
const invoicedLast = await sum(db,
`SELECT COALESCE(SUM(payable_paise),0) AS n FROM document
WHERE doc_type='INVOICE' AND doc_no IS NOT NULL AND status<>'cancelled' AND doc_date>=? AND doc_date<?`,
lastMonth, thisMonth)
const cnThis = await sum(db,
`SELECT COALESCE(SUM(payable_paise),0) AS n FROM document
WHERE doc_type='CREDIT_NOTE' AND doc_no IS NOT NULL AND status<>'cancelled' AND doc_date>=? AND doc_date<?`,
thisMonth, nextMonth)
// Cash received by received_on month.
const collectedThis = await sum(db, `SELECT COALESCE(SUM(amount_paise),0) AS n FROM payment WHERE received_on>=? AND received_on<?`, thisMonth, nextMonth)
const collectedLast = await sum(db, `SELECT COALESCE(SUM(amount_paise),0) AS n FROM payment WHERE received_on>=? AND received_on<?`, lastMonth, thisMonth)
// Outstanding = the dues-aging total (same computation the Reports page shows).
const dues = await duesAging(db, today)
const outstandingPaise = dues.reduce((s, r) => s + r.totalPaise, 0)
// Open pipeline = issued/draft quotes+proformas not yet won/lost.
const pipelineCount = await sum(db,
`SELECT COUNT(*) AS n FROM document
WHERE doc_type IN ('QUOTATION','PROFORMA') AND status IN ('draft','sent','accepted')`)
const pipelineValuePaise = await sum(db,
`SELECT COALESCE(SUM(payable_paise),0) AS n FROM document
WHERE doc_type IN ('QUOTATION','PROFORMA') AND status IN ('draft','sent','accepted')`)
// Renewals in the next 30 days (modules + AMC), reusing the renewals repo.
const to30 = new Date(Date.parse(today) + 30 * 86_400_000).toISOString().slice(0, 10)
const renewals = await renewalsDue(db, today, to30, today)
const renewals30Paise = renewals.reduce((s, r) => s + (r.amountPaise ?? 0), 0)
const tc = await ticketCounts(db)
const qc = await queueCounts(db)
const sms = await smsLowCount(db)
return {
today, thisMonth, lastMonth,
invoicedThisPaise: invoicedThis, invoicedLastPaise: invoicedLast, creditNoteThisPaise: cnThis,
collectedThisPaise: collectedThis, collectedLastPaise: collectedLast,
outstandingPaise, outstandingClients: dues.length,
pipelineCount, pipelineValuePaise,
renewals30Count: renewals.length, renewals30Paise,
tickets: { open: tc['open'] ?? 0, inProgress: tc['in_progress'] ?? 0, waiting: tc['waiting'] ?? 0 },
reminders: { queued: qc.queued, failed: qc.failed },
smsLow: sms.low, smsThreshold: sms.threshold,
}
}