/** * Shared pure render functions used by both TimelineResourcePanel and TimelineProjectPanel. * Extracted to avoid duplication of identical vacation blocks, range overlay, and overbooking blink logic. */ import React from "react"; import { clsx } from "clsx"; import { VACATION_TIMELINE_COLORS, VACATION_TIMELINE_BORDER, VACATION_TYPE_LABELS_SHORT, } from "~/lib/status-styles.js"; import type { RangeState } from "~/hooks/useTimelineDrag.js"; import type { TimelineAssignmentEntry } from "./TimelineContext.js"; import type { VacationEntry } from "./TimelineContext.js"; // ─── Shared types ───────────────────────────────────────────────────────────── export interface VacationBlockInfo { vacation: VacationEntry; left: number; width: number; } export function buildVacationBlocksByResource( vacationsByResource: Map, showVacations: boolean, toLeft: (date: Date) => number, toWidth: (start: Date, end: Date) => number, cellWidth: number, totalCanvasWidth: number, showWeekends = true, ) { if (!showVacations) return new Map(); const result = new Map(); for (const [resourceId, vacations] of vacationsByResource) { const blocks: VacationBlockInfo[] = []; for (const vacation of vacations) { const start = new Date(vacation.startDate); const end = new Date(vacation.endDate); // When weekends are hidden, skip single-day entries that fall on a weekend // (e.g. Easter Sunday). They would otherwise render at Monday's column, // shadowing Monday's own holiday (e.g. Easter Monday). if (!showWeekends && start.toDateString() === end.toDateString()) { const dow = start.getDay(); if (dow === 0 || dow === 6) continue; } const left = toLeft(start); const width = Math.max(cellWidth, toWidth(start, end)); if (width <= 0 || left >= totalCanvasWidth) continue; blocks.push({ vacation, left, width }); } if (blocks.length > 0) { result.set(resourceId, blocks); } } return result; } // ─── Vacation block overlays ───────────────────────────────────────────────── export function renderVacationBlocks(blocks: VacationBlockInfo[], rowHeight: number) { if (blocks.length === 0) return null; return blocks.map(({ vacation: v, left, width }) => { const colorClass = VACATION_TIMELINE_COLORS[v.type] ?? "bg-orange-400/40"; const borderClass = VACATION_TIMELINE_BORDER[v.type] ?? "border-orange-500"; const label = VACATION_TYPE_LABELS_SHORT[v.type] ?? v.type; const isPending = v.status === "PENDING"; return (
{width > 40 && ( {isPending ? "\u23F3" : "\uD83C\uDFD6"} {label} )}
); }); } // ─── Range selection overlay ───────────────────────────────────────────────── export function renderRangeOverlay( rangeState: RangeState, resourceId: string, rowHeight: number, toLeft: (d: Date) => number, toWidth: (s: Date, e: Date) => number, CELL_WIDTH: number, ) { if (!rangeState.isSelecting || rangeState.resourceId !== resourceId || !rangeState.startDate) { return null; } const end = rangeState.currentDate ?? rangeState.startDate; const [selStart, selEnd] = rangeState.startDate <= end ? [rangeState.startDate, end] : [end, rangeState.startDate]; const left = toLeft(selStart); const width = Math.max(CELL_WIDTH, toWidth(selStart, selEnd)); return (
); } // ─── Overbooking blink overlay ─────────────────────────────────────────────── export function renderOverbookingBlink( allocs: TimelineAssignmentEntry[], dates: Date[], CELL_WIDTH: number, capacityHoursByDay?: number[], bookingFactorsByDay?: number[], ) { const overbooked: number[] = []; const allocRanges = allocs.map((a) => { const s = new Date(a.startDate); s.setHours(0, 0, 0, 0); const e = new Date(a.endDate); e.setHours(0, 0, 0, 0); return { s: s.getTime(), e: e.getTime(), h: a.hoursPerDay }; }); for (let i = 0; i < dates.length; i++) { const d = new Date(dates[i]!); d.setHours(0, 0, 0, 0); const t = d.getTime(); let totalH = 0; const factor = bookingFactorsByDay?.[i] ?? 1; for (const { s, e, h } of allocRanges) { if (t >= s && t <= e) { totalH += h * factor; } } if (totalH > (capacityHoursByDay?.[i] ?? 8)) overbooked.push(i); } if (overbooked.length === 0) return null; return overbooked.map((i) => (
)); }