# Right-Click Multi-Selection on Timeline ## Anforderungsanalyse Vier neue Funktionen für die Timeline: 1. **Right-click + Drag Multi-Selection** — Rechtsklick + Drag auf dem Canvas, um mehrere Allocation-Blocks auszuwählen. Die Selektion kann über mehrere Ressourcen-Zeilen hinweggehen (Lasso/Rectangle-Selection). 2. **Shift+Click Additive Selection** — Shift+Klick auf einen Allocation-Block fügt ihn zur bestehenden Multi-Selection hinzu (oder entfernt ihn, wenn bereits selektiert = Toggle). Ermöglicht präzise Einzelauswahl ohne Lasso. Funktioniert auch als Einstieg: Erster Shift+Click startet die Multi-Selection, weitere Shift+Clicks erweitern sie. 3. **Floating Action Bar** — Bei aktiver Multi-Selection erscheint eine schwebende Toolbar mit: - **"Delete / Cancel"** — Batch-Löschung aller selektierten Allocations (`allocation.batchDelete` existiert bereits) - **"Assign"** — Auf leeren Zeilen: Batch-Erstellung neuer Allocations über `timeline.quickAssign` (eine pro selektierter Ressource-Zeile) - **"Clear Selection"** — Selektion aufheben 4. **Multi-Resource Assignment** — Right-click + Drag über leere Zeilen mehrerer Ressourcen → öffnet einen Popover/Bar, der ein Projekt wählen lässt und dann Allocations für alle selektierten Ressourcen auf einmal erstellt. **Betroffene Pakete:** `apps/web` (Frontend), `packages/api` (neuer `batchQuickAssign`-Endpoint) ## Betroffene Pakete & Dateien | Paket | Dateien | Art der Änderung | |-------|---------|-----------------:| | apps/web | `src/hooks/useTimelineDrag.ts` | edit — neuer `multiSelectState`, right-click drag handler | | apps/web | `src/components/timeline/TimelineView.tsx` | edit — multiSelectState empfangen, FloatingActionBar rendern, batch-Actions verdrahten | | apps/web | `src/components/timeline/FloatingActionBar.tsx` | create — schwebende Toolbar-Komponente | | apps/web | `src/components/timeline/BatchAssignPopover.tsx` | create — Projekt-Picker für Multi-Resource-Assignment | | apps/web | `src/components/timeline/TimelineResourcePanel.tsx` | edit — Selection-Overlay rendern, onContextMenu anpassen | | apps/web | `src/components/timeline/TimelineProjectPanel.tsx` | edit — Selection-Overlay rendern, onContextMenu anpassen | | packages/api | `src/router/timeline.ts` | edit — neuer `batchQuickAssign`-Endpoint | ## Architektur-Entscheidungen ### Right-click vs. Left-click vs. Shift-click Abgrenzung - **Left-click drag** (button 0): Bestehende Funktionen — Alloc-Move, Alloc-Resize, Range-Select (NewAllocationPopover) - **Right-click drag** (button 2): Neue Multi-Selection (Lasso-Rectangle) - **Right-click ohne Drag** auf bestehende Allocation: Öffnet weiterhin `AllocationPopover` (Einzelbearbeitung) — das ist der bestehende `onContextMenu`-Handler - **Shift+Click** auf Allocation-Block: Toggle-Selektion (addiert/entfernt Block zur/aus Multi-Selection). Startet Multi-Selection wenn noch keine aktiv. Kein Drag nötig — sofortige Einzelauswahl. ### Multi-Select State Neuer State `MultiSelectState` im `useTimelineDrag`-Hook: ```ts interface MultiSelectState { isSelecting: boolean; // Rectangle coordinates (canvas-relative pixels) startX: number; startY: number; currentX: number; currentY: number; // Resolved after mouseUp: selectedAllocationIds: string[]; selectedResourceIds: string[]; // Resources within the rectangle (for empty-row assign) dateRange: { start: Date; end: Date } | null; } ``` ### Intersection-Logik Die Selektion geschieht als **Rectangle Intersection**: 1. Während des Drag wird ein visuelles Rechteck gezeichnet (semi-transparenter blauer Rahmen) 2. Bei mouseUp wird berechnet, welche Allocation-Blocks innerhalb des Rechtecks liegen (Pixel-basiert: Block-Position vs. Selection-Rect) 3. Selektierte Blocks erhalten einen visuellen Highlight (z.B. `ring-2 ring-brand-500`) **Berechnung der Intersection:** Da die Allocation-Blocks als absolute `left/width/top/height` positioniert sind, können wir die Intersection über die `toLeft()`/`toWidth()`-Funktionen + Zeilen-Index berechnen. Die Berechnung geschieht im `TimelineResourcePanel`/`TimelineProjectPanel` und gibt IDs zurück. ### Batch-API - **Delete:** `allocation.batchDelete` existiert bereits (max 100 IDs) - **Assign:** Neuer `timeline.batchQuickAssign`-Endpoint, der ein Array von `{ resourceId, projectId, startDate, endDate, hoursPerDay }` akzeptiert und in einer Transaktion erstellt ## Task-Liste ### Task 1: Multi-Select State im Drag-Hook - [ ] **Task 1a:** `MultiSelectState` Interface + Initial State definieren → Datei: `useTimelineDrag.ts` ```ts export interface MultiSelectState { isSelecting: boolean; startX: number; startY: number; currentX: number; currentY: number; selectedAllocationIds: string[]; selectedResourceIds: string[]; dateRange: { start: Date; end: Date } | null; } ``` - [ ] **Task 1b:** Right-click drag handlers implementieren → Datei: `useTimelineDrag.ts` Neuer `onCanvasRightMouseDown` Handler: - Prüfe `e.button === 2` - `e.preventDefault()` (verhindert nativen Kontextmenü) - Starte `multiSelectState` mit `isSelecting: true` und Mausposition - Registriere `document.addEventListener("mousemove", ...)` und `document.addEventListener("mouseup", ...)` (analog zum AllocDrag-Pattern) - Bei mousemove: Update `currentX/currentY` - Bei mouseUp ohne Bewegung (< 5px): Fallback auf bestehenden `onAllocationContextMenu` (Einzelblock-Rechtsklick) - Bei mouseUp mit Bewegung: Setze `isSelecting: false` aber behalte `selectedAllocationIds`/`selectedResourceIds` (werden vom Parent berechnet und reingesetzt) Neuer `onCanvasContextMenu` Handler: - Wird auf dem Canvas registriert, um `e.preventDefault()` global zu setzen (verhindert Browser-Kontextmenü) Return-Werte erweitern um `multiSelectState`, `setMultiSelectState`, `onCanvasRightMouseDown`, `clearMultiSelect`. - [ ] **Task 1c:** `clearMultiSelect` Funktion → Datei: `useTimelineDrag.ts` Setzt `multiSelectState` auf Initial zurück. Wird von ESC-Handler und FloatingActionBar genutzt. ### Task 1d: Shift+Click Toggle-Selection - [ ] **Task 1d:** Shift+Click Handler für Allocation-Blocks → Datei: `useTimelineDrag.ts` Im bestehenden `onAllocMouseDown`-Handler (und im mouseUp-Pfad wo `daysDelta === 0` als Click behandelt wird): **Logik im mouseUp (daysDelta === 0):** ```ts if (e.shiftKey) { // Toggle this allocation in multi-select setMultiSelectState(prev => { const ids = new Set(prev.selectedAllocationIds); if (ids.has(alloc.allocationId)) { ids.delete(alloc.allocationId); // Deselect } else { ids.add(alloc.allocationId); // Add to selection } return { ...prev, isSelecting: false, selectedAllocationIds: [...ids] }; }); return; // Don't open popover } // ... existing click → popover logic ``` **Wichtig:** Der Shift-Key-Check muss im mouseUp geschehen (nicht mouseDown), weil erst dort feststeht ob es ein Click (daysDelta === 0) oder Drag war. **Interaktion mit bestehendem Code:** - `onAllocMouseDown` startet den Drag (button 0 check auf Zeile 311) - Im `handleUp` closure (Zeile 370): Wenn `daysDelta === 0` → aktuell wird `onBlockClickRef.current` aufgerufen - **Änderung:** Vor dem `onBlockClick`-Call prüfen ob `shiftKey` gedrückt war (muss im mouseDown-Event gespeichert werden, da mouseUp ein document-event ist und das Original-React-Event nicht mehr verfügbar) - → `shiftKeyRef` im Closure capturen: `const wasShift = e.shiftKey;` im `onAllocMouseDown` Neuer Callback in Hook-Return: `onShiftClickAllocation?: (allocationId: string) => void` — wird vom Parent (`TimelineView`) gesetzt und toggelt den multiSelectState. **Alternativ (einfacher):** Den Shift-Check direkt in `TimelineView.onBlockClick` machen, da dieser Callback bereits den `multiSelectState`-Zugriff hat. ### Task 2: Selection-Overlay in Panels rendern - [ ] **Task 2a:** Selection-Rectangle als visuelles Overlay rendern → Datei: `TimelineResourcePanel.tsx` Props erweitern um `multiSelectState: MultiSelectState`. Neues `