Files
CapaKraken/apps/web/src/components/timeline/timelineAvailability.ts
T

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;
}