/** * Computes which allocations/resources fall within the multi-select rectangle * after the user finishes a right-click drag selection. */ import { useEffect } from "react"; import { LABEL_WIDTH } from "~/components/timeline/timelineConstants.js"; import type { MultiSelectState } from "~/hooks/useTimelineDrag.js"; import type { TimelineAssignmentEntry, TimelineDemandEntry, ViewMode, ResourceBrief, } from "~/components/timeline/TimelineContext.js"; interface ProjectGroup { id: string; resourceRows: { resource: { id: string }; allocs: TimelineAssignmentEntry[]; }[]; } export function useMultiSelectIntersection({ multiSelectState, setMultiSelectState, clearMultiSelect, canvasRef, viewMode, resources, allocsByResource, projectGroups, openDemandsByProject, dates, today, CELL_WIDTH, toLeft, toWidth, }: { multiSelectState: MultiSelectState; setMultiSelectState: React.Dispatch>; clearMultiSelect: () => void; canvasRef: React.RefObject; viewMode: ViewMode; resources: ResourceBrief[]; allocsByResource: Map; projectGroups: ProjectGroup[]; openDemandsByProject: Map; dates: Date[]; today: Date; CELL_WIDTH: number; toLeft: (d: Date) => number; toWidth: (s: Date, e: Date) => number; }) { useEffect(() => { // Only compute when drag just ended (isSelecting false but has coordinates) if (multiSelectState.isSelecting) return; if (multiSelectState.startX === 0 && multiSelectState.startY === 0) return; if (multiSelectState.selectedAllocationIds.length > 0 || multiSelectState.selectedResourceIds.length > 0) return; const canvasEl = canvasRef.current; if (!canvasEl) return; // Selection rectangle in viewport coordinates const selTop = Math.min(multiSelectState.startY, multiSelectState.currentY); const selBottom = Math.max(multiSelectState.startY, multiSelectState.currentY); const selLeft = Math.min(multiSelectState.startX, multiSelectState.currentX); const selRight = Math.max(multiSelectState.startX, multiSelectState.currentX); // Convert viewport X to canvas-relative X for allocation matching const canvasRect = canvasEl.getBoundingClientRect(); const canvasXOffset = canvasRect.left + LABEL_WIDTH; const toCanvasX = (clientX: number) => clientX - canvasXOffset; const selLeftCanvas = toCanvasX(selLeft); const selRightCanvas = toCanvasX(selRight); // Derive date range from pixel X positions const colIndexStart = Math.max(0, Math.min(dates.length - 1, Math.floor(selLeftCanvas / CELL_WIDTH))); const colIndexEnd = Math.max(0, Math.min(dates.length - 1, Math.floor(selRightCanvas / CELL_WIDTH))); const startDate = dates[colIndexStart] ?? today; const endDate = dates[colIndexEnd] ?? today; const selectedIds: string[] = []; const selectedResIds: string[] = []; // Query all rendered row elements (virtualizer only renders visible + overscan rows) const rowElements = canvasEl.querySelectorAll("[data-index]"); if (viewMode === "resource") { rowElements.forEach((rowEl) => { const idx = Number(rowEl.dataset.index); const resource = resources[idx]; if (!resource) return; const rowRect = rowEl.getBoundingClientRect(); if (rowRect.bottom < selTop || rowRect.top > selBottom) return; selectedResIds.push(resource.id); const allocs = allocsByResource.get(resource.id) ?? []; for (const alloc of allocs) { const allocLeft = toLeft(new Date(alloc.startDate)); const allocRight = allocLeft + toWidth(new Date(alloc.startDate), new Date(alloc.endDate)); if (allocRight >= selLeftCanvas && allocLeft <= selRightCanvas) { selectedIds.push(alloc.id); } } }); } else if (viewMode === "project") { const projectRowEls = canvasEl.querySelectorAll("[data-project-resource-row]"); projectRowEls.forEach((rowEl) => { const rowRect = rowEl.getBoundingClientRect(); if (rowRect.bottom < selTop || rowRect.top > selBottom) return; const projectId = rowEl.dataset.projectId; const resourceId = rowEl.dataset.resourceId; if (!projectId || !resourceId) return; const group = projectGroups.find((g) => g.id === projectId); if (!group) return; const row = group.resourceRows.find((r) => r.resource.id === resourceId); if (!row) return; for (const alloc of row.allocs) { const allocLeft = toLeft(new Date(alloc.startDate)); const allocRight = allocLeft + toWidth(new Date(alloc.startDate), new Date(alloc.endDate)); if (allocRight >= selLeftCanvas && allocLeft <= selRightCanvas) { selectedIds.push(alloc.id); } } }); // Also check demand rows for open demand selection const demandRowEls = canvasEl.querySelectorAll("[data-project-demand-row]"); demandRowEls.forEach((rowEl) => { const rowRect = rowEl.getBoundingClientRect(); if (rowRect.bottom < selTop || rowRect.top > selBottom) return; const projectId = rowEl.dataset.projectId; if (!projectId) return; const demands = openDemandsByProject.get(projectId) ?? []; for (const demand of demands) { const allocLeft = toLeft(new Date(demand.startDate)); const allocRight = allocLeft + toWidth(new Date(demand.startDate), new Date(demand.endDate)); if (allocRight >= selLeftCanvas && allocLeft <= selRightCanvas) { selectedIds.push(demand.id); } } }); } if (selectedIds.length > 0 || selectedResIds.length > 0) { setMultiSelectState(prev => ({ ...prev, selectedAllocationIds: selectedIds, selectedResourceIds: selectedResIds, dateRange: { start: startDate, end: endDate }, })); } else { clearMultiSelect(); } }, [multiSelectState.isSelecting, multiSelectState.startX, multiSelectState.startY]); // eslint-disable-line react-hooks/exhaustive-deps }