130 lines
3.8 KiB
TypeScript
130 lines
3.8 KiB
TypeScript
import type { WeekdayAvailability } from "@capakraken/shared";
|
|
import type { TimelineAssignmentEntry } from "./TimelineContext.js";
|
|
|
|
const DAY_KEYS: (keyof WeekdayAvailability)[] = [
|
|
"sunday",
|
|
"monday",
|
|
"tuesday",
|
|
"wednesday",
|
|
"thursday",
|
|
"friday",
|
|
"saturday",
|
|
];
|
|
|
|
export const DEFAULT_TIMELINE_AVAILABILITY: WeekdayAvailability = {
|
|
sunday: 0,
|
|
monday: 8,
|
|
tuesday: 8,
|
|
wednesday: 8,
|
|
thursday: 8,
|
|
friday: 8,
|
|
saturday: 0,
|
|
};
|
|
|
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
return typeof value === "object" && value !== null;
|
|
}
|
|
|
|
export function toLocalStartOfDay(value: Date | string): Date {
|
|
const date = value instanceof Date ? new Date(value) : new Date(value);
|
|
date.setHours(0, 0, 0, 0);
|
|
return date;
|
|
}
|
|
|
|
export function toLocalDateKey(value: Date | string): string {
|
|
const date = toLocalStartOfDay(value);
|
|
const year = date.getFullYear();
|
|
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
const day = String(date.getDate()).padStart(2, "0");
|
|
return `${year}-${month}-${day}`;
|
|
}
|
|
|
|
export function normalizeTimelineAvailability(value: unknown): WeekdayAvailability {
|
|
if (!isRecord(value)) {
|
|
return DEFAULT_TIMELINE_AVAILABILITY;
|
|
}
|
|
|
|
const normalized = { ...DEFAULT_TIMELINE_AVAILABILITY };
|
|
for (const key of DAY_KEYS) {
|
|
const next = value[key];
|
|
if (typeof next === "number" && Number.isFinite(next)) {
|
|
normalized[key] = next;
|
|
}
|
|
}
|
|
return normalized;
|
|
}
|
|
|
|
export function getTimelineAvailabilityHoursForDate(
|
|
availability: WeekdayAvailability,
|
|
date: Date,
|
|
): number {
|
|
const dayKey = DAY_KEYS[toLocalStartOfDay(date).getDay()];
|
|
return dayKey ? (availability[dayKey] ?? 0) : 0;
|
|
}
|
|
|
|
export function getEffectiveAllocationAvailability(
|
|
allocation: Pick<TimelineAssignmentEntry, "resource" | "metadata">,
|
|
): WeekdayAvailability {
|
|
const availability = normalizeTimelineAvailability(allocation.resource?.availability);
|
|
const metadata = allocation.metadata as Record<string, unknown> | null;
|
|
const includeSaturday = metadata?.includeSaturday === true;
|
|
return includeSaturday ? availability : { ...availability, saturday: 0 };
|
|
}
|
|
|
|
export function isAllocationScheduledOnDate(
|
|
allocation: Pick<TimelineAssignmentEntry, "startDate" | "endDate" | "resource" | "metadata">,
|
|
date: Date,
|
|
): boolean {
|
|
const target = toLocalStartOfDay(date);
|
|
const start = toLocalStartOfDay(allocation.startDate);
|
|
const end = toLocalStartOfDay(allocation.endDate);
|
|
|
|
if (target < start || target > end) {
|
|
return false;
|
|
}
|
|
|
|
return getTimelineAvailabilityHoursForDate(getEffectiveAllocationAvailability(allocation), target) > 0;
|
|
}
|
|
|
|
export function buildAllocationWorkingDaySegments(
|
|
allocation: Pick<TimelineAssignmentEntry, "startDate" | "endDate" | "resource" | "metadata">,
|
|
rangeStart?: Date,
|
|
rangeEnd?: Date,
|
|
): Array<{ start: Date; end: Date }> {
|
|
const availability = getEffectiveAllocationAvailability(allocation);
|
|
const start = toLocalStartOfDay(rangeStart ?? allocation.startDate);
|
|
const end = toLocalStartOfDay(rangeEnd ?? allocation.endDate);
|
|
|
|
if (start > end) {
|
|
return [];
|
|
}
|
|
|
|
const segments: Array<{ start: Date; end: Date }> = [];
|
|
let segmentStart: Date | null = null;
|
|
let segmentEnd: Date | null = null;
|
|
const cursor = new Date(start);
|
|
|
|
while (cursor <= end) {
|
|
const isWorkingDay = getTimelineAvailabilityHoursForDate(availability, cursor) > 0;
|
|
|
|
if (isWorkingDay) {
|
|
if (!segmentStart) {
|
|
segmentStart = new Date(cursor);
|
|
}
|
|
segmentEnd = new Date(cursor);
|
|
} else if (segmentStart && segmentEnd) {
|
|
segments.push({ start: segmentStart, end: segmentEnd });
|
|
segmentStart = null;
|
|
segmentEnd = null;
|
|
}
|
|
|
|
cursor.setDate(cursor.getDate() + 1);
|
|
}
|
|
|
|
if (segmentStart && segmentEnd) {
|
|
segments.push({ start: segmentStart, end: segmentEnd });
|
|
}
|
|
|
|
return segments;
|
|
}
|