"use client"; import { clsx } from "clsx"; import { useEffect, useRef, useState } from "react"; import { AllocationStatus } from "@planarchy/shared"; import { trpc } from "~/lib/trpc/client.js"; import { DateInput } from "~/components/ui/DateInput.js"; interface NewAllocationPopoverProps { resourceId: string; startDate: Date; endDate: Date; /** Pre-selected project (from project-view sub-row context) */ suggestedProjectId?: string | null; anchorX: number; anchorY: number; onClose: () => void; onCreated: () => void; } function toDateInput(d: Date): string { 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 NewAllocationPopover({ resourceId, startDate, endDate, suggestedProjectId, anchorX, anchorY, onClose, onCreated, }: NewAllocationPopoverProps) { const ref = useRef(null); const utils = trpc.useUtils(); const [search, setSearch] = useState(""); const [selectedProjectId, setSelectedProjectId] = useState( suggestedProjectId ?? null, ); const [role, setRole] = useState("Team Member"); const [hoursPerDay, setHoursPerDay] = useState(8); const [start, setStart] = useState(toDateInput(startDate)); const [end, setEnd] = useState(toDateInput(endDate)); const [dropdownOpen, setDropdownOpen] = useState(!suggestedProjectId); const { data: projectsData } = trpc.project.list.useQuery( { search, limit: 20 }, { staleTime: 30_000 }, ); // eslint-disable-next-line @typescript-eslint/no-explicit-any const projects = (projectsData?.projects ?? []) as Array<{ id: string; name: string; orderType?: string }>; const selectedProject = projects.find((p) => p.id === selectedProjectId) ?? (suggestedProjectId ? projects.find((p) => p.id === suggestedProjectId) : null); const createMutation = trpc.timeline.quickAssign.useMutation({ onSuccess: () => { void utils.timeline.getEntries.invalidate(); void utils.timeline.getEntriesView.invalidate(); void utils.timeline.getProjectContext.invalidate(); void utils.timeline.getBudgetStatus.invalidate(); onCreated(); onClose(); }, }); // Close on outside click useEffect(() => { function handleClick(e: MouseEvent) { if (ref.current && !ref.current.contains(e.target as Node)) { onClose(); } } document.addEventListener("mousedown", handleClick); return () => document.removeEventListener("mousedown", handleClick); }, [onClose]); function handleCreate() { if (!selectedProjectId) return; createMutation.mutate({ resourceId, projectId: selectedProjectId, startDate: new Date(start), endDate: new Date(end), hoursPerDay, role, status: AllocationStatus.PROPOSED, }); } const canCreate = !!selectedProjectId && !!start && !!end && hoursPerDay > 0; const left = Math.min(anchorX - 10, typeof window !== "undefined" ? window.innerWidth - 340 : anchorX); const top = Math.min(anchorY + 8, typeof window !== "undefined" ? window.innerHeight - 440 : anchorY); return (
{/* Header */}
Assign to Project
{/* Date range */}
{/* Project picker */}
{selectedProject && !dropdownOpen ? (
{ setDropdownOpen(true); setSearch(""); }} > {selectedProject.name}
) : (
setSearch(e.target.value)} onFocus={() => setDropdownOpen(true)} className="w-full border border-gray-200 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-brand-400" /> {dropdownOpen && projects.length > 0 && (
{projects.map((p) => ( ))}
)}
)}
{/* Role */}
setRole(e.target.value)} className="w-full border border-gray-200 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 rounded-lg px-3 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-brand-400" />
{/* Hours per day */}
setHoursPerDay(parseFloat(e.target.value))} className="w-24 border border-gray-200 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 rounded-lg px-3 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-brand-400" />
{[4, 6, 8].map((h) => ( ))}
{/* Overbooking notice */}

Overlapping allocations are allowed — resource may be overbooked.

{/* Error */} {createMutation.isError && (

{createMutation.error.message}

)} {/* Actions */}
); }