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 => ((await db.get<{ n: number }>(sql, ...a))!).n export async function misCockpit(db: DB, today: string): Promise { 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'cancelled' AND doc_date>=? AND doc_date'cancelled' AND doc_date>=? AND doc_date=? AND received_on=? AND received_on 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, } }