chore(repo): initialize planarchy workspace
This commit is contained in:
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user