feat(platform): checkpoint current implementation state
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user