import { eq, and, gte, lte, desc } from "drizzle-orm"; import { scheduledReports, kpiSnapshots } from "../../db/schema"; import type { DrizzleDB } from "../../db/config/migrations"; import type { NewScheduledReport, ScheduledReport } from "../../db/schema"; import { getAllLatestKPIs, getKPIHistory, getKPIStatus, type KPIKey } from "./kpi-service"; export interface ReportData { periodStart: Date; periodEnd: Date; kpis: Record; alerts: string[]; summary: string; } export async function generateWeeklyReport(db: DrizzleDB): Promise { const now = new Date(); const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); const kpis = await getAllLatestKPIs(db); const kpiData: ReportData["kpis"] = {}; for (const [key, snapshot] of Object.entries(kpis)) { if (!snapshot) { kpiData[key] = { value: 0, status: "warning", change: 0 }; continue; } const history = await getKPIHistory(db, key as KPIKey, weekAgo, now); const previousValue = history.length > 1 ? history[history.length - 2]?.kpiValue ?? snapshot.kpiValue : snapshot.kpiValue; const change = previousValue !== 0 ? ((snapshot.kpiValue - previousValue) / previousValue) * 100 : 0; const status = getKPIStatus(key as KPIKey, snapshot.kpiValue); kpiData[key] = { value: snapshot.kpiValue, status, change }; } const alertMessages = Object.entries(kpiData) .filter(([, data]) => data.status !== "healthy") .map(([key, data]) => `⚠️ ${key}: ${data.value.toFixed(2)} (${data.status})`); const healthyCount = Object.values(kpiData).filter((d) => d.status === "healthy").length; const totalKPIs = Object.keys(kpiData).length; const summary = `Weekly Report (${weekAgo.toISOString().split("T")[0]} - ${now.toISOString().split("T")[0]})\n${healthyCount}/${totalKPIs} KPIs healthy. ${alertMessages.length > 0 ? "Alerts: " + alertMessages.join(", ") : "No alerts."}`; return { periodStart: weekAgo, periodEnd: now, kpis: kpiData, alerts: alertMessages, summary, }; } export async function generateMonthlyReport(db: DrizzleDB): Promise { const now = new Date(); const monthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); const kpis = await getAllLatestKPIs(db); const kpiData: ReportData["kpis"] = {}; for (const [key, snapshot] of Object.entries(kpis)) { if (!snapshot) { kpiData[key] = { value: 0, status: "warning", change: 0 }; continue; } const history = await getKPIHistory(db, key as KPIKey, monthAgo, now); const previousValue = history.length > 1 ? history[history.length - 2]?.kpiValue ?? snapshot.kpiValue : snapshot.kpiValue; const change = previousValue !== 0 ? ((snapshot.kpiValue - previousValue) / previousValue) * 100 : 0; const status = getKPIStatus(key as KPIKey, snapshot.kpiValue); kpiData[key] = { value: snapshot.kpiValue, status, change }; } const alertMessages = Object.entries(kpiData) .filter(([, data]) => data.status !== "healthy") .map(([key, data]) => `⚠️ ${key}: ${data.value.toFixed(2)} (${data.status})`); const healthyCount = Object.values(kpiData).filter((d) => d.status === "healthy").length; const totalKPIs = Object.keys(kpiData).length; const summary = `Monthly Report (${monthAgo.toISOString().split("T")[0]} - ${now.toISOString().split("T")[0]})\n${healthyCount}/${totalKPIs} KPIs healthy.`; return { periodStart: monthAgo, periodEnd: now, kpis: kpiData, alerts: alertMessages, summary, }; } export async function formatReportMarkdown(report: ReportData): Promise { const lines: string[] = []; lines.push(`# KPI Report`); lines.push(``); lines.push(`**Period:** ${report.periodStart.toISOString().split("T")[0]} → ${report.periodEnd.toISOString().split("T")[0]}`); lines.push(``); lines.push(`## Summary`); lines.push(``); lines.push(report.summary); lines.push(``); lines.push(`## KPI Details`); lines.push(``); lines.push(`| KPI | Value | Status | Change |`); lines.push(`|-----|-------|--------|--------|`); for (const [key, data] of Object.entries(report.kpis)) { const statusIcon = data.status === "healthy" ? "✅" : data.status === "warning" ? "⚠️" : "🔴"; const changeStr = data.change >= 0 ? `+${data.change.toFixed(1)}%` : `${data.change.toFixed(1)}%`; lines.push(`| ${key} | ${data.value.toFixed(2)} | ${statusIcon} ${data.status} | ${changeStr} |`); } if (report.alerts.length > 0) { lines.push(``); lines.push(`## Alerts`); lines.push(``); for (const alert of report.alerts) { lines.push(`- ${alert}`); } } return lines.join("\n"); } export async function formatReportSlackBlocks(report: ReportData): Promise { const blocks: unknown[] = [ { type: "header", text: { type: "plain_text", text: `📊 KPI Report`, emoji: true, }, }, { type: "section", text: { type: "mrkdwn", text: `*Period:* ${report.periodStart.toISOString().split("T")[0]} → ${report.periodEnd.toISOString().split("T")[0]}`, }, }, ]; for (const [key, data] of Object.entries(report.kpis)) { const statusIcon = data.status === "healthy" ? "✅" : data.status === "warning" ? "⚠️" : "🔴"; const changeStr = data.change >= 0 ? `+${data.change.toFixed(1)}%` : `${data.change.toFixed(1)}%`; blocks.push({ type: "section", fields: [ { type: "mrkdwn", text: `*${key}:*\n${data.value.toFixed(2)}`, }, { type: "mrkdwn", text: `*Status:*\n${statusIcon} ${data.status}`, }, { type: "mrkdwn", text: `*Change:*\n${changeStr}`, }, ], }); } return blocks; } export async function createScheduledReport( db: DrizzleDB, input: Omit ): Promise { const result = await db.insert(scheduledReports).values({ ...input, lastRunAt: null, nextRunAt: computeNextRun(input.schedule), }).returning(); return result[0]!; } export async function getActiveScheduledReports( db: DrizzleDB ): Promise { return await db .select() .from(scheduledReports) .where(eq(scheduledReports.isActive, true)) .orderBy(scheduledReports.nextRunAt); } export async function runDueReports(db: DrizzleDB): Promise { const now = new Date(); const dueReports = await db .select() .from(scheduledReports) .where(and( eq(scheduledReports.isActive, true), lte(scheduledReports.nextRunAt, now) )); const runResults: ScheduledReport[] = []; for (const report of dueReports) { let reportData: ReportData; switch (report.reportType) { case "weekly_kpi": reportData = await generateWeeklyReport(db); break; case "monthly_kpi": reportData = await generateMonthlyReport(db); break; default: reportData = await generateWeeklyReport(db); } await db .update(scheduledReports) .set({ lastRunAt: now, nextRunAt: computeNextRun(report.schedule), }) .where(eq(scheduledReports.id, report.id)); runResults.push(report); } return runResults; } function computeNextRun(schedule: string): Date { const now = new Date(); switch (schedule) { case "weekly": const nextWeek = new Date(now); nextWeek.setDate(nextWeek.getDate() + 7); nextWeek.setHours(9, 0, 0, 0); return nextWeek; case "monthly": const nextMonth = new Date(now); nextMonth.setMonth(nextMonth.getMonth() + 1); nextMonth.setDate(1); nextMonth.setHours(9, 0, 0, 0); return nextMonth; case "daily": const tomorrow = new Date(now); tomorrow.setDate(tomorrow.getDate() + 1); tomorrow.setHours(9, 0, 0, 0); return tomorrow; default: return new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000); } }