"use client"; import { useState, useEffect, useRef } from "react"; import { createPortal } from "react-dom"; import { trpc } from "~/lib/trpc/client.js"; import { useInvalidatePlanningViews } from "~/hooks/useInvalidatePlanningViews.js"; interface InlineAllocationEditorProps { allocationId: string; initialStartDate: Date; initialEndDate: Date; initialHoursPerDay: number; /** Bounding rect of the allocation bar (used for positioning) */ barRect: DOMRect; onClose: () => void; onSaved: () => void; } function toIso(d: Date): string { return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`; } export function InlineAllocationEditor({ allocationId, initialStartDate, initialEndDate, initialHoursPerDay, barRect, onClose, onSaved, }: InlineAllocationEditorProps) { const [startDate, setStartDate] = useState(toIso(initialStartDate)); const [endDate, setEndDate] = useState(toIso(initialEndDate)); const [hoursPerDay, setHoursPerDay] = useState(initialHoursPerDay); const [error, setError] = useState(null); const panelRef = useRef(null); const invalidatePlanningViews = useInvalidatePlanningViews(); // eslint-disable-next-line @typescript-eslint/no-explicit-any const updateMutation = (trpc.allocation.update.useMutation as any)({ onSuccess: () => { void invalidatePlanningViews(); onSaved(); }, onError: (err: { message: string }) => { setError(err.message || "Save failed"); }, }) as { mutate: (input: unknown) => void; isPending: boolean }; // Position: below the bar, clamped to viewport const top = Math.min(barRect.bottom + 6, window.innerHeight - 220); const left = Math.min(Math.max(8, barRect.left), window.innerWidth - 248); // Close on click outside useEffect(() => { const handler = (e: MouseEvent) => { if (panelRef.current && !panelRef.current.contains(e.target as Node)) { onClose(); } }; document.addEventListener("mousedown", handler); return () => document.removeEventListener("mousedown", handler); }, [onClose]); // Close on Escape useEffect(() => { const handler = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); }; window.addEventListener("keydown", handler); return () => window.removeEventListener("keydown", handler); }, [onClose]); function handleSave(e: React.FormEvent) { e.preventDefault(); setError(null); const s = new Date(startDate); const en = new Date(endDate); if (en < s) { setError("End date must be after start date"); return; } updateMutation.mutate({ id: allocationId, data: { startDate: s, endDate: en, hoursPerDay }, }); } const panel = (
Edit Allocation
setStartDate(e.target.value)} className="w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-900 px-2 py-1 text-xs text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-1 focus:ring-brand-500" autoFocus />
setEndDate(e.target.value)} className="w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-900 px-2 py-1 text-xs text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-1 focus:ring-brand-500" />
setHoursPerDay(Number(e.target.value))} className="w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-900 px-2 py-1 text-xs text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-1 focus:ring-brand-500" />
{error &&

{error}

}
); return typeof document === "undefined" ? panel : createPortal(panel, document.body); }