"use client"; import { useRef, useState } from "react"; import { useFocusTrap } from "~/hooks/useFocusTrap.js"; import { VacationType } from "@capakraken/shared"; import { trpc } from "~/lib/trpc/client.js"; import { DateInput } from "~/components/ui/DateInput.js"; import { useDebounce } from "~/hooks/useDebounce.js"; import { InfoTooltip } from "~/components/ui/InfoTooltip.js"; import { VACATION_TYPE_LABELS } from "~/lib/status-styles.js"; const VACATION_TYPES = Object.values(VacationType); interface VacationModalProps { resourceId?: string; onClose: () => void; onSuccess: () => void; } function toDateInputValue(date: Date | string | null | undefined): string { if (!date) return ""; const d = typeof date === "string" ? new Date(date) : date; const y = d.getFullYear(); const m = String(d.getMonth() + 1).padStart(2, "0"); const day = String(d.getDate()).padStart(2, "0"); return `${y}-${m}-${day}`; } export function VacationModal({ resourceId: initialResourceId, onClose, onSuccess }: VacationModalProps) { const [resourceId, setResourceId] = useState(initialResourceId ?? ""); const [type, setType] = useState(VacationType.ANNUAL); const [startDate, setStartDate] = useState(""); const [endDate, setEndDate] = useState(""); const [note, setNote] = useState(""); const [isHalfDay, setIsHalfDay] = useState(false); const [halfDayPart, setHalfDayPart] = useState<"MORNING" | "AFTERNOON">("MORNING"); const [serverError, setServerError] = useState(null); const panelRef = useRef(null); useFocusTrap(panelRef, true); const debouncedStart = useDebounce(startDate, 400); const debouncedEnd = useDebounce(endDate, 400); const { data: resources } = trpc.resource.list.useQuery( { isActive: true, limit: 500 }, { staleTime: 60_000 }, ); // Team overlap: other resources in same chapter off in this period const { data: teamOverlap } = trpc.vacation.getTeamOverlap.useQuery( { resourceId: resourceId, startDate: debouncedStart ? new Date(debouncedStart) : new Date(), endDate: debouncedEnd ? new Date(debouncedEnd) : new Date(), }, { enabled: !!resourceId && !!debouncedStart && !!debouncedEnd, staleTime: 10_000, }, ); // Show existing vacations for this resource in the selected period const { data: existingVacations } = trpc.vacation.list.useQuery( { resourceId: resourceId || undefined, startDate: startDate ? new Date(startDate) : undefined, endDate: endDate ? new Date(endDate) : undefined, }, { enabled: !!resourceId && !!startDate && !!endDate, staleTime: 10_000 }, ); const utils = trpc.useUtils(); const createMutation = trpc.vacation.create.useMutation({ onSuccess: async () => { await utils.vacation.list.invalidate(); onSuccess(); }, onError: (err) => setServerError(err.message), }); const isPending = createMutation.isPending; function handleSubmit(e: React.FormEvent) { e.preventDefault(); setServerError(null); if (!resourceId || !startDate || !endDate) { setServerError("Please fill in all required fields."); return; } const start = new Date(startDate); const end = new Date(endDate); if (end < start) { setServerError("End date must be on or after start date."); return; } createMutation.mutate({ resourceId, type, startDate: start, endDate: end, note: note || undefined, isHalfDay, ...(isHalfDay ? { halfDayPart } : {}), }); } const inputClass = "w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-500 text-sm dark:bg-gray-900 dark:text-gray-100"; const labelClass = "block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"; const resourceList: { id: string; displayName: string; eid: string }[] = resources?.resources ?? []; return (
{ if (e.target === e.currentTarget) onClose(); }} >
{ if (e.key === "Escape") onClose(); }} > {/* Header */}

Request Vacation

{/* Resource */} {!initialResourceId && (
)} {/* Type */}
{/* Dates */}
{/* Half-day toggle */}
{isHalfDay && (
)}
{/* Conflict warning */} {existingVacations && existingVacations.length > 0 && (
Existing vacation in this period:
    {existingVacations.map((v) => (
  • {VACATION_TYPE_LABELS[v.type as VacationType]} —{" "} {new Date(v.startDate).toLocaleDateString("en-GB")} to{" "} {new Date(v.endDate).toLocaleDateString("en-GB")} ({v.status})
  • ))}
)} {/* Team overlap warning */} {teamOverlap && teamOverlap.length > 0 && (
Team members off in this period ({teamOverlap.length}):
    {teamOverlap.map((v) => { const r = v.resource as { displayName: string; eid: string } | null; return (
  • {r?.displayName ?? "—"}{" "} ({new Date(v.startDate).toLocaleDateString("en-GB")} – {new Date(v.endDate).toLocaleDateString("en-GB")})
  • ); })}
)} {/* Note */}