chore(repo): initialize planarchy workspace

This commit is contained in:
2026-03-14 14:31:09 +01:00
commit dd55d0e78b
769 changed files with 166461 additions and 0 deletions
@@ -0,0 +1,166 @@
import type {
AllocationCalculationInput,
AllocationCalculationResult,
DailyBreakdown,
WeekdayAvailability,
} from "@planarchy/shared";
import { getRecurringHoursForDay } from "./recurrence.js";
/** Day-of-week index → availability key */
const DOW_KEYS: (keyof WeekdayAvailability)[] = [
"sunday",
"monday",
"tuesday",
"wednesday",
"thursday",
"friday",
"saturday",
];
/**
* Returns the availability hours for a given date.
* Returns 0 for days not in the availability map (treated as non-working).
*/
export function getAvailableHoursForDate(
date: Date,
availability: WeekdayAvailability,
): number {
const key = DOW_KEYS[date.getDay()];
if (!key) return 0;
return availability[key] ?? 0;
}
/**
* Checks whether a given date is a working day for this resource.
*/
export function isWorkday(date: Date, availability: WeekdayAvailability): boolean {
return getAvailableHoursForDate(date, availability) > 0;
}
/**
* Counts working days between startDate and endDate (inclusive).
*/
export function countWorkingDays(
startDate: Date,
endDate: Date,
availability: WeekdayAvailability,
): number {
let count = 0;
const current = new Date(startDate);
current.setHours(0, 0, 0, 0);
const end = new Date(endDate);
end.setHours(0, 0, 0, 0);
while (current <= end) {
if (isWorkday(current, availability)) {
count++;
}
current.setDate(current.getDate() + 1);
}
return count;
}
/**
* Core allocation calculator: given hours/day, LCR, and date range,
* computes total hours, total cost, and daily breakdown.
*
* Monetary values always in integer cents.
*/
export function calculateAllocation(input: AllocationCalculationInput): AllocationCalculationResult {
const { lcrCents, hoursPerDay, startDate, endDate, availability, includeSaturday, recurrence, vacationDates } = input;
// When includeSaturday is not explicitly true, zero out saturday availability
const effectiveAvailability: WeekdayAvailability = includeSaturday
? availability
: { ...availability, saturday: 0 };
// Pre-compute vacation date set (YYYY-MM-DD strings for O(1) lookup)
const vacationDateSet = new Set<string>(
(vacationDates ?? []).map((d) => {
const copy = new Date(d);
copy.setHours(0, 0, 0, 0);
return copy.toISOString().split("T")[0]!;
}),
);
const allocationStart = new Date(startDate);
allocationStart.setHours(0, 0, 0, 0);
const breakdown: DailyBreakdown[] = [];
const current = new Date(startDate);
current.setHours(0, 0, 0, 0);
const end = new Date(endDate);
end.setHours(0, 0, 0, 0);
let workingDays = 0;
let totalHours = 0;
while (current <= end) {
const dateKey = current.toISOString().split("T")[0]!;
const isVacation = vacationDateSet.has(dateKey);
let effectiveHours: number;
let dayIsWorkday: boolean;
if (isVacation) {
// Vacation always blocks the day
effectiveHours = 0;
dayIsWorkday = false;
} else if (recurrence) {
// Recurrence pattern — may override hoursPerDay or skip the day entirely
const recurHours = getRecurringHoursForDay(current, recurrence, hoursPerDay, allocationStart);
if (recurHours === 0) {
effectiveHours = 0;
dayIsWorkday = false;
} else {
const availableHours = getAvailableHoursForDate(current, effectiveAvailability);
dayIsWorkday = availableHours > 0;
effectiveHours = dayIsWorkday ? Math.min(recurHours, availableHours) : 0;
}
} else {
const availableHours = getAvailableHoursForDate(current, effectiveAvailability);
dayIsWorkday = availableHours > 0;
effectiveHours = dayIsWorkday ? Math.min(hoursPerDay, availableHours) : 0;
}
// Cost = hours × lcrCents (already in cents-per-hour)
const dayCostCents = Math.round(effectiveHours * lcrCents);
breakdown.push({
date: new Date(current),
isWorkday: dayIsWorkday,
hours: effectiveHours,
costCents: dayCostCents,
});
if (dayIsWorkday) {
workingDays++;
totalHours += effectiveHours;
}
current.setDate(current.getDate() + 1);
}
const totalCostCents = breakdown.reduce((sum, d) => sum + d.costCents, 0);
const dailyCostCents = Math.round(hoursPerDay * lcrCents);
return {
workingDays,
totalHours,
totalCostCents,
dailyCostCents,
dailyBreakdown: breakdown,
};
}
/**
* Calculates total allocation cost for a simple case (without full breakdown).
* Useful for quick budget checks.
*/
export function calculateTotalCost(
lcrCents: number,
hoursPerDay: number,
workingDays: number,
): number {
return Math.round(lcrCents * hoursPerDay * workingDays);
}