Files
CapaKraken/apps/web/src/components/timeline/timelineCapacity.ts
T

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;
}