diff --git a/packages/api/src/router/computation-graph-resource-snapshot.ts b/packages/api/src/router/computation-graph-resource-snapshot.ts new file mode 100644 index 0000000..b3968c5 --- /dev/null +++ b/packages/api/src/router/computation-graph-resource-snapshot.ts @@ -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, + }; +} diff --git a/packages/api/src/router/computation-graph-resource.ts b/packages/api/src/router/computation-graph-resource.ts index 8167386..ac0f540 100644 --- a/packages/api/src/router/computation-graph-resource.ts +++ b/packages/api/src/router/computation-graph-resource.ts @@ -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 { loadResourceGraphAvailability } from "./computation-graph-resource-availability.js"; import { readResourceBudgetGraph } from "./computation-graph-resource-budget.js"; import { buildResourceGraphSnapshot } from "./computation-graph-resource-graph.js"; - -type ResourceGraphInput = { - resourceId: string; - month: string; -}; +import { + loadResourceGraphSnapshot, + type ResourceGraphInput, +} from "./computation-graph-resource-snapshot.js"; export async function readResourceGraphSnapshot( - ctx: { db: TRPCContext["db"] }, + ctx: Parameters[0], 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 { absenceDateStrings, absenceDayEquivalent, absenceDays, absenceHoursDeduction, + assignments, baseAvailableHours, baseWorkingDays, + calcRules, + dailyHours, effectiveAvailableHours, effectiveHoursPerWorkingDay, effectiveWorkingDays, @@ -92,55 +27,32 @@ export async function readResourceGraphSnapshot( holidayExamples, holidayScopeBreakdown, holidayScopeSummary, + monthEnd, + monthStart, publicHolidayCount, publicHolidayHoursDeduction, - publicHolidayStrings, publicHolidayWorkdayCount, resolvedHolidays, - sickDayCount, - vacationDayCount, - } = 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, + sahResult, scheduleRules, - fte: resource.fte, - periodStart: monthStart, - periodEnd: monthEnd, - publicHolidays: publicHolidayStrings, - absenceDays: absenceDateStrings, - }); - - const assignmentSummary = buildResourceAllocationSummary({ - assignments, - effectiveAvailableHours, - monthStart, - monthEnd, - resourceLcrCents: resource.lcrCents, - resourceFte: resource.fte, + sickDayCount, targetPct, + vacationDayCount, weeklyAvailability, + } = await loadResourceGraphSnapshot(ctx, input); + + const allocationSummary = buildResourceAllocationSummary({ + assignments, absenceDays, calcRules, + effectiveAvailableHours, + monthEnd, + monthStart, + resourceFte: resource.fte, + resourceLcrCents: resource.lcrCents, + targetPct, + weeklyAvailability, }); const { nodes: budgetNodes, links: budgetLinks } = await readResourceBudgetGraph( ctx.db, @@ -176,17 +88,17 @@ export async function readResourceGraphSnapshot( baseAvailableHours, effectiveAvailableHours, effectiveHoursPerWorkingDay, - totalWorkingDaysInMonth: assignmentSummary.totalWorkingDaysInMonth, - totalAllocHours: assignmentSummary.totalAllocHours, - totalAllocCostCents: assignmentSummary.totalAllocCostCents, - totalChargeableHours: assignmentSummary.totalChargeableHours, - totalProjectCostCents: assignmentSummary.totalProjectCostCents, - hasRulesEffect: assignmentSummary.hasRulesEffect, - dailyCostCents: assignmentSummary.dailyCostCents, - avgHoursPerDay: assignmentSummary.avgHoursPerDay, - utilizationPct: assignmentSummary.utilizationPct, - forecast: assignmentSummary.forecast, - chargeableHours: assignmentSummary.chargeableHours, + totalWorkingDaysInMonth: allocationSummary.totalWorkingDaysInMonth, + totalAllocHours: allocationSummary.totalAllocHours, + totalAllocCostCents: allocationSummary.totalAllocCostCents, + totalChargeableHours: allocationSummary.totalChargeableHours, + totalProjectCostCents: allocationSummary.totalProjectCostCents, + hasRulesEffect: allocationSummary.hasRulesEffect, + dailyCostCents: allocationSummary.dailyCostCents, + avgHoursPerDay: allocationSummary.avgHoursPerDay, + utilizationPct: allocationSummary.utilizationPct, + forecast: allocationSummary.forecast, + chargeableHours: allocationSummary.chargeableHours, budgetNodes, budgetLinks, resolvedHolidays: resolvedHolidays.map((holiday) => ({ @@ -196,7 +108,7 @@ export async function readResourceGraphSnapshot( calendarName: holiday.calendarName, sourceType: holiday.sourceType ?? null, })), - assignmentBreakdown: assignmentSummary.assignmentBreakdown, + assignmentBreakdown: allocationSummary.assignmentBreakdown, absenceDayEquivalent, }); }