132 lines
4.6 KiB
TypeScript
132 lines
4.6 KiB
TypeScript
import { type WeekdayAvailability } from "@capakraken/shared";
|
|
import {
|
|
calculateEffectiveAvailableHours,
|
|
calculateEffectiveBookedHours,
|
|
countEffectiveWorkingDays,
|
|
type ResourceDailyAvailabilityContext,
|
|
} from "../lib/resource-capacity.js";
|
|
import { averagePerWorkingDay, getBaseDayAvailability, round1 } from "./staffing-shared.js";
|
|
|
|
type CapacityBooking = {
|
|
startDate: Date;
|
|
endDate: Date;
|
|
hoursPerDay: number;
|
|
};
|
|
|
|
export function summarizeHolidayAndAbsenceCapacity(input: {
|
|
availability: WeekdayAvailability;
|
|
context: ResourceDailyAvailabilityContext | undefined;
|
|
}) {
|
|
const holidayDates = [...(input.context?.holidayDates ?? new Set<string>())].sort();
|
|
const holidayWorkdayCount = holidayDates.reduce((count, isoDate) => (
|
|
count + (getBaseDayAvailability(input.availability, new Date(`${isoDate}T00:00:00.000Z`)) > 0 ? 1 : 0)
|
|
), 0);
|
|
const holidayHoursDeduction = holidayDates.reduce((sum, isoDate) => (
|
|
sum + getBaseDayAvailability(input.availability, new Date(`${isoDate}T00:00:00.000Z`))
|
|
), 0);
|
|
|
|
let excludedCapacityDays = 0;
|
|
for (const fraction of input.context?.absenceFractionsByDate.values() ?? []) {
|
|
excludedCapacityDays += fraction;
|
|
}
|
|
|
|
let absenceDayEquivalent = 0;
|
|
let absenceHoursDeduction = 0;
|
|
for (const [isoDate, fraction] of input.context?.vacationFractionsByDate ?? []) {
|
|
const dayHours = getBaseDayAvailability(input.availability, new Date(`${isoDate}T00:00:00.000Z`));
|
|
if (dayHours <= 0 || input.context?.holidayDates.has(isoDate)) {
|
|
continue;
|
|
}
|
|
absenceDayEquivalent += fraction;
|
|
absenceHoursDeduction += dayHours * fraction;
|
|
}
|
|
|
|
return {
|
|
excludedCapacityDays: round1(excludedCapacityDays),
|
|
holidayDates,
|
|
holidayWorkdayCount,
|
|
holidayHoursDeduction: round1(holidayHoursDeduction),
|
|
absenceDayEquivalent: round1(absenceDayEquivalent),
|
|
absenceHoursDeduction: round1(absenceHoursDeduction),
|
|
};
|
|
}
|
|
|
|
export function buildResourceCapacitySummary(input: {
|
|
availability: WeekdayAvailability;
|
|
periodStart: Date;
|
|
periodEnd: Date;
|
|
context: ResourceDailyAvailabilityContext | undefined;
|
|
bookings?: CapacityBooking[] | undefined;
|
|
}) {
|
|
const baseWorkingDays = countEffectiveWorkingDays({
|
|
availability: input.availability,
|
|
periodStart: input.periodStart,
|
|
periodEnd: input.periodEnd,
|
|
context: undefined,
|
|
});
|
|
const workingDays = countEffectiveWorkingDays({
|
|
availability: input.availability,
|
|
periodStart: input.periodStart,
|
|
periodEnd: input.periodEnd,
|
|
context: input.context,
|
|
});
|
|
const baseAvailableHours = calculateEffectiveAvailableHours({
|
|
availability: input.availability,
|
|
periodStart: input.periodStart,
|
|
periodEnd: input.periodEnd,
|
|
context: undefined,
|
|
});
|
|
const availableHours = calculateEffectiveAvailableHours({
|
|
availability: input.availability,
|
|
periodStart: input.periodStart,
|
|
periodEnd: input.periodEnd,
|
|
context: input.context,
|
|
});
|
|
const bookedHours = (input.bookings ?? []).reduce(
|
|
(sum, booking) =>
|
|
sum + calculateEffectiveBookedHours({
|
|
availability: input.availability,
|
|
startDate: booking.startDate,
|
|
endDate: booking.endDate,
|
|
hoursPerDay: booking.hoursPerDay,
|
|
periodStart: input.periodStart,
|
|
periodEnd: input.periodEnd,
|
|
context: input.context,
|
|
}),
|
|
0,
|
|
);
|
|
const remainingHours = Math.max(0, availableHours - bookedHours);
|
|
const holidayAndAbsence = summarizeHolidayAndAbsenceCapacity({
|
|
availability: input.availability,
|
|
context: input.context,
|
|
});
|
|
|
|
return {
|
|
baseWorkingDays: round1(baseWorkingDays),
|
|
workingDays: round1(workingDays),
|
|
baseAvailableHours: round1(baseAvailableHours),
|
|
availableHours: round1(availableHours),
|
|
bookedHours: round1(bookedHours),
|
|
remainingHours: round1(remainingHours),
|
|
remainingHoursPerDay: averagePerWorkingDay(remainingHours, workingDays),
|
|
holidaySummary: {
|
|
count: holidayAndAbsence.holidayDates.length,
|
|
workdayCount: holidayAndAbsence.holidayWorkdayCount,
|
|
hoursDeduction: holidayAndAbsence.holidayHoursDeduction,
|
|
holidayDates: holidayAndAbsence.holidayDates,
|
|
},
|
|
absenceSummary: {
|
|
dayEquivalent: holidayAndAbsence.absenceDayEquivalent,
|
|
hoursDeduction: holidayAndAbsence.absenceHoursDeduction,
|
|
},
|
|
excludedCapacityDays: holidayAndAbsence.excludedCapacityDays,
|
|
capacityBreakdown: {
|
|
formula: "baseAvailableHours - holidayHoursDeduction - absenceHoursDeduction = availableHours",
|
|
baseAvailableHours: round1(baseAvailableHours),
|
|
holidayHoursDeduction: holidayAndAbsence.holidayHoursDeduction,
|
|
absenceHoursDeduction: holidayAndAbsence.absenceHoursDeduction,
|
|
availableHours: round1(availableHours),
|
|
},
|
|
};
|
|
}
|