139 lines
4.2 KiB
TypeScript
139 lines
4.2 KiB
TypeScript
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<ReturnType<typeof loadResourceGraphAvailability>>["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,
|
|
};
|
|
}
|