148 lines
4.4 KiB
TypeScript
148 lines
4.4 KiB
TypeScript
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<string, number> {
|
|
const holidayDates = new Set<string>();
|
|
const vacationFractionsByDate = new Map<string, number>();
|
|
const absenceFractionsByDate = new Map<string, number>();
|
|
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<string, TimelineAssignmentEntry[]>,
|
|
vacationsByResource: Map<string, VacationEntry[]>,
|
|
dates: Date[],
|
|
): Map<string, ResourceCapacitySeries> {
|
|
const result = new Map<string, ResourceCapacitySeries>();
|
|
|
|
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;
|
|
}
|