124 lines
3.7 KiB
TypeScript
124 lines
3.7 KiB
TypeScript
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 },
|
|
);
|
|
}
|