"use client"; import { useState } from "react"; import { VacationStatus, VacationType } from "@capakraken/shared"; import { VACATION_CALENDAR_COLORS } from "~/lib/status-styles.js"; interface VacationEntry { id: string; startDate: Date | string; endDate: Date | string; type: string; status: string; resource?: { displayName: string; eid: string } | null; } interface VacationCalendarProps { vacations: VacationEntry[]; year?: number; initialMonth?: number; // 0-indexed } const STATUS_OPACITY: Record = { APPROVED: "opacity-100", PENDING: "opacity-60", REJECTED: "opacity-30", CANCELLED: "opacity-20", }; const DAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]; const MONTH_NAMES = [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December", ]; function isoDate(d: Date | string): string { const date = typeof d === "string" ? new Date(d) : d; return date.toISOString().slice(0, 10); } function addDays(dateStr: string, n: number): string { const d = new Date(dateStr); d.setUTCDate(d.getUTCDate() + n); return d.toISOString().slice(0, 10); } function getDatesInRange(start: Date | string, end: Date | string): Set { const dates = new Set(); let cur = isoDate(start); const last = isoDate(end); while (cur <= last) { dates.add(cur); cur = addDays(cur, 1); } return dates; } export function VacationCalendar({ vacations, year = new Date().getFullYear(), initialMonth = new Date().getMonth() }: VacationCalendarProps) { const [month, setMonth] = useState(initialMonth); const [currentYear, setCurrentYear] = useState(year); function prevMonth() { if (month === 0) { setMonth(11); setCurrentYear(y => y - 1); } else setMonth(m => m - 1); } function nextMonth() { if (month === 11) { setMonth(0); setCurrentYear(y => y + 1); } else setMonth(m => m + 1); } // Build a set of date → vacation entries for fast lookup const dateMap = new Map(); for (const v of vacations) { if ([VacationStatus.CANCELLED, VacationStatus.REJECTED].includes(v.status as VacationStatus)) continue; const dates = getDatesInRange(v.startDate, v.endDate); for (const d of dates) { const existing = dateMap.get(d) ?? []; existing.push(v); dateMap.set(d, existing); } } // Build calendar grid const firstDay = new Date(Date.UTC(currentYear, month, 1)); const daysInMonth = new Date(Date.UTC(currentYear, month + 1, 0)).getUTCDate(); // ISO weekday: Mon=1, Sun=7 → index 0-6 const startOffset = (firstDay.getUTCDay() + 6) % 7; // Mon first const cells: (number | null)[] = [ ...Array(startOffset).fill(null), ...Array.from({ length: daysInMonth }, (_, i) => i + 1), ]; // Pad to complete last row while (cells.length % 7 !== 0) cells.push(null); return (
{/* Header */}

{MONTH_NAMES[month]} {currentYear}

{/* Day names */}
{DAYS.map((d) => (
{d}
))}
{/* Days grid */}
{cells.map((day, idx) => { if (!day) { return
; } const dateStr = `${currentYear}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`; const dayVacations = dateMap.get(dateStr) ?? []; const today = new Date().toISOString().slice(0, 10); const isToday = dateStr === today; return (
{day}
{dayVacations.slice(0, 3).map((v) => { const colorClass = VACATION_CALENDAR_COLORS[v.type] ?? "bg-gray-400"; const opacityClass = STATUS_OPACITY[v.status] ?? "opacity-100"; const name = v.resource?.displayName ?? "—"; return (
{name.split(" ")[0]}
); })} {dayVacations.length > 3 && (
+{dayVacations.length - 3}
)}
); })}
{/* Legend */}
{Object.entries(VACATION_CALENDAR_COLORS).map(([type, color]) => ( {type.replace("_", " ")} ))}
); }