export interface Anomaly { type: "budget" | "staffing" | "utilization" | "timeline"; severity: "warning" | "critical"; entityId: string; entityName: string; message: string; } interface InsightDemandRecord { headcount: number; startDate: Date; endDate: Date; _count: { assignments: number; }; } interface InsightProjectAssignmentRecord { resourceId: string; startDate: Date; endDate: Date; hoursPerDay: number; dailyCostCents: number; status: string; } interface InsightProjectRecord { id: string; name: string; budgetCents: number; startDate: Date; endDate: Date; demandRequirements: InsightDemandRecord[]; assignments: InsightProjectAssignmentRecord[]; } interface InsightResourceRecord { id: string; displayName: string; availability: unknown; } interface InsightAssignmentLoadRecord { resourceId: string; hoursPerDay: number; } export interface InsightSnapshot { anomalies: Anomaly[]; summary: { total: number; criticalCount: number; budget: number; staffing: number; timeline: number; utilization: number; }; } export interface InsightsDbAccess { project: { findMany(args: Record): Promise; }; resource: { findMany(args: Record): Promise; }; assignment: { findMany(args: Record): Promise; }; } function countBusinessDays(start: Date, end: Date): number { let count = 0; const date = new Date(start); while (date <= end) { const dayOfWeek = date.getDay(); if (dayOfWeek !== 0 && dayOfWeek !== 6) { count++; } date.setDate(date.getDate() + 1); } return count; } async function loadInsightProjects(db: InsightsDbAccess["project"]) { return db.findMany({ where: { status: { in: ["ACTIVE", "DRAFT"] } }, include: { demandRequirements: { select: { headcount: true, startDate: true, endDate: true, _count: { select: { assignments: true } }, }, }, assignments: { select: { resourceId: true, startDate: true, endDate: true, hoursPerDay: true, dailyCostCents: true, status: true, }, }, }, }) as Promise; } async function loadInsightResources(db: InsightsDbAccess["resource"]) { return db.findMany({ where: { isActive: true }, select: { id: true, displayName: true, availability: true, }, }) as Promise; } async function loadInsightAssignmentLoads(db: InsightsDbAccess["assignment"], now: Date) { const periodStart = new Date(now.getFullYear(), now.getMonth(), 1); const periodEnd = new Date(now.getFullYear(), now.getMonth() + 1, 0); return db.findMany({ where: { status: { in: ["ACTIVE", "CONFIRMED"] }, startDate: { lte: periodEnd }, endDate: { gte: periodStart }, }, select: { resourceId: true, hoursPerDay: true, }, }) as Promise; } function summarizeAnomalies(anomalies: Anomaly[]): InsightSnapshot["summary"] { return anomalies.reduce((summary, anomaly) => { summary.total += 1; summary[anomaly.type] += 1; if (anomaly.severity === "critical") { summary.criticalCount += 1; } return summary; }, { total: 0, criticalCount: 0, budget: 0, staffing: 0, timeline: 0, utilization: 0, }); } export async function buildInsightSnapshot( db: InsightsDbAccess, now = new Date(), ): Promise { const [projects, resources, activeAssignments] = await Promise.all([ loadInsightProjects(db.project), loadInsightResources(db.resource), loadInsightAssignmentLoads(db.assignment, now), ]); const twoWeeksFromNow = new Date(now.getTime() + 14 * 24 * 60 * 60 * 1000); const anomalies: Anomaly[] = []; for (const project of projects) { if (project.budgetCents > 0) { const totalDays = countBusinessDays(project.startDate, project.endDate); const elapsedDays = countBusinessDays(project.startDate, now < project.endDate ? now : project.endDate); if (totalDays > 0 && elapsedDays > 0) { const expectedBurnRate = elapsedDays / totalDays; const totalCostCents = project.assignments.reduce((sum, assignment) => { const assignmentStart = assignment.startDate < project.startDate ? project.startDate : assignment.startDate; const assignmentEnd = assignment.endDate > now ? now : assignment.endDate; if (assignmentEnd < assignmentStart) { return sum; } return sum + assignment.dailyCostCents * countBusinessDays(assignmentStart, assignmentEnd); }, 0); const actualBurnRate = totalCostCents / project.budgetCents; if (actualBurnRate > expectedBurnRate * 1.2) { const overSpendPercent = Math.round(((actualBurnRate - expectedBurnRate) / expectedBurnRate) * 100); anomalies.push({ type: "budget", severity: actualBurnRate > expectedBurnRate * 1.5 ? "critical" : "warning", entityId: project.id, entityName: project.name, message: `Burning budget ${overSpendPercent}% faster than expected. ${Math.round(actualBurnRate * 100)}% spent at ${Math.round(expectedBurnRate * 100)}% timeline.`, }); } } } const upcomingDemands = project.demandRequirements.filter( (demand) => demand.startDate <= twoWeeksFromNow && demand.endDate >= now, ); for (const demand of upcomingDemands) { const unfilledCount = demand.headcount - demand._count.assignments; const unfillPct = demand.headcount > 0 ? unfilledCount / demand.headcount : 0; if (unfillPct > 0.3) { anomalies.push({ type: "staffing", severity: unfillPct > 0.6 ? "critical" : "warning", entityId: project.id, entityName: project.name, message: `${unfilledCount} of ${demand.headcount} positions unfilled, starting ${demand.startDate.toISOString().slice(0, 10)}.`, }); } } const overrunAssignments = project.assignments.filter( (assignment) => assignment.endDate > project.endDate && (assignment.status === "ACTIVE" || assignment.status === "CONFIRMED"), ); if (overrunAssignments.length > 0) { anomalies.push({ type: "timeline", severity: "warning", entityId: project.id, entityName: project.name, message: `${overrunAssignments.length} assignment(s) extend beyond the project end date (${project.endDate.toISOString().slice(0, 10)}).`, }); } } const resourceHoursMap = new Map(); for (const assignment of activeAssignments) { const currentHours = resourceHoursMap.get(assignment.resourceId) ?? 0; resourceHoursMap.set(assignment.resourceId, currentHours + assignment.hoursPerDay); } for (const resource of resources) { const availability = resource.availability as Record | null; if (!availability) { continue; } const dailyAvailableHours = Object.values(availability).reduce((sum, hours) => sum + (hours ?? 0), 0) / 5; if (dailyAvailableHours <= 0) { continue; } const bookedHours = resourceHoursMap.get(resource.id) ?? 0; const utilizationPercent = Math.round((bookedHours / dailyAvailableHours) * 100); if (utilizationPercent > 110) { anomalies.push({ type: "utilization", severity: utilizationPercent > 130 ? "critical" : "warning", entityId: resource.id, entityName: resource.displayName, message: `Resource at ${utilizationPercent}% utilization (${bookedHours.toFixed(1)}h/${dailyAvailableHours.toFixed(1)}h per day).`, }); } else if (utilizationPercent < 40 && bookedHours > 0) { anomalies.push({ type: "utilization", severity: "warning", entityId: resource.id, entityName: resource.displayName, message: `Resource at only ${utilizationPercent}% utilization (${bookedHours.toFixed(1)}h/${dailyAvailableHours.toFixed(1)}h per day).`, }); } } anomalies.sort((left, right) => { if (left.severity !== right.severity) { return left.severity === "critical" ? -1 : 1; } return left.type.localeCompare(right.type); }); return { anomalies, summary: summarizeAnomalies(anomalies), }; }