407266bc28
Co-Authored-By: claude-flow <ruv@ruv.net>
165 lines
6.1 KiB
TypeScript
165 lines
6.1 KiB
TypeScript
/**
|
|
* 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<React.SetStateAction<MultiSelectState>>;
|
|
clearMultiSelect: () => void;
|
|
canvasRef: React.RefObject<HTMLDivElement | null>;
|
|
viewMode: ViewMode;
|
|
resources: ResourceBrief[];
|
|
allocsByResource: Map<string, TimelineAssignmentEntry[]>;
|
|
projectGroups: ProjectGroup[];
|
|
openDemandsByProject: Map<string, TimelineDemandEntry[]>;
|
|
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<HTMLElement>("[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<HTMLElement>("[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<HTMLElement>("[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
|
|
}
|