import type { PrismaClient } from "@planarchy/db"; import { calculateInclusiveDays, MILLISECONDS_PER_DAY } from "./shared.js"; export interface BudgetForecastRow { projectName: string; shortCode: string; budgetCents: number; spentCents: number; burnRate: number; estimatedExhaustionDate: string | null; pctUsed: number; } export async function getDashboardBudgetForecast( db: PrismaClient, ): Promise { const projects = await db.project.findMany({ where: { status: "ACTIVE" }, select: { id: true, name: true, shortCode: true, budgetCents: true, startDate: true, endDate: true, }, }); if (projects.length === 0) return []; const projectIds = projects.map((p) => p.id); const assignments = await db.assignment.findMany({ where: { projectId: { in: projectIds }, status: { not: "CANCELLED" }, }, select: { projectId: true, startDate: true, endDate: true, dailyCostCents: true, }, }); const now = new Date(); const spentByProject = new Map(); const monthlyBurnByProject = new Map(); for (const a of assignments) { const days = calculateInclusiveDays(a.startDate, a.endDate); const totalCost = (a.dailyCostCents ?? 0) * days; spentByProject.set( a.projectId, (spentByProject.get(a.projectId) ?? 0) + totalCost, ); // Approximate monthly burn from active assignments that overlap today if (a.startDate <= now && a.endDate >= now) { // ~22 working days per month const monthlyContribution = (a.dailyCostCents ?? 0) * 22; monthlyBurnByProject.set( a.projectId, (monthlyBurnByProject.get(a.projectId) ?? 0) + monthlyContribution, ); } } const rows: BudgetForecastRow[] = projects.map((p) => { const spentCents = spentByProject.get(p.id) ?? 0; const burnRate = monthlyBurnByProject.get(p.id) ?? 0; const pctUsed = p.budgetCents > 0 ? Math.round((spentCents / p.budgetCents) * 100) : 0; let estimatedExhaustionDate: string | null = null; if (burnRate > 0 && p.budgetCents > spentCents) { const remainingCents = p.budgetCents - spentCents; const monthsRemaining = remainingCents / burnRate; const exhaustionDate = new Date( now.getTime() + monthsRemaining * 30 * MILLISECONDS_PER_DAY, ); estimatedExhaustionDate = exhaustionDate.toISOString().slice(0, 10); } return { projectName: p.name, shortCode: p.shortCode, budgetCents: p.budgetCents, spentCents, burnRate, estimatedExhaustionDate, pctUsed, }; }); rows.sort((a, b) => b.pctUsed - a.pctUsed); return rows; }