"use client"; import { useState } from "react"; import { VacationStatus } from "@capakraken/shared"; import { trpc } from "~/lib/trpc/client.js"; import { InfoTooltip } from "~/components/ui/InfoTooltip.js"; import { VACATION_CALENDAR_COLORS } from "~/lib/status-styles.js"; const MONTH_NAMES = [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", ]; function isoDate(d: Date | string): string { const date = typeof d === "string" ? new Date(d) : d; return date.toISOString().slice(0, 10); } export function TeamCalendar() { const now = new Date(); const [month, setMonth] = useState(now.getMonth()); const [year, setYear] = useState(now.getFullYear()); const [chapter, setChapter] = useState(""); const firstDay = new Date(Date.UTC(year, month, 1)); const daysInMonth = new Date(Date.UTC(year, month + 1, 0)).getUTCDate(); const { data: resources } = trpc.resource.list.useQuery( { isActive: true, limit: 500, ...(chapter ? { chapter } : {}) }, { staleTime: 60_000 }, ); const { data: vacations } = trpc.vacation.list.useQuery( { startDate: firstDay, endDate: new Date(Date.UTC(year, month + 1, 0)), limit: 500, }, { staleTime: 15_000 }, ); // Fetch all chapters independently so the dropdown isn't affected by chapter filter const { data: allChapters } = trpc.resource.chapters.useQuery(undefined, { staleTime: 60_000 }); const chapters = allChapters ?? []; const resourceList: { id: string; displayName: string }[] = resources?.resources ?? []; const vacationList = (vacations ?? []).filter( (v) => v.status !== VacationStatus.CANCELLED && v.status !== VacationStatus.REJECTED, ); // Build map: resourceId → date → vacation const vacationMap = new Map>(); for (const v of vacationList) { const start = isoDate(v.startDate); const end = isoDate(v.endDate); let cur = start; while (cur <= end) { const resourceVacs = vacationMap.get(v.resourceId) ?? new Map(); if (!resourceVacs.has(cur)) { resourceVacs.set(cur, { type: v.type, status: v.status }); } vacationMap.set(v.resourceId, resourceVacs); // next day const d = new Date(cur); d.setUTCDate(d.getUTCDate() + 1); cur = d.toISOString().slice(0, 10); } } const days = Array.from({ length: daysInMonth }, (_, i) => i + 1); const today = now.toISOString().slice(0, 10); function prevMonth() { if (month === 0) { setMonth(11); setYear(y => y - 1); } else setMonth(m => m - 1); } function nextMonth() { if (month === 11) { setMonth(0); setYear(y => y + 1); } else setMonth(m => m + 1); } return (
{/* Toolbar */}
{MONTH_NAMES[month]} {year}
{/* Grid */}
{days.map((d) => { const dateStr = `${year}-${String(month + 1).padStart(2, "0")}-${String(d).padStart(2, "0")}`; const dow = new Date(dateStr).getUTCDay(); // 0=Sun, 6=Sat const isWeekend = dow === 0 || dow === 6; const isToday = dateStr === today; return ( ); })} {resourceList.map((r) => { const rMap = vacationMap.get(r.id); return ( {days.map((d) => { const dateStr = `${year}-${String(month + 1).padStart(2, "0")}-${String(d).padStart(2, "0")}`; const vac = rMap?.get(dateStr); const dow = new Date(dateStr).getUTCDay(); const isWeekend = dow === 0 || dow === 6; const isToday = dateStr === today; let cellClass = "w-7 h-7"; if (vac) { const color = VACATION_CALENDAR_COLORS[vac.type] ?? "bg-gray-400"; const opacity = vac.status === "PENDING" ? "opacity-50" : ""; cellClass += ` ${color} ${opacity}`; } else if (isWeekend) { cellClass += " bg-gray-50 dark:bg-gray-900"; } else if (isToday) { cellClass += " bg-brand-50"; } return ( ); })} ); })}
Resource {d}
{r.displayName}
{resourceList.length === 0 && (
No resources found.
)}
{/* Legend */}
{Object.entries(VACATION_CALENDAR_COLORS).map(([type, color]) => ( {type.replace("_", " ")} ))} Pending
); }