"use client"; import { clsx } from "clsx"; import { useEffect, useRef, useState } from "react"; import type { AllocationLike, AllocationReadModel, Assignment } from "@planarchy/shared"; import { trpc } from "~/lib/trpc/client.js"; import { getPlanningEntryMutationId } from "~/lib/planningEntryIds.js"; import { DateInput } from "~/components/ui/DateInput.js"; interface AllocationPopoverProps { allocationId: string; projectId: string; onClose: () => void; onOpenPanel: (projectId: string) => void; /** Pixel position relative to the viewport */ anchorX: number; anchorY: number; } type AllocationPopoverAssignment = Assignment; export function AllocationPopover({ allocationId, projectId, onClose, onOpenPanel, anchorX, anchorY, }: AllocationPopoverProps) { const ref = useRef(null); const utils = trpc.useUtils(); const { data: allocationView, isLoading } = trpc.allocation.listView.useQuery( { projectId }, { staleTime: 10_000 }, ) as { data: AllocationReadModel | undefined; isLoading: boolean }; const allocation = allocationView?.assignments.find((entry) => entry.id === allocationId) as AllocationPopoverAssignment | undefined; const [hoursPerDay, setHoursPerDay] = useState(null); const [startDate, setStartDate] = useState(""); const [endDate, setEndDate] = useState(""); const [includeSaturday, setIncludeSaturday] = useState(false); const [role, setRole] = useState(""); useEffect(() => { if (allocation) { setHoursPerDay(allocation.hoursPerDay); setStartDate(toDateInput(new Date(allocation.startDate))); setEndDate(toDateInput(new Date(allocation.endDate))); const meta = allocation.metadata as { includeSaturday?: boolean } | null; setIncludeSaturday(meta?.includeSaturday ?? false); setRole(allocation.role ?? ""); } }, [allocation]); const updateMutation = trpc.timeline.updateAllocationInline.useMutation({ onSuccess: () => { void utils.timeline.getEntries.invalidate(); void utils.timeline.getEntriesView.invalidate(); void utils.timeline.getProjectContext.invalidate(); void utils.timeline.getBudgetStatus.invalidate(); void utils.allocation.listView.invalidate(); 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 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}`; } function handleSave() { if (!allocation || hoursPerDay === null) return; updateMutation.mutate({ allocationId: getPlanningEntryMutationId(allocation), hoursPerDay, startDate: startDate ? new Date(startDate) : undefined, endDate: endDate ? new Date(endDate) : undefined, includeSaturday, role: role || undefined, }); } // Position popover so it stays on screen const popoverStyle: React.CSSProperties = { position: "fixed", left: Math.min(anchorX, window.innerWidth - 320), top: Math.min(anchorY + 8, window.innerHeight - 360), zIndex: 50, width: 300, }; if (isLoading || !allocation) { return (
Loading...
); } const dailyCostEUR = ((hoursPerDay ?? allocation.hoursPerDay) * (allocation.resource?.lcrCents ?? 0) / 100).toFixed(2); return (
{/* Header */}
{role}
{/* Resource */}
Resource: {allocation.resource?.displayName} {" "}· {allocation.resource?.eid}
{/* Role */}
setRole(e.target.value)} className="w-full border border-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-full border border-gray-200 rounded-lg px-3 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-brand-400" />
{/* Date range */}
{/* Include Saturday */} {/* Error */} {updateMutation.isError && (

{updateMutation.error.message}

)} {/* Actions */}
{/* Link to full panel */}
); }