import { type WeekdayAvailability } from "@capakraken/shared"; import { type ResourceDailyAvailabilityContext } from "../lib/resource-capacity.js"; const DAY_KEYS: (keyof WeekdayAvailability)[] = [ "sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday", ]; export const ACTIVE_STATUSES = new Set(["PROPOSED", "CONFIRMED", "ACTIVE"]); export function toIsoDate(value: Date): string { return value.toISOString().slice(0, 10); } function createUtcDate(year: number, monthIndex: number, day: number): Date { return new Date(Date.UTC(year, monthIndex, day)); } function normalizeUtcDate(value: Date): Date { return createUtcDate(value.getUTCFullYear(), value.getUTCMonth(), value.getUTCDate()); } export function createDateRange(input: { startDate?: Date | undefined; endDate?: Date | undefined; durationDays?: number | undefined; }): { startDate: Date; endDate: Date } { const startDate = input.startDate ? normalizeUtcDate(input.startDate) : createUtcDate(new Date().getUTCFullYear(), new Date().getUTCMonth(), new Date().getUTCDate()); const endDate = input.endDate ? normalizeUtcDate(input.endDate) : createUtcDate( startDate.getUTCFullYear(), startDate.getUTCMonth(), startDate.getUTCDate() + Math.max((input.durationDays ?? 21) - 1, 0), ); if (endDate < startDate) { throw new Error("endDate must be on or after startDate."); } return { startDate, endDate }; } export function round1(value: number): number { return Math.round(value * 10) / 10; } export function getBaseDayAvailability( availability: WeekdayAvailability, date: Date, ): number { const key = DAY_KEYS[date.getUTCDay()]; return key ? (availability[key] ?? 0) : 0; } export function getEffectiveDayAvailability( availability: WeekdayAvailability, date: Date, context: ResourceDailyAvailabilityContext | undefined, ): number { const key = DAY_KEYS[date.getUTCDay()]; const baseHours = key ? (availability[key] ?? 0) : 0; if (baseHours <= 0) { return 0; } const fraction = context?.absenceFractionsByDate.get(toIsoDate(date)) ?? 0; return Math.max(0, baseHours * (1 - fraction)); } function overlapsDateRange(startDate: Date, endDate: Date, date: Date): boolean { return date >= startDate && date <= endDate; } export function averagePerWorkingDay(totalHours: number, workingDays: number): number { if (workingDays <= 0) { return 0; } return round1(totalHours / workingDays); } export function createLocationLabel(input: { countryCode?: string | null; federalState?: string | null; metroCityName?: string | null; }): string { return [ input.countryCode ?? null, input.federalState ?? null, input.metroCityName ?? null, ].filter((value): value is string => Boolean(value && value.trim().length > 0)).join(" / "); } export function calculateAllocatedHoursForDay(input: { bookings: Array<{ startDate: Date; endDate: Date; hoursPerDay: number; status: string; isChargeable?: boolean }>; date: Date; context: ResourceDailyAvailabilityContext | undefined; }): { allocatedHours: number; chargeableHours: number } { const isoDate = toIsoDate(input.date); const dayFraction = Math.max(0, 1 - (input.context?.absenceFractionsByDate.get(isoDate) ?? 0)); return input.bookings.reduce( (acc, booking) => { if (!ACTIVE_STATUSES.has(booking.status) || !overlapsDateRange(booking.startDate, booking.endDate, input.date)) { return acc; } const effectiveHours = booking.hoursPerDay * dayFraction; acc.allocatedHours += effectiveHours; if (booking.isChargeable) { acc.chargeableHours += effectiveHours; } return acc; }, { allocatedHours: 0, chargeableHours: 0 }, ); }