refactor(api): split resource graph snapshot loading

This commit is contained in:
2026-03-31 22:16:31 +02:00
parent 7411aaa77b
commit a539e748a5
2 changed files with 163 additions and 124 deletions
@@ -0,0 +1,127 @@
import {
calculateSAH,
getMonthRange,
DEFAULT_CALCULATION_RULES,
} from "@capakraken/engine";
import type { CalculationRule, SpainScheduleRule, WeekdayAvailability } from "@capakraken/shared";
import type { TRPCContext } from "../trpc.js";
import { loadResourceGraphAvailability } from "./computation-graph-resource-availability.js";
export type ResourceGraphInput = {
resourceId: string;
month: string;
};
export async function loadResourceGraphSnapshot(
ctx: { db: TRPCContext["db"] },
input: ResourceGraphInput,
) {
const [year, month] = input.month.split("-").map(Number) as [number, number];
const { start: monthStart, end: monthEnd } = getMonthRange(year, month);
const resource = await ctx.db.resource.findUniqueOrThrow({
where: { id: input.resourceId },
select: {
id: true,
displayName: true,
eid: true,
fte: true,
lcrCents: true,
chargeabilityTarget: true,
countryId: true,
federalState: true,
metroCityId: true,
availability: true,
country: { select: { id: true, code: true, name: true, dailyWorkingHours: true, scheduleRules: true } },
metroCity: { select: { id: true, name: true } },
managementLevelGroup: { select: { id: true, name: true, targetPercentage: true } },
},
});
const dailyHours = resource.country?.dailyWorkingHours ?? 8;
const scheduleRules = resource.country?.scheduleRules as SpainScheduleRule | null;
const targetPct = resource.managementLevelGroup?.targetPercentage ?? (resource.chargeabilityTarget / 100);
const availability = resource.availability as WeekdayAvailability | null;
const weeklyAvailability: WeekdayAvailability = availability ?? {
monday: dailyHours,
tuesday: dailyHours,
wednesday: dailyHours,
thursday: dailyHours,
friday: dailyHours,
saturday: 0,
sunday: 0,
};
const assignments = await ctx.db.assignment.findMany({
where: {
resourceId: input.resourceId,
startDate: { lte: monthEnd },
endDate: { gte: monthStart },
status: { in: ["CONFIRMED", "ACTIVE", "PROPOSED"] },
},
select: {
id: true,
hoursPerDay: true,
startDate: true,
endDate: true,
dailyCostCents: true,
status: true,
project: {
select: {
id: true,
name: true,
shortCode: true,
budgetCents: true,
winProbability: true,
utilizationCategory: { select: { code: true } },
},
},
},
});
const availabilitySummary = await loadResourceGraphAvailability({
db: ctx.db,
resource,
weeklyAvailability,
monthStart,
monthEnd,
});
let calcRules: CalculationRule[] = DEFAULT_CALCULATION_RULES;
try {
const dbRules = await ctx.db.calculationRule.findMany({
where: { isActive: true },
orderBy: [{ priority: "desc" }],
});
if (dbRules.length > 0) {
calcRules = dbRules as unknown as CalculationRule[];
}
} catch {
// table may not exist yet
}
const sahResult = calculateSAH({
dailyWorkingHours: dailyHours,
scheduleRules,
fte: resource.fte,
periodStart: monthStart,
periodEnd: monthEnd,
publicHolidays: availabilitySummary.publicHolidayStrings,
absenceDays: availabilitySummary.absenceDateStrings,
});
return {
assignments,
calcRules,
dailyHours,
monthEnd,
monthStart,
resource,
sahResult,
scheduleRules,
targetPct,
weeklyAvailability,
...availabilitySummary,
};
}
@@ -1,90 +1,25 @@
import {
calculateSAH,
getMonthRange,
DEFAULT_CALCULATION_RULES,
} from "@capakraken/engine";
import type { CalculationRule, SpainScheduleRule, WeekdayAvailability } from "@capakraken/shared";
import type { TRPCContext } from "../trpc.js";
import { buildResourceAllocationSummary } from "./computation-graph-resource-allocation.js"; import { buildResourceAllocationSummary } from "./computation-graph-resource-allocation.js";
import { loadResourceGraphAvailability } from "./computation-graph-resource-availability.js";
import { readResourceBudgetGraph } from "./computation-graph-resource-budget.js"; import { readResourceBudgetGraph } from "./computation-graph-resource-budget.js";
import { buildResourceGraphSnapshot } from "./computation-graph-resource-graph.js"; import { buildResourceGraphSnapshot } from "./computation-graph-resource-graph.js";
import {
type ResourceGraphInput = { loadResourceGraphSnapshot,
resourceId: string; type ResourceGraphInput,
month: string; } from "./computation-graph-resource-snapshot.js";
};
export async function readResourceGraphSnapshot( export async function readResourceGraphSnapshot(
ctx: { db: TRPCContext["db"] }, ctx: Parameters<typeof loadResourceGraphSnapshot>[0],
input: ResourceGraphInput, input: ResourceGraphInput,
) { ) {
const [year, month] = input.month.split("-").map(Number) as [number, number];
const { start: monthStart, end: monthEnd } = getMonthRange(year, month);
const resource = await ctx.db.resource.findUniqueOrThrow({
where: { id: input.resourceId },
select: {
id: true,
displayName: true,
eid: true,
fte: true,
lcrCents: true,
chargeabilityTarget: true,
countryId: true,
federalState: true,
metroCityId: true,
availability: true,
country: { select: { id: true, code: true, name: true, dailyWorkingHours: true, scheduleRules: true } },
metroCity: { select: { id: true, name: true } },
managementLevelGroup: { select: { id: true, name: true, targetPercentage: true } },
},
});
const dailyHours = resource.country?.dailyWorkingHours ?? 8;
const scheduleRules = resource.country?.scheduleRules as SpainScheduleRule | null;
const targetPct = resource.managementLevelGroup?.targetPercentage ?? (resource.chargeabilityTarget / 100);
const avail = resource.availability as WeekdayAvailability | null;
const weeklyAvailability: WeekdayAvailability = avail ?? {
monday: dailyHours, tuesday: dailyHours, wednesday: dailyHours,
thursday: dailyHours, friday: dailyHours, saturday: 0, sunday: 0,
};
const assignments = await ctx.db.assignment.findMany({
where: {
resourceId: input.resourceId,
startDate: { lte: monthEnd },
endDate: { gte: monthStart },
status: { in: ["CONFIRMED", "ACTIVE", "PROPOSED"] },
},
select: {
id: true,
hoursPerDay: true,
startDate: true,
endDate: true,
dailyCostCents: true,
status: true,
project: {
select: {
id: true,
name: true,
shortCode: true,
budgetCents: true,
winProbability: true,
utilizationCategory: { select: { code: true } },
},
},
},
});
const { const {
absenceDateStrings, absenceDateStrings,
absenceDayEquivalent, absenceDayEquivalent,
absenceDays, absenceDays,
absenceHoursDeduction, absenceHoursDeduction,
assignments,
baseAvailableHours, baseAvailableHours,
baseWorkingDays, baseWorkingDays,
calcRules,
dailyHours,
effectiveAvailableHours, effectiveAvailableHours,
effectiveHoursPerWorkingDay, effectiveHoursPerWorkingDay,
effectiveWorkingDays, effectiveWorkingDays,
@@ -92,55 +27,32 @@ export async function readResourceGraphSnapshot(
holidayExamples, holidayExamples,
holidayScopeBreakdown, holidayScopeBreakdown,
holidayScopeSummary, holidayScopeSummary,
monthEnd,
monthStart,
publicHolidayCount, publicHolidayCount,
publicHolidayHoursDeduction, publicHolidayHoursDeduction,
publicHolidayStrings,
publicHolidayWorkdayCount, publicHolidayWorkdayCount,
resolvedHolidays, resolvedHolidays,
sickDayCount,
vacationDayCount,
} = await loadResourceGraphAvailability({
db: ctx.db,
resource, resource,
weeklyAvailability, sahResult,
monthStart,
monthEnd,
});
let calcRules: CalculationRule[] = DEFAULT_CALCULATION_RULES;
try {
const dbRules = await ctx.db.calculationRule.findMany({
where: { isActive: true },
orderBy: [{ priority: "desc" }],
});
if (dbRules.length > 0) {
calcRules = dbRules as unknown as CalculationRule[];
}
} catch {
// table may not exist yet
}
const sahResult = calculateSAH({
dailyWorkingHours: dailyHours,
scheduleRules, scheduleRules,
fte: resource.fte, sickDayCount,
periodStart: monthStart,
periodEnd: monthEnd,
publicHolidays: publicHolidayStrings,
absenceDays: absenceDateStrings,
});
const assignmentSummary = buildResourceAllocationSummary({
assignments,
effectiveAvailableHours,
monthStart,
monthEnd,
resourceLcrCents: resource.lcrCents,
resourceFte: resource.fte,
targetPct, targetPct,
vacationDayCount,
weeklyAvailability, weeklyAvailability,
} = await loadResourceGraphSnapshot(ctx, input);
const allocationSummary = buildResourceAllocationSummary({
assignments,
absenceDays, absenceDays,
calcRules, calcRules,
effectiveAvailableHours,
monthEnd,
monthStart,
resourceFte: resource.fte,
resourceLcrCents: resource.lcrCents,
targetPct,
weeklyAvailability,
}); });
const { nodes: budgetNodes, links: budgetLinks } = await readResourceBudgetGraph( const { nodes: budgetNodes, links: budgetLinks } = await readResourceBudgetGraph(
ctx.db, ctx.db,
@@ -176,17 +88,17 @@ export async function readResourceGraphSnapshot(
baseAvailableHours, baseAvailableHours,
effectiveAvailableHours, effectiveAvailableHours,
effectiveHoursPerWorkingDay, effectiveHoursPerWorkingDay,
totalWorkingDaysInMonth: assignmentSummary.totalWorkingDaysInMonth, totalWorkingDaysInMonth: allocationSummary.totalWorkingDaysInMonth,
totalAllocHours: assignmentSummary.totalAllocHours, totalAllocHours: allocationSummary.totalAllocHours,
totalAllocCostCents: assignmentSummary.totalAllocCostCents, totalAllocCostCents: allocationSummary.totalAllocCostCents,
totalChargeableHours: assignmentSummary.totalChargeableHours, totalChargeableHours: allocationSummary.totalChargeableHours,
totalProjectCostCents: assignmentSummary.totalProjectCostCents, totalProjectCostCents: allocationSummary.totalProjectCostCents,
hasRulesEffect: assignmentSummary.hasRulesEffect, hasRulesEffect: allocationSummary.hasRulesEffect,
dailyCostCents: assignmentSummary.dailyCostCents, dailyCostCents: allocationSummary.dailyCostCents,
avgHoursPerDay: assignmentSummary.avgHoursPerDay, avgHoursPerDay: allocationSummary.avgHoursPerDay,
utilizationPct: assignmentSummary.utilizationPct, utilizationPct: allocationSummary.utilizationPct,
forecast: assignmentSummary.forecast, forecast: allocationSummary.forecast,
chargeableHours: assignmentSummary.chargeableHours, chargeableHours: allocationSummary.chargeableHours,
budgetNodes, budgetNodes,
budgetLinks, budgetLinks,
resolvedHolidays: resolvedHolidays.map((holiday) => ({ resolvedHolidays: resolvedHolidays.map((holiday) => ({
@@ -196,7 +108,7 @@ export async function readResourceGraphSnapshot(
calendarName: holiday.calendarName, calendarName: holiday.calendarName,
sourceType: holiday.sourceType ?? null, sourceType: holiday.sourceType ?? null,
})), })),
assignmentBreakdown: assignmentSummary.assignmentBreakdown, assignmentBreakdown: allocationSummary.assignmentBreakdown,
absenceDayEquivalent, absenceDayEquivalent,
}); });
} }