From 459ab6911bda7fe3565a70260d42dcb64e9a3686 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hartmut=20N=C3=B6renberg?= Date: Tue, 31 Mar 2026 22:01:29 +0200 Subject: [PATCH] refactor(api): split resource graph availability --- ...computation-graph-resource-availability.ts | 185 ++++++++++++++++++ .../src/router/computation-graph-resource.ts | 161 +++------------ 2 files changed, 212 insertions(+), 134 deletions(-) create mode 100644 packages/api/src/router/computation-graph-resource-availability.ts diff --git a/packages/api/src/router/computation-graph-resource-availability.ts b/packages/api/src/router/computation-graph-resource-availability.ts new file mode 100644 index 0000000..52ea2fe --- /dev/null +++ b/packages/api/src/router/computation-graph-resource-availability.ts @@ -0,0 +1,185 @@ +import { VacationStatus } from "@capakraken/db"; +import type { WeekdayAvailability } from "@capakraken/shared"; +import { + asHolidayResolverDb, + collectHolidayAvailability, + getResolvedCalendarHolidays, +} from "../lib/holiday-availability.js"; +import { + calculateEffectiveAvailableHours, + countEffectiveWorkingDays, + loadResourceDailyAvailabilityContexts, +} from "../lib/resource-capacity.js"; + +type ResourceLocation = { + countryId: string | null; + federalState: string | null; + metroCityId: string | null; + country?: { code?: string | null; name?: string | null } | null; + metroCity?: { name?: string | null } | null; +}; + +type ResourceAvailabilityInput = { + db: any; + resource: ResourceLocation & { + id: string; + }; + weeklyAvailability: WeekdayAvailability; + monthStart: Date; + monthEnd: Date; +}; + +function getAvailabilityHoursForDate( + availability: WeekdayAvailability, + date: Date, +): number { + const dayKey = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"][date.getUTCDay()] as keyof WeekdayAvailability; + return availability[dayKey] ?? 0; +} + +function sumAvailabilityHoursForDates( + availability: WeekdayAvailability, + dates: Date[], +): number { + return dates.reduce((sum, date) => sum + getAvailabilityHoursForDate(availability, date), 0); +} + +export async function loadResourceGraphAvailability(input: ResourceAvailabilityInput) { + const { db, resource, weeklyAvailability, monthStart, monthEnd } = input; + + const vacations = await db.vacation.findMany({ + where: { + resourceId: resource.id, + status: VacationStatus.APPROVED, + startDate: { lte: monthEnd }, + endDate: { gte: monthStart }, + }, + select: { startDate: true, endDate: true, type: true, isHalfDay: true }, + }); + const resolvedHolidays = await getResolvedCalendarHolidays(asHolidayResolverDb(db), { + periodStart: monthStart, + periodEnd: monthEnd, + countryId: resource.countryId, + countryCode: resource.country?.code, + federalState: resource.federalState, + metroCityId: resource.metroCityId, + metroCityName: resource.metroCity?.name, + }); + const holidayAvailability = collectHolidayAvailability({ + vacations, + periodStart: monthStart, + periodEnd: monthEnd, + countryCode: resource.country?.code, + federalState: resource.federalState, + metroCityName: resource.metroCity?.name, + resolvedHolidayStrings: resolvedHolidays.map((holiday) => holiday.date), + }); + const publicHolidayStrings = holidayAvailability.publicHolidayStrings; + const absenceDateStrings = holidayAvailability.absenceDateStrings; + const absenceDays = holidayAvailability.absenceDays; + const halfDayCount = absenceDays.filter((absence) => absence.isHalfDay).length; + const vacationDayCount = absenceDays.filter((absence) => absence.type === "VACATION").length; + const sickDayCount = absenceDays.filter((absence) => absence.type === "SICK").length; + const publicHolidayCount = resolvedHolidays.length; + const absenceDayEquivalent = absenceDays.reduce((sum, absence) => { + if (absence.type === "PUBLIC_HOLIDAY") { + return sum; + } + return sum + (absence.isHalfDay ? 0.5 : 1); + }, 0); + + const contexts = await loadResourceDailyAvailabilityContexts( + db, + [{ + id: resource.id, + availability: weeklyAvailability, + countryId: resource.countryId, + countryCode: resource.country?.code, + federalState: resource.federalState, + metroCityId: resource.metroCityId, + metroCityName: resource.metroCity?.name, + }], + monthStart, + monthEnd, + ); + const availabilityContext = contexts.get(resource.id); + + const baseWorkingDays = countEffectiveWorkingDays({ + availability: weeklyAvailability, + periodStart: monthStart, + periodEnd: monthEnd, + context: undefined, + }); + const effectiveWorkingDays = countEffectiveWorkingDays({ + availability: weeklyAvailability, + periodStart: monthStart, + periodEnd: monthEnd, + context: availabilityContext, + }); + const baseAvailableHours = calculateEffectiveAvailableHours({ + availability: weeklyAvailability, + periodStart: monthStart, + periodEnd: monthEnd, + context: undefined, + }); + const effectiveAvailableHours = calculateEffectiveAvailableHours({ + availability: weeklyAvailability, + periodStart: monthStart, + periodEnd: monthEnd, + context: availabilityContext, + }); + const publicHolidayDates = resolvedHolidays.map((holiday) => new Date(`${holiday.date}T00:00:00.000Z`)); + const publicHolidayWorkdayCount = publicHolidayDates.reduce((count, date) => ( + count + (getAvailabilityHoursForDate(weeklyAvailability, date) > 0 ? 1 : 0) + ), 0); + const publicHolidayHoursDeduction = sumAvailabilityHoursForDates( + weeklyAvailability, + publicHolidayDates, + ); + const absenceHoursDeduction = absenceDays.reduce((sum, absence) => { + if (absence.type === "PUBLIC_HOLIDAY") { + return sum; + } + const baseHours = getAvailabilityHoursForDate(weeklyAvailability, absence.date); + return sum + baseHours * (absence.isHalfDay ? 0.5 : 1); + }, 0); + const effectiveHoursPerWorkingDay = effectiveWorkingDays > 0 + ? effectiveAvailableHours / effectiveWorkingDays + : 0; + const holidayScopeSummary = [ + resource.country?.code ?? "—", + resource.federalState ?? "—", + resource.metroCity?.name ?? "—", + ].join(" / "); + const holidayExamples = resolvedHolidays.length > 0 + ? resolvedHolidays.slice(0, 4).map((holiday) => `${holiday.date} ${holiday.name}`).join(", ") + : "none"; + const holidayScopeBreakdown = resolvedHolidays.reduce>((counts, holiday) => { + counts[holiday.scope] = (counts[holiday.scope] ?? 0) + 1; + return counts; + }, {}); + + return { + absenceDateStrings, + absenceDayEquivalent, + absenceDays, + absenceHoursDeduction, + availabilityContext, + baseAvailableHours, + baseWorkingDays, + effectiveAvailableHours, + effectiveHoursPerWorkingDay, + effectiveWorkingDays, + halfDayCount, + holidayExamples, + holidayScopeBreakdown, + holidayScopeSummary, + publicHolidayCount, + publicHolidayHoursDeduction, + publicHolidayStrings, + publicHolidayWorkdayCount, + resolvedHolidays, + sickDayCount, + vacationDayCount, + }; +} diff --git a/packages/api/src/router/computation-graph-resource.ts b/packages/api/src/router/computation-graph-resource.ts index 4de71e6..b81512f 100644 --- a/packages/api/src/router/computation-graph-resource.ts +++ b/packages/api/src/router/computation-graph-resource.ts @@ -7,19 +7,9 @@ import { type AssignmentSlice, } from "@capakraken/engine"; import type { CalculationRule, SpainScheduleRule, WeekdayAvailability } from "@capakraken/shared"; -import { VacationStatus } from "@capakraken/db"; import type { TRPCContext } from "../trpc.js"; import { fmtEur } from "../lib/format-utils.js"; -import { - asHolidayResolverDb, - collectHolidayAvailability, - getResolvedCalendarHolidays, -} from "../lib/holiday-availability.js"; -import { - calculateEffectiveAvailableHours, - countEffectiveWorkingDays, - loadResourceDailyAvailabilityContexts, -} from "../lib/resource-capacity.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"; @@ -28,21 +18,6 @@ type ResourceGraphInput = { month: string; }; -function getAvailabilityHoursForDate( - availability: WeekdayAvailability, - date: Date, -): number { - const dayKey = ["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"][date.getUTCDay()] as keyof WeekdayAvailability; - return availability[dayKey] ?? 0; -} - -function sumAvailabilityHoursForDates( - availability: WeekdayAvailability, - dates: Date[], -): number { - return dates.reduce((sum, date) => sum + getAvailabilityHoursForDate(availability, date), 0); -} - export async function readResourceGraphSnapshot( ctx: { db: TRPCContext["db"] }, input: ResourceGraphInput, @@ -106,62 +81,34 @@ export async function readResourceGraphSnapshot( }, }); - const vacations = await ctx.db.vacation.findMany({ - where: { - resourceId: input.resourceId, - status: VacationStatus.APPROVED, - startDate: { lte: monthEnd }, - endDate: { gte: monthStart }, - }, - select: { startDate: true, endDate: true, type: true, isHalfDay: true }, - }); - const resolvedHolidays = await getResolvedCalendarHolidays(asHolidayResolverDb(ctx.db), { - periodStart: monthStart, - periodEnd: monthEnd, - countryId: resource.countryId, - countryCode: resource.country?.code, - federalState: resource.federalState, - metroCityId: resource.metroCityId, - metroCityName: resource.metroCity?.name, - }); - const holidayAvailability = collectHolidayAvailability({ - vacations, - periodStart: monthStart, - periodEnd: monthEnd, - countryCode: resource.country?.code, - federalState: resource.federalState, - metroCityName: resource.metroCity?.name, - resolvedHolidayStrings: resolvedHolidays.map((holiday) => holiday.date), - }); - const publicHolidayStrings = holidayAvailability.publicHolidayStrings; - const absenceDateStrings = holidayAvailability.absenceDateStrings; - const absenceDays = holidayAvailability.absenceDays; - const halfDayCount = absenceDays.filter((absence) => absence.isHalfDay).length; - const vacationDayCount = absenceDays.filter((absence) => absence.type === "VACATION").length; - const sickDayCount = absenceDays.filter((absence) => absence.type === "SICK").length; - const publicHolidayCount = resolvedHolidays.length; - const absenceDayEquivalent = absenceDays.reduce((sum, absence) => { - if (absence.type === "PUBLIC_HOLIDAY") { - return sum; - } - return sum + (absence.isHalfDay ? 0.5 : 1); - }, 0); - - const contexts = await loadResourceDailyAvailabilityContexts( - ctx.db, - [{ - id: resource.id, - availability: weeklyAvailability, - countryId: resource.countryId, - countryCode: resource.country?.code, - federalState: resource.federalState, - metroCityId: resource.metroCityId, - metroCityName: resource.metroCity?.name, - }], + const { + absenceDateStrings, + absenceDayEquivalent, + absenceDays, + absenceHoursDeduction, + baseAvailableHours, + baseWorkingDays, + effectiveAvailableHours, + effectiveHoursPerWorkingDay, + effectiveWorkingDays, + halfDayCount, + holidayExamples, + holidayScopeBreakdown, + holidayScopeSummary, + publicHolidayCount, + publicHolidayHoursDeduction, + publicHolidayStrings, + publicHolidayWorkdayCount, + resolvedHolidays, + sickDayCount, + vacationDayCount, + } = await loadResourceGraphAvailability({ + db: ctx.db, + resource, + weeklyAvailability, monthStart, monthEnd, - ); - const availabilityContext = contexts.get(resource.id); + }); let calcRules: CalculationRule[] = DEFAULT_CALCULATION_RULES; try { @@ -185,60 +132,6 @@ export async function readResourceGraphSnapshot( publicHolidays: publicHolidayStrings, absenceDays: absenceDateStrings, }); - const baseWorkingDays = countEffectiveWorkingDays({ - availability: weeklyAvailability, - periodStart: monthStart, - periodEnd: monthEnd, - context: undefined, - }); - const effectiveWorkingDays = countEffectiveWorkingDays({ - availability: weeklyAvailability, - periodStart: monthStart, - periodEnd: monthEnd, - context: availabilityContext, - }); - const baseAvailableHours = calculateEffectiveAvailableHours({ - availability: weeklyAvailability, - periodStart: monthStart, - periodEnd: monthEnd, - context: undefined, - }); - const effectiveAvailableHours = calculateEffectiveAvailableHours({ - availability: weeklyAvailability, - periodStart: monthStart, - periodEnd: monthEnd, - context: availabilityContext, - }); - const publicHolidayDates = resolvedHolidays.map((holiday) => new Date(`${holiday.date}T00:00:00.000Z`)); - const publicHolidayWorkdayCount = publicHolidayDates.reduce((count, date) => ( - count + (getAvailabilityHoursForDate(weeklyAvailability, date) > 0 ? 1 : 0) - ), 0); - const publicHolidayHoursDeduction = sumAvailabilityHoursForDates( - weeklyAvailability, - publicHolidayDates, - ); - const absenceHoursDeduction = absenceDays.reduce((sum, absence) => { - if (absence.type === "PUBLIC_HOLIDAY") { - return sum; - } - const baseHours = getAvailabilityHoursForDate(weeklyAvailability, absence.date); - return sum + baseHours * (absence.isHalfDay ? 0.5 : 1); - }, 0); - const effectiveHoursPerWorkingDay = effectiveWorkingDays > 0 - ? effectiveAvailableHours / effectiveWorkingDays - : 0; - const holidayScopeSummary = [ - resource.country?.code ?? "—", - resource.federalState ?? "—", - resource.metroCity?.name ?? "—", - ].join(" / "); - const holidayExamples = resolvedHolidays.length > 0 - ? resolvedHolidays.slice(0, 4).map((holiday) => `${holiday.date} ${holiday.name}`).join(", ") - : "none"; - const holidayScopeBreakdown = resolvedHolidays.reduce>((counts, holiday) => { - counts[holiday.scope] = (counts[holiday.scope] ?? 0) + 1; - return counts; - }, {}); const slices: AssignmentSlice[] = []; const assignmentBreakdown: Array<{