"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 { useInvalidateTimeline } from "~/hooks/useInvalidatePlanningViews.js"; interface BatchAssignPopoverProps { resourceIds: string[]; startDate: Date; endDate: Date; onClose: () => void; onCreated: () => void; } function toDateDisplay(d: Date): string { return d.toLocaleDateString("en-GB", { day: "2-digit", month: "short", year: "numeric", }); } export function BatchAssignPopover({ resourceIds, startDate, endDate, onClose, onCreated, }: BatchAssignPopoverProps) { const ref = useRef(null); const invalidateTimeline = useInvalidateTimeline(); const [search, setSearch] = useState(""); const [selectedProjectId, setSelectedProjectId] = useState( null, ); const [hoursPerDay, setHoursPerDay] = useState(8); const [dropdownOpen, setDropdownOpen] = useState(true); const { data: projectsData } = trpc.project.list.useQuery( { search, limit: 20 }, { staleTime: 30_000 }, ); const projects = (projectsData?.projects ?? []) as Array<{ id: string; name: string; }>; const selectedProject = projects.find((p) => p.id === selectedProjectId); const batchMutation = trpc.timeline.batchQuickAssign.useMutation({ onSuccess: () => { invalidateTimeline(); 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]); // Close on ESC useEffect(() => { function handleKey(e: KeyboardEvent) { if (e.key === "Escape") onClose(); } document.addEventListener("keydown", handleKey); return () => document.removeEventListener("keydown", handleKey); }, [onClose]); function handleAssign() { if (!selectedProjectId) return; batchMutation.mutate({ assignments: resourceIds.map((resourceId) => ({ resourceId, projectId: selectedProjectId, startDate, endDate, hoursPerDay, role: "Team Member", status: AllocationStatus.PROPOSED, })), }); } const canAssign = !!selectedProjectId && resourceIds.length > 0 && hoursPerDay > 0; return (
{/* Header */}
Batch Assign
{/* Info line */}

Assigning to{" "} {resourceIds.length} {" "} resource{resourceIds.length !== 1 ? "s" : ""}

{toDateDisplay(startDate)} – {toDateDisplay(endDate)}

{/* 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-sky-400 dark:focus:ring-sky-500" /> {dropdownOpen && projects.length > 0 && (
{projects.map((p) => ( ))}
)}
)}
{/* 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-sky-400 dark:focus:ring-sky-500" />
{[4, 6, 8].map((h) => ( ))}
{/* Error */} {batchMutation.isError && (

{batchMutation.error.message}

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