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.
94 lines
4.4 KiB
TypeScript
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,
|
|
}
|
|
}
|