import type { PrismaClient } from "@planarchy/db"; import { calculateInclusiveDays } from "./shared.js"; export interface ProjectHealthRow { projectName: string; shortCode: string; budgetHealth: number; staffingHealth: number; timelineHealth: number; compositeScore: number; } export async function getDashboardProjectHealth( db: PrismaClient, ): Promise { const projects = await db.project.findMany({ where: { status: "ACTIVE" }, select: { id: true, name: true, shortCode: true, budgetCents: true, endDate: true, demandRequirements: { select: { id: true, headcount: true, status: true, assignments: { where: { status: { not: "CANCELLED" } }, select: { id: true }, }, }, }, }, }); if (projects.length === 0) return []; const projectIds = projects.map((p) => p.id); // Fetch assignments for budget calculation const assignments = await db.assignment.findMany({ where: { projectId: { in: projectIds }, status: { not: "CANCELLED" }, }, select: { projectId: true, startDate: true, endDate: true, dailyCostCents: true, }, }); const spentByProject = new Map(); for (const a of assignments) { const days = calculateInclusiveDays(a.startDate, a.endDate); const cost = (a.dailyCostCents ?? 0) * days; spentByProject.set(a.projectId, (spentByProject.get(a.projectId) ?? 0) + cost); } const now = new Date(); const rows: ProjectHealthRow[] = projects.map((p) => { // Budget health: 100 - pctUsed (capped at 100) const spentCents = spentByProject.get(p.id) ?? 0; const pctUsed = p.budgetCents > 0 ? Math.round((spentCents / p.budgetCents) * 100) : 0; const budgetHealth = Math.max(0, 100 - Math.min(pctUsed, 100)); // Staffing health: filledDemands / totalDemands * 100 let totalDemands = 0; let filledDemands = 0; for (const dr of p.demandRequirements) { totalDemands += dr.headcount; filledDemands += Math.min(dr.assignments.length, dr.headcount); } const staffingHealth = totalDemands > 0 ? Math.round((filledDemands / totalDemands) * 100) : 100; // Timeline health: 100 if end date > today, else 0 const timelineHealth = p.endDate > now ? 100 : 0; // Composite = average of 3 dimensions const compositeScore = Math.round( (budgetHealth + staffingHealth + timelineHealth) / 3, ); return { projectName: p.name, shortCode: p.shortCode, budgetHealth, staffingHealth, timelineHealth, compositeScore, }; }); rows.sort((a, b) => a.compositeScore - b.compositeScore); return rows; }