import type { WeekdayAvailability } from "@capakraken/shared"; import type { TimelineAssignmentEntry, VacationEntry } from "./TimelineContext.js"; import { DEFAULT_TIMELINE_AVAILABILITY, getTimelineAvailabilityHoursForDate, normalizeTimelineAvailability, toLocalDateKey, toLocalStartOfDay, } from "./timelineAvailability.js"; const DEFAULT_AVAILABILITY: WeekdayAvailability = DEFAULT_TIMELINE_AVAILABILITY; export type ResourceCapacitySeries = { baseHoursByDay: number[]; capacityHoursByDay: number[]; bookingFactorsByDay: number[]; }; function normalizeAvailability(value: unknown): WeekdayAvailability { return normalizeTimelineAvailability(value); } function getAvailabilityHoursForDate(availability: WeekdayAvailability, date: Date): number { return getTimelineAvailabilityHoursForDate(availability, date); } function resolveResourceAvailability(allocs: TimelineAssignmentEntry[]): WeekdayAvailability { for (const alloc of allocs) { const availability = alloc.resource?.availability; if (availability !== null && availability !== undefined) { return normalizeAvailability(availability); } } return DEFAULT_AVAILABILITY; } function buildAbsenceFractionsByDate( vacations: VacationEntry[], periodStart: Date, periodEnd: Date, ): Map { const holidayDates = new Set(); const vacationFractionsByDate = new Map(); const absenceFractionsByDate = new Map(); const normalizedStart = toLocalStartOfDay(periodStart); const normalizedEnd = toLocalStartOfDay(periodEnd); for (const vacation of vacations) { const isPublicHoliday = vacation.type === "PUBLIC_HOLIDAY"; const isApprovedVacation = vacation.status === "APPROVED"; if (!isPublicHoliday && !isApprovedVacation) { continue; } const overlapStart = new Date( Math.max( toLocalStartOfDay(vacation.startDate).getTime(), normalizedStart.getTime(), ), ); const overlapEnd = new Date( Math.min( toLocalStartOfDay(vacation.endDate).getTime(), normalizedEnd.getTime(), ), ); if (overlapStart > overlapEnd) { continue; } const fraction = vacation.isHalfDay ? 0.5 : 1; const cursor = new Date(overlapStart); while (cursor <= overlapEnd) { const isoDate = toLocalDateKey(cursor); if (isPublicHoliday) { holidayDates.add(isoDate); } else { const existingVacation = vacationFractionsByDate.get(isoDate) ?? 0; vacationFractionsByDate.set(isoDate, Math.max(existingVacation, fraction)); } const existingAbsence = absenceFractionsByDate.get(isoDate) ?? 0; if (isPublicHoliday || !holidayDates.has(isoDate)) { absenceFractionsByDate.set(isoDate, Math.max(existingAbsence, fraction)); } cursor.setDate(cursor.getDate() + 1); } } for (const isoDate of holidayDates) { const existingAbsence = absenceFractionsByDate.get(isoDate) ?? 0; absenceFractionsByDate.set(isoDate, Math.max(existingAbsence, 1)); } return absenceFractionsByDate; } export function buildResourceCapacitySeries( allocsByResource: Map, vacationsByResource: Map, dates: Date[], ): Map { const result = new Map(); if (dates.length === 0) { return result; } const periodStart = dates[0]!; const periodEnd = dates[dates.length - 1]!; for (const [resourceId, allocs] of allocsByResource) { const availability = resolveResourceAvailability(allocs); const absenceFractionsByDate = buildAbsenceFractionsByDate( vacationsByResource.get(resourceId) ?? [], periodStart, periodEnd, ); const baseHoursByDay: number[] = []; const capacityHoursByDay: number[] = []; const bookingFactorsByDay: number[] = []; for (const date of dates) { const baseHours = getAvailabilityHoursForDate(availability, date); const absenceFraction = absenceFractionsByDate.get(toLocalDateKey(date)) ?? 0; const bookingFactor = baseHours > 0 ? Math.max(0, 1 - absenceFraction) : 0; baseHoursByDay.push(baseHours); bookingFactorsByDay.push(bookingFactor); capacityHoursByDay.push(baseHours * bookingFactor); } result.set(resourceId, { baseHoursByDay, capacityHoursByDay, bookingFactorsByDay, }); } return result; }