import { calculateAllocation, deriveResourceForecast, type AssignmentSlice, } from "@capakraken/engine"; import type { CalculationRule, WeekdayAvailability } from "@capakraken/shared"; import type { loadResourceGraphAvailability } from "./computation-graph-resource-availability.js"; type ResourceGraphAssignment = { id: string; hoursPerDay: number; startDate: Date; endDate: Date; dailyCostCents: number | null; status: string; project: { id: string; name: string; shortCode: string; utilizationCategory: { code: string; } | null; }; }; type ResourceGraphAllocationInput = { assignments: ResourceGraphAssignment[]; absenceDays: Awaited>["absenceDays"]; calcRules: CalculationRule[]; effectiveAvailableHours: number; monthEnd: Date; monthStart: Date; resourceLcrCents: number; resourceFte: number; targetPct: number; weeklyAvailability: WeekdayAvailability; }; export function buildResourceAllocationSummary(input: ResourceGraphAllocationInput) { const slices: AssignmentSlice[] = []; const assignmentBreakdown: Array<{ id: string; projectId: string; projectName: string; projectCode: string; status: string; bookedHours: number; }> = []; let totalAllocHours = 0; let totalAllocCostCents = 0; let totalChargeableHours = 0; let totalProjectCostCents = 0; let totalWorkingDaysInMonth = 0; let hasRulesEffect = false; for (const assignment of input.assignments) { const overlapStart = new Date(Math.max(input.monthStart.getTime(), assignment.startDate.getTime())); const overlapEnd = new Date(Math.min(input.monthEnd.getTime(), assignment.endDate.getTime())); const categoryCode = assignment.project.utilizationCategory?.code ?? "Chg"; const calcResult = calculateAllocation({ lcrCents: input.resourceLcrCents, hoursPerDay: assignment.hoursPerDay, startDate: overlapStart, endDate: overlapEnd, availability: input.weeklyAvailability, absenceDays: input.absenceDays, calculationRules: input.calcRules, }); if (calcResult.workingDays <= 0 && calcResult.totalHours <= 0) { continue; } totalWorkingDaysInMonth += calcResult.workingDays; totalAllocHours += calcResult.totalHours; totalAllocCostCents += calcResult.totalCostCents; assignmentBreakdown.push({ id: assignment.id, projectId: assignment.project.id, projectName: assignment.project.name, projectCode: assignment.project.shortCode, status: assignment.status, bookedHours: calcResult.totalHours, }); if (calcResult.totalChargeableHours !== undefined) { totalChargeableHours += calcResult.totalChargeableHours; totalProjectCostCents += calcResult.totalProjectCostCents ?? calcResult.totalCostCents; hasRulesEffect = true; } else { totalChargeableHours += calcResult.totalHours; totalProjectCostCents += calcResult.totalCostCents; } slices.push({ hoursPerDay: assignment.hoursPerDay, workingDays: calcResult.workingDays, categoryCode, ...(calcResult.totalChargeableHours !== undefined ? { totalChargeableHours: calcResult.totalChargeableHours } : {}), }); } const forecast = deriveResourceForecast({ fte: input.resourceFte, targetPercentage: input.targetPct, assignments: slices, sah: input.effectiveAvailableHours, }); const dailyCostCents = input.assignments.length > 0 ? Math.round(input.assignments[0]!.hoursPerDay * input.resourceLcrCents) : 0; const avgHoursPerDay = input.assignments.length > 0 ? input.assignments.reduce((sum, assignment) => sum + assignment.hoursPerDay, 0) / input.assignments.length : 0; const utilizationPct = input.effectiveAvailableHours > 0 ? (totalAllocHours / input.effectiveAvailableHours) * 100 : 0; const chargeableHours = forecast.chg * input.effectiveAvailableHours; return { assignmentBreakdown, avgHoursPerDay, chargeableHours, dailyCostCents, forecast, hasRulesEffect, totalAllocCostCents, totalAllocHours, totalChargeableHours, totalProjectCostCents, totalWorkingDaysInMonth, utilizationPct, }; }