Files
CapaKraken/packages/api/src/router/staffing-shared.ts
T
Hartmut 1df208dbcc feat(timeline): add pulse animation for in-flight drag mutations
Allocation bars that have active optimistic overrides (post-drag,
awaiting server confirmation) now pulse subtly via animate-pulse.
The pending set is derived from the existing optimisticAllocations
map keys, requiring no additional state.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 13:28:46 +02:00

94 lines
3.1 KiB
TypeScript

import { toIsoDate, type WeekdayAvailability } from "@capakraken/shared";
export { toIsoDate, round1, averagePerWorkingDay } from "@capakraken/shared";
import { getAvailabilityHoursForDate, calculateEffectiveDayAvailability as _calcEffective, type ResourceDailyAvailabilityContext } from "../lib/resource-capacity.js";
export const ACTIVE_STATUSES = new Set(["PROPOSED", "CONFIRMED", "ACTIVE"]);
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 getBaseDayAvailability(
availability: WeekdayAvailability,
date: Date,
): number {
return getAvailabilityHoursForDate(availability, date);
}
export function getEffectiveDayAvailability(
availability: WeekdayAvailability,
date: Date,
context: ResourceDailyAvailabilityContext | undefined,
): number {
return _calcEffective({ availability, date, context });
}
function overlapsDateRange(startDate: Date, endDate: Date, date: Date): boolean {
return date >= startDate && date <= endDate;
}
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 },
);
}