diff --git a/apps/web/src/hooks/useMultiSelectIntersection.ts b/apps/web/src/hooks/useMultiSelectIntersection.ts index 1182366..092ac2f 100644 --- a/apps/web/src/hooks/useMultiSelectIntersection.ts +++ b/apps/web/src/hooks/useMultiSelectIntersection.ts @@ -5,9 +5,13 @@ import { useEffect } from "react"; import { LABEL_WIDTH } from "~/components/timeline/timelineConstants.js"; -import type { MultiSelectState, AllocDragMode } from "~/hooks/useTimelineDrag.js"; -import type { TimelineAssignmentEntry } from "~/components/timeline/TimelineContext.js"; -import type { ViewMode, ResourceBrief } from "~/components/timeline/TimelineContext.js"; +import type { MultiSelectState } from "~/hooks/useTimelineDrag.js"; +import type { + TimelineAssignmentEntry, + TimelineDemandEntry, + ViewMode, + ResourceBrief, +} from "~/components/timeline/TimelineContext.js"; interface ProjectGroup { id: string; @@ -17,12 +21,6 @@ interface ProjectGroup { }[]; } -interface DemandEntry { - id: string; - startDate: Date | string; - endDate: Date | string; -} - export function useMultiSelectIntersection({ multiSelectState, setMultiSelectState, @@ -47,7 +45,7 @@ export function useMultiSelectIntersection({ resources: ResourceBrief[]; allocsByResource: Map; projectGroups: ProjectGroup[]; - openDemandsByProject: Map; + openDemandsByProject: Map; dates: Date[]; today: Date; CELL_WIDTH: number; diff --git a/plan.md b/plan.md index 5a430fa..fdc8653 100644 --- a/plan.md +++ b/plan.md @@ -1,321 +1,258 @@ -# Right-Click Multi-Selection on Timeline +# Refactor v2 — Code Optimization, De-duplication & Maintainability ## Anforderungsanalyse -Vier neue Funktionen für die Timeline: +Vollstaendiger Optimierungsdurchlauf des Planarchy-Monorepos. Ziel: Code-Duplikate eliminieren, +Performance verbessern, Wartbarkeit erhoehen. Betroffen sind alle Pakete: `apps/web`, `packages/api`, +`packages/db`, `packages/shared`. -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). +Die Analyse identifiziert **7 unabhaengige Arbeitsstroeme (Waves)**, die von Agenten parallel +bearbeitet werden koennen, plus eine abschliessende Schema-Migration. -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 | +| Paket | Dateien | Art | +|-------|---------|-----| +| `apps/web` | `src/lib/format.ts`, 10+ Consumer-Dateien | edit | +| `apps/web` | `src/lib/status-styles.ts`, `src/components/timeline/timelineConstants.ts`, 6 Consumer-Dateien | edit | +| `apps/web` | `src/hooks/useInvalidatePlanningViews.ts`, 14 Consumer-Dateien | edit | +| `apps/web` | `src/components/timeline/renderHelpers.ts` | create | +| `apps/web` | `src/components/timeline/TimelineResourcePanel.tsx`, `TimelineProjectPanel.tsx` | edit | +| `apps/web` | `src/components/timeline/TimelineView.tsx` | edit | +| `apps/web` | `src/hooks/useTimelineDrag.ts` | edit | +| `packages/api` | `src/db/helpers.ts`, 7+ Router-Dateien | edit | +| `packages/api` | `src/db/selects.ts`, 6+ Router-Dateien | edit | +| `packages/db` | `prisma/schema.prisma` | edit | -## 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. +## Wave 1 — Centralize `formatMoney()` / `formatCents()` (Agent: format-consolidator) -### Multi-Select State -Neuer State `MultiSelectState` im `useTimelineDrag`-Hook: +**Problem:** Zentralisierte Funktionen existieren in `apps/web/src/lib/format.ts`, aber 10+ +Stellen nutzen Inline-`(x / 100).toLocaleString("de-DE", ...)` statt der zentralen Imports. +Zusaetzlich gibt es 2 lokale `fmtEur()` Helfer in API-Routern. -```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; -} +**Dateien die keine Aenderung brauchen:** +- `ShiftPreviewTooltip.tsx` — spezialisierter Delta-Formatter mit +/- Prefix (bleibt) + +### Tasks + +- [ ] **1.1** Inline-Formatierung in `FillOpenDemandModal.tsx` (5 Stellen, Lines ~214/417/434/444/452) durch `formatCents()` Import ersetzen → `apps/web/src/components/allocations/FillOpenDemandModal.tsx` +- [ ] **1.2** Inline-Formatierung in `DemandPopover.tsx` (Lines ~146/152) durch `formatCents()` Import ersetzen → `apps/web/src/components/timeline/DemandPopover.tsx` +- [ ] **1.3** Inline-Formatierung in `ResourceHoverCard.tsx` (Lines ~123/130) durch `formatCents()` Import ersetzen → `apps/web/src/components/timeline/ResourceHoverCard.tsx` +- [ ] **1.4** Inline-Formatierung in `ProjectWizard.tsx` (Lines ~509-511) ersetzen → `apps/web/src/components/projects/ProjectWizard.tsx` +- [ ] **1.5** Inline-Formatierung in `ProjectAssignmentsTable.tsx` (Line ~130) ersetzen → `apps/web/src/components/projects/ProjectAssignmentsTable.tsx` +- [ ] **1.6** Inline-Formatierung in `ProjectDemandsTable.tsx` (Lines ~135/138-139) ersetzen → `apps/web/src/components/projects/ProjectDemandsTable.tsx` +- [ ] **1.7** Inline-Formatierung in `ProjectsClient.tsx` (Line ~403) ersetzen → `apps/web/src/app/(app)/projects/ProjectsClient.tsx` +- [ ] **1.8** Inline-Formatierung in `ProjectTableWidget.tsx` (Lines ~274/283) ersetzen → `apps/web/src/components/dashboard/widgets/ProjectTableWidget.tsx` +- [ ] **1.9** Inline-`.toFixed(0)` in `ResourcesClient.tsx` (Line ~1178) und `ResourceDetail.tsx` (Lines ~279/286) durch `formatMoney()` ersetzen → 2 Dateien +- [ ] **1.10** Duplizierte `fmtEur()` in `assistant-tools.ts` (Line ~44-46) und `computation-graph.ts` (Line ~53-55) entfernen — gemeinsamen Helfer in `packages/api/src/lib/format-utils.ts` erstellen (da API-Router keinen Zugriff auf `apps/web/src/lib/format.ts` haben) → `packages/api/src/lib/format-utils.ts` (create), 2 Router editieren + +### Akzeptanzkriterien +- Kein `/ 100).toLocaleString("de-DE"` mehr in Component-Dateien (ausser ShiftPreviewTooltip) +- Kein lokales `fmtEur()` mehr in API-Routern + +--- + +## Wave 2 — Adopt `findUniqueOrThrow()` Helper (Agent: db-helper-consolidator) + +**Problem:** `packages/api/src/db/helpers.ts` existiert mit `findUniqueOrThrow()`, aber 7 Router-Dateien nutzen ihn nicht und haben manuelle `findUnique` + `if (!x) throw` Bloecke. + +### Tasks + +- [ ] **2.1** `entitlement.ts` — 7 manuelle Stellen durch `findUniqueOrThrow()` ersetzen → `packages/api/src/router/entitlement.ts` +- [ ] **2.2** `calculation-rules.ts` — 3 manuelle if+throw Stellen (Lines ~24-26, 62-64, 89-91) ersetzen → `packages/api/src/router/calculation-rules.ts` +- [ ] **2.3** `notification.ts` — 3 Stellen migrieren → `packages/api/src/router/notification.ts` +- [ ] **2.4** `settings.ts` — 3 Stellen migrieren → `packages/api/src/router/settings.ts` +- [ ] **2.5** `user.ts` — verbleibende manuelle Stellen migrieren → `packages/api/src/router/user.ts` +- [ ] **2.6** `resource.ts` — 8 verbleibende manuelle Stellen migrieren → `packages/api/src/router/resource.ts` +- [ ] **2.7** `vacation.ts` — 12 verbleibende manuelle Stellen migrieren → `packages/api/src/router/vacation.ts` +- [ ] **2.8** `timeline.ts` — 4 verbleibende Stellen migrieren → `packages/api/src/router/timeline.ts` +- [ ] **2.9** `assistant.ts` — 1 Stelle migrieren → `packages/api/src/router/assistant.ts` + +**Hinweis:** `assistant-tools.ts` nutzt `{ error: "..." }` Return-Pattern statt throw — NICHT migrieren (anderes Error-Handling). + +### Akzeptanzkriterien +- Alle Router (ausser assistant-tools.ts) nutzen `findUniqueOrThrow()` fuer NOT_FOUND Checks +- `pnpm --filter @planarchy/api exec tsc --noEmit` — gruen + +--- + +## Wave 3 — Prisma Select Constants adoptieren (Agent: select-consolidator) + +**Problem:** `packages/api/src/db/selects.ts` definiert `ROLE_BRIEF_SELECT`, `PROJECT_BRIEF_SELECT`, `RESOURCE_BRIEF_SELECT`, aber Adoption ist gering (nur `allocation.ts` nutzt alle drei). + +### Tasks + +- [ ] **3.1** `vacation.ts` — 5 Inline-Resource-Selects (Lines ~102/123/213/542/579) durch `RESOURCE_BRIEF_SELECT` ersetzen → `packages/api/src/router/vacation.ts` +- [ ] **3.2** `role.ts` — 1 Inline-Resource-Select (Line ~92) ersetzen → `packages/api/src/router/role.ts` +- [ ] **3.3** `project-planning-read-model.ts` — 1 Inline-Role-Select (Line ~34) durch `ROLE_BRIEF_SELECT` ersetzen → `packages/api/src/router/project-planning-read-model.ts` +- [ ] **3.4** `resource.ts` — 1 Inline-Role-Select (Line ~313) durch `ROLE_BRIEF_SELECT` ersetzen → `packages/api/src/router/resource.ts` +- [ ] **3.5** `calculation-rules.ts` — 2 Inline-Project-Selects (Lines ~13/22) durch `PROJECT_BRIEF_SELECT` ersetzen → `packages/api/src/router/calculation-rules.ts` +- [ ] **3.6** `entitlement.ts` — 1 Inline-Resource-Select (Line ~269) durch `RESOURCE_BRIEF_SELECT` (+ spread `chapter: true`) ersetzen → `packages/api/src/router/entitlement.ts` + +### Akzeptanzkriterien +- Keine `{ id: true, name: true, color: true }` Inline-Selects mehr in Routern (ausser dort wo erweitert) +- `pnpm --filter @planarchy/api exec tsc --noEmit` — gruen + +--- + +## Wave 4 — Status Badge & Vacation Constant Consolidation (Agent: style-consolidator) + +**Problem:** Mehrere duplizierte Konstanten-Maps fuer Status-Badges und Vacation-Type-Farben: +- `VACATION_TYPE_LABELS` dupliziert in `VacationModal.tsx` (Line ~13-18) +- `TYPE_COLORS` + `TYPE_BORDER` + `TYPE_LABELS_SHORT` identisch in `TimelineResourcePanel.tsx` (Lines ~563-580) und `TimelineProjectPanel.tsx` (Lines ~1299-1316) +- `TYPE_COLOR` fuer Kalender dupliziert in `VacationCalendar.tsx` (Line ~21) und `TeamCalendar.tsx` (Line ~8) — mit Inkonsistenz bei PUBLIC_HOLIDAY + +### Tasks + +- [ ] **4.1** Vacation-Timeline-Konstanten (`VACATION_TIMELINE_COLORS`, `VACATION_TIMELINE_BORDER`, `VACATION_TYPE_LABELS_SHORT`) in `status-styles.ts` als Exports hinzufuegen → `apps/web/src/lib/status-styles.ts` +- [ ] **4.2** Vacation-Kalender-Konstanten (`VACATION_CALENDAR_COLORS`) in `status-styles.ts` hinzufuegen, PUBLIC_HOLIDAY-Inkonsistenz auf `emerald-500` vereinheitlichen → `apps/web/src/lib/status-styles.ts` +- [ ] **4.3** `TimelineResourcePanel.tsx` — lokale `TYPE_COLORS/TYPE_BORDER/TYPE_LABELS_SHORT` (Lines ~563-580) entfernen, Import aus `status-styles.ts` → `apps/web/src/components/timeline/TimelineResourcePanel.tsx` +- [ ] **4.4** `TimelineProjectPanel.tsx` — lokale `TYPE_COLORS/TYPE_BORDER/TYPE_LABELS_SHORT` (Lines ~1299-1316) entfernen, Import aus `status-styles.ts` → `apps/web/src/components/timeline/TimelineProjectPanel.tsx` +- [ ] **4.5** `VacationModal.tsx` — lokales `VACATION_TYPE_LABELS` (Lines ~13-18) entfernen, Import aus `status-styles.ts` → `apps/web/src/components/vacations/VacationModal.tsx` +- [ ] **4.6** `VacationCalendar.tsx` — lokales `TYPE_COLOR` (Line ~21) ersetzen durch Import → `apps/web/src/components/vacations/VacationCalendar.tsx` +- [ ] **4.7** `TeamCalendar.tsx` — lokales `TYPE_COLOR` (Line ~8) ersetzen durch Import → `apps/web/src/components/vacations/TeamCalendar.tsx` + +### Akzeptanzkriterien +- Keine duplizierten Vacation/Status-Konstanten mehr in Komponenten +- `VacationCalendar` und `TeamCalendar` nutzen identische Farben + +--- + +## Wave 5 — Adopt `useInvalidatePlanningViews()` Hook (Agent: invalidation-consolidator) + +**Problem:** Hook existiert in `apps/web/src/hooks/useInvalidatePlanningViews.ts` mit 8 Queries, +wird aber von KEINER Mutation genutzt. 14+ Stellen kopieren die 4-Query-Timeline-Invalidierung manuell. +Ausserdem fehlt `getProjectContext` in `useTimelineDrag.ts` (Line ~238). + +### Tasks + +- [ ] **5.1** `useInvalidatePlanningViews` in eine `useInvalidateTimeline()` (4 Timeline-Queries) und `useInvalidateAllAllocViews()` (alle 8) aufspalten, da manche Stellen nur Timeline invalidieren → `apps/web/src/hooks/useInvalidatePlanningViews.ts` (edit) +- [ ] **5.2** `TimelineView.tsx` — 2 manuelle Invalidierungsbloecke (Lines ~73-76, ~333-336) durch Hook ersetzen → `apps/web/src/components/timeline/TimelineView.tsx` +- [ ] **5.3** `AllocationPopover.tsx` — Invalidierungsblock (Lines ~58-62) durch Hook ersetzen → `apps/web/src/components/timeline/AllocationPopover.tsx` +- [ ] **5.4** `NewAllocationPopover.tsx` — Invalidierungsblock (Lines ~63-66) ersetzen → `apps/web/src/components/timeline/NewAllocationPopover.tsx` +- [ ] **5.5** `BatchAssignPopover.tsx` — Invalidierungsblock (Lines ~54-57) ersetzen → `apps/web/src/components/timeline/BatchAssignPopover.tsx` +- [ ] **5.6** `ProjectPanel.tsx` — 3 Invalidierungsbloecke (Lines ~106-109, 115-118, 124-127) ersetzen → `apps/web/src/components/timeline/ProjectPanel.tsx` +- [ ] **5.7** `useAllocationHistory.ts` — 3 Invalidierungsbloecke (Lines ~31-34, 40-43, 62-65) ersetzen → `apps/web/src/hooks/useAllocationHistory.ts` +- [ ] **5.8** `useTimelineDrag.ts` — 2 Bloecke (Lines ~238-241, 254-257) ersetzen + fehlende `getProjectContext` Invalidierung fixen → `apps/web/src/hooks/useTimelineDrag.ts` +- [ ] **5.9** `FillOpenDemandModal.tsx` — Invalidierungsblock (Lines ~76-79) ersetzen → `apps/web/src/components/allocations/FillOpenDemandModal.tsx` + +### Akzeptanzkriterien +- Keine manuellen 4-Query-Timeline-Invalidierungsbloecke mehr +- `useTimelineDrag.ts` invalidiert alle 4 Timeline-Queries (inkl. `getProjectContext`) + +--- + +## Wave 6 — Timeline Render-Helpers & React.memo (Agent: timeline-optimizer) + +**Problem:** 3 identische Render-Funktionen in beiden Panel-Komponenten (Vacation-Blocks, Range-Overlay, Overbooking-Blink). Keine `React.memo` auf den grossen Panel-Komponenten. `useTimelineDrag.ts` ist 883 Zeilen Monolith. + +### Tasks + +- [ ] **6.1** `renderHelpers.ts` erstellen mit extrahierten Shared-Funktionen: `renderVacationBlocks()`, `renderRangeOverlay()`, `renderOverbookingBlink()` → `apps/web/src/components/timeline/renderHelpers.ts` (create) +- [ ] **6.2** `TimelineResourcePanel.tsx` — lokale `renderVacationBlocksForRow`, `renderRangeOverlay`, `renderOverbookingBlink` (Lines ~582-636, 895-930) durch Imports aus `renderHelpers.ts` ersetzen → edit +- [ ] **6.3** `TimelineProjectPanel.tsx` — lokale `renderVacationBlocksForProjectRow`, `renderRangeOverlayProject`, `renderOverbookingBlinkProject` durch Imports ersetzen → edit +- [ ] **6.4** `React.memo()` auf `TimelineResourcePanel` wrappen → `TimelineResourcePanel.tsx` +- [ ] **6.5** `React.memo()` auf `TimelineProjectPanel` wrappen → `TimelineProjectPanel.tsx` +- [ ] **6.6** Multi-Select-Intersection-Logic (Lines ~573-634 in `TimelineView.tsx`) in eigenen Hook `useMultiSelectIntersection.ts` extrahieren → `apps/web/src/hooks/useMultiSelectIntersection.ts` (create), `TimelineView.tsx` (edit) +- [ ] **6.7** `useTimelineDrag.ts` — Drag-Math-Utilities (`pixelsToDays`, `constrainToGrid`, `clampDate`) in `dragMath.ts` extrahieren → `apps/web/src/components/timeline/dragMath.ts` (create), `useTimelineDrag.ts` (edit) + +### Akzeptanzkriterien +- Keine duplizierten Render-Funktionen zwischen den Panel-Komponenten +- Beide Panels als `React.memo()` exportiert +- `useTimelineDrag.ts` unter 800 Zeilen +- Timeline rendert korrekt in beiden Views + Overbooking-Blink funktioniert + +--- + +## Wave 7 — Composite Database Indexes (Agent: db-index-optimizer) + +**Problem:** Mehrere haeufig abgefragte Modelle haben keine optimalen Composite-Indexes fuer +die kombinierten WHERE-Bedingungen der tRPC-Router. + +### Tasks + +- [ ] **7.1** `DemandRequirement` — `@@index([projectId, status, startDate, endDate])` hinzufuegen (ersetzt/ergaenzt bestehenden 3-Feld-Index) → `packages/db/prisma/schema.prisma` +- [ ] **7.2** `Resource` — `@@index([isActive, orgUnitId])` hinzufuegen fuer "aktive Ressourcen pro OrgUnit" Queries → `packages/db/prisma/schema.prisma` +- [ ] **7.3** `Project` — `@@index([status, startDate, endDate])` hinzufuegen fuer Timeline-Filterung → `packages/db/prisma/schema.prisma` +- [ ] **7.4** `Estimate` — `@@index([projectId, status])` hinzufuegen → `packages/db/prisma/schema.prisma` +- [ ] **7.5** `pnpm db:push` ausfuehren, `.next/` Cache loeschen, Dev-Server neu starten + +**Bereits optimal:** +- `Vacation` — hat `@@index([resourceId, status, startDate, endDate])` ✅ +- `Assignment` — hat `@@index([resourceId, status, startDate])` + `@@index([projectId, startDate, endDate])` ✅ + +### Akzeptanzkriterien +- Neue Indexes in Schema sichtbar +- `pnpm db:push` erfolgreich +- Dev-Server startet sauber + +--- + +## Abhaengigkeiten & Parallelisierung + +``` +Wave 1 (format) ─┐ +Wave 2 (findUniqueOrThrow) ─┤ +Wave 3 (selects) ─┤── Alle parallel ausfuehrbar (verschiedene Dateien/Domains) +Wave 4 (status-styles) ─┤ +Wave 5 (invalidation) ─┤ +Wave 6 (timeline render) ─┘ + +Wave 7 (DB indexes) ── Sequentiell NACH Wave 1-6 (erfordert db:push + Restart) ``` -### 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 `
` im Canvas-Bereich: - ```tsx - {multiSelectState.isSelecting && ( -
- )} - ``` - - Allocation-Blocks die in `selectedAllocationIds` sind: Extra CSS-Klasse `ring-2 ring-brand-500 ring-offset-1 z-20`. - -- [ ] **Task 2b:** Dasselbe für `TimelineProjectPanel.tsx` - -### Task 3: Intersection-Berechnung - -- [ ] **Task 3a:** Funktion `computeSelectedAllocations` → Datei: `TimelineResourcePanel.tsx` (oder neue Utility-Datei) - - Wird als `useMemo` in `TimelineViewContent` berechnet. Nimmt `multiSelectState` + Layout-Daten (`toLeft`, `toWidth`, Row-Höhen, Ressource-Reihenfolge) und gibt `{ allocationIds: string[], resourceIds: string[], dateRange }` zurück. - - **Algorithmus:** - 1. Konvertiere Pixel-Rechteck zu Datums-Range (via `xToDate`) und Zeilen-Range (via Row-Index-Berechnung) - 2. Für jede Ressource im Zeilen-Range: Prüfe alle Allocations ob sie zeitlich überlappen - 3. Sammle Treffer-IDs - - **Wichtig:** Die Berechnung muss die Scroll-Position des Containers berücksichtigen (`scrollContainerRef.scrollLeft/scrollTop`). - -- [ ] **Task 3b:** `setMultiSelectState` mit berechneten IDs updaten → Datei: `TimelineView.tsx` - - Nach der Intersection-Berechnung via `useEffect`: Wenn `multiSelectState` sich ändert und nicht mehr `isSelecting`, update die `selectedAllocationIds`/`selectedResourceIds`. - -### Task 4: Floating Action Bar - -- [ ] **Task 4a:** `FloatingActionBar` Komponente erstellen → Datei: `FloatingActionBar.tsx` - - ```tsx - interface FloatingActionBarProps { - selectedCount: number; - selectedResourceCount: number; - onDelete: () => void; - onAssign: () => void; - onClear: () => void; - isDeleting: boolean; - } - ``` - - Positionierung: `fixed bottom-6 left-1/2 -translate-x-1/2` — zentriert am unteren Bildschirmrand. - - UI: Pill-förmige Bar mit: - - Zähler: "3 allocations selected" oder "5 resources × 10 days selected" - - Delete-Button (rot, nur wenn Allocations selektiert) - - Assign-Button (brand, nur wenn leere Ressource-Zeilen im Bereich) - - Clear-Button (grau) - - Keyboard hint: "ESC to clear" - - Dark-Mode: `dark:bg-gray-800 dark:border-gray-700` etc. - -- [ ] **Task 4b:** FloatingActionBar in `TimelineView.tsx` einbinden - - Rendern wenn `multiSelectState.selectedAllocationIds.length > 0 || multiSelectState.selectedResourceIds.length > 0`. - - Actions verdrahten: - - Delete → `allocation.batchDelete.mutate({ ids: selectedAllocationIds })` - - Assign → öffnet `BatchAssignPopover` - - Clear → `clearMultiSelect()` - - ESC-Handler erweitern: Multi-Select hat Priorität vor anderen Overlays. - -### Task 5: Batch-Assign Popover - -- [ ] **Task 5a:** `BatchAssignPopover` Komponente erstellen → Datei: `BatchAssignPopover.tsx` - - Ähnlich wie `NewAllocationPopover`, aber für mehrere Ressourcen: - - Projekt-Picker (Dropdown mit Suche) - - Hours/day Selector - - Anzeige: "Assigning to N resources, Start – End" - - "Assign All" Button - - Props: - ```tsx - interface BatchAssignPopoverProps { - resourceIds: string[]; - startDate: Date; - endDate: Date; - onClose: () => void; - onCreated: () => void; - } - ``` - -- [ ] **Task 5b:** `batchQuickAssign` API-Endpoint erstellen → Datei: `packages/api/src/router/timeline.ts` - - ```ts - batchQuickAssign: managerProcedure - .input(z.object({ - assignments: z.array(z.object({ - resourceId: z.string(), - projectId: z.string(), - startDate: z.coerce.date(), - endDate: z.coerce.date(), - hoursPerDay: z.number().min(0.5).max(24).default(8), - role: z.string().min(1).max(200).default("Team Member"), - status: z.nativeEnum(AllocationStatus).default(AllocationStatus.PROPOSED), - })).min(1).max(50), - })) - .mutation(async ({ ctx, input }) => { - requirePermission(ctx, PermissionKey.MANAGE_ALLOCATIONS); - // Validate all, then create in single transaction - }) - ``` - -### Task 6: Integration & Polish - -- [ ] **Task 6a:** `onContextMenu` auf Canvas-Level `e.preventDefault()` setzen → Datei: `TimelineView.tsx` - - Damit das Browser-Kontextmenü nicht erscheint während der Rechtsklick-Drag aktiv ist. - -- [ ] **Task 6b:** ESC-Handler Priorität anpassen → Datei: `TimelineView.tsx` - - Reihenfolge: Multi-Select clear → Popover → NewAllocPopover → DemandModal → ProjectPanel - -- [ ] **Task 6c:** Cursor-Style anpassen → Datei: `TimelineView.tsx` - - `cursor-crosshair` wenn Multi-Select aktiv. - -- [ ] **Task 6d:** Confirmation-Dialog vor Batch-Delete → Datei: `FloatingActionBar.tsx` oder `TimelineView.tsx` - - Einfacher `window.confirm()` oder inline-Bestätigung: "Delete 5 allocations? This cannot be undone." - -## Abhängigkeiten - -- Task 1 (Hook) muss vor Task 2–6 abgeschlossen sein (State-Grundlage) -- Task 2 (Overlay) und Task 3 (Intersection) können parallel entwickelt werden, aber Task 3 hängt logisch von Task 2 ab (Overlay-Koordinaten) -- Task 4 (ActionBar) hängt von Task 1 ab (braucht multiSelectState) -- Task 5a (BatchAssignPopover) ist unabhängig vom Rest -- Task 5b (API) ist unabhängig vom Rest -- Task 6 (Integration) kommt zuletzt - -**Empfohlene Reihenfolge:** Task 1a → 1b → 1c → parallel(Task 2a+2b, Task 5b) → Task 3a → 3b → Task 4a → 4b → Task 5a → Task 6a–6d - -## Akzeptanzkriterien - -- [ ] `pnpm --filter @planarchy/web exec tsc --noEmit` — keine neuen Errors -- [ ] `pnpm --filter @planarchy/api exec tsc --noEmit` — keine neuen Errors -- [ ] Resource View: Rechtsklick + Drag zeichnet Selection-Rechteck über Canvas -- [ ] Resource View: Allocation-Blocks innerhalb des Rechtecks werden highlighted -- [ ] Floating Action Bar erscheint mit korrektem Zähler -- [ ] "Delete" löscht alle selektierten Allocations nach Bestätigung -- [ ] "Assign" öffnet BatchAssignPopover mit korrekten Ressourcen und Datumrange -- [ ] BatchAssign erstellt Allocations für alle selektierten Ressourcen in einer Transaktion -- [ ] Project View: Multi-Selection funktioniert analog -- [ ] Rechtsklick auf einzelnen Block ohne Drag: Öffnet weiterhin AllocationPopover -- [ ] Shift+Click auf Allocation-Block: Fügt ihn zur Multi-Selection hinzu (FloatingActionBar erscheint) -- [ ] Shift+Click auf bereits selektierten Block: Entfernt ihn aus der Selection -- [ ] Shift+Click + Rechtsklick-Drag kombinierbar: Erst Shift-Clicks, dann Lasso erweitert die Selection -- [ ] ESC räumt Multi-Selection auf -- [ ] Dark Mode: Alle neuen Komponenten haben `dark:` Klassen -- [ ] Browser-Kontextmenü wird unterdrückt während Multi-Select aktiv - -## Risiken & offene Fragen - -- **Scroll-Position:** Die Intersection-Berechnung muss `scrollLeft`/`scrollTop` berücksichtigen. Sonst stimmen die Pixel-Koordinaten nicht mit den Allocation-Positionen überein. -- **Virtualisierung:** `TimelineResourcePanel` nutzt `@tanstack/react-virtual`. Nicht-sichtbare Zeilen sind nicht im DOM → die Intersection-Berechnung muss auf Daten-Ebene (nicht DOM-Ebene) erfolgen. -- **Performance:** Bei vielen Allocations könnte die Intersection-Berechnung während des Drag teuer werden. Lösung: Nur bei mouseUp berechnen, nicht während mousemove. -- **Rechtsklick-Einzelblock:** Muss sauber vom Drag unterschieden werden (< 5px Threshold). Der bestehende `onContextMenu`-Handler auf Allocation-Blocks (`e.stopPropagation()`) sollte erhalten bleiben. -- **Touch-Support:** Rechtsklick hat kein Touch-Äquivalent. Long-press wäre möglich, ist aber ein separates Feature. Zunächst nur Mouse. -- **`batchQuickAssign` Limit:** Max 50 Assignments pro Call, um DB-Last zu begrenzen. +**Innerhalb der Waves:** +- Wave 1: Task 1.10 (API format-utils) kann parallel zu Tasks 1.1-1.9 (web components) +- Wave 2: Alle Tasks unabhaengig (verschiedene Router-Dateien) +- Wave 4: Task 4.1-4.2 ZUERST (erstellt Exports), dann Tasks 4.3-4.7 parallel +- Wave 5: Task 5.1 ZUERST (Hook-Refactoring), dann Tasks 5.2-5.9 parallel +- Wave 6: Task 6.1 ZUERST (erstellt renderHelpers.ts), dann Tasks 6.2-6.7 parallel + +**Dateikonflikte vermeiden:** +- Wave 2 und Wave 3 editieren teilweise gleiche Router-Dateien (`vacation.ts`, `entitlement.ts`, `calculation-rules.ts`) — diese Tasks SEQUENTIELL innerhalb eines Agents ausfuehren +- Wave 4 und Wave 6 editieren beide Panel-Dateien — unterschiedliche Abschnitte, koennen aber sicherer sequentiell sein + +--- + +## Akzeptanzkriterien (Gesamt) + +- [ ] `pnpm --filter @planarchy/web exec tsc --noEmit` — null Errors +- [ ] `pnpm --filter @planarchy/api exec tsc --noEmit` — null Errors +- [ ] `pnpm --filter @planarchy/engine exec vitest run` — alle Tests gruen +- [ ] `pnpm --filter @planarchy/staffing exec vitest run` — alle Tests gruen +- [ ] Dev-Server startet, Timeline rendert in beiden Views +- [ ] Overbooking-Blink funktioniert +- [ ] Demand-Popover und Resource-Hover-Card funktionieren +- [ ] Keine duplizierten `formatMoney/formatCents/fmtEur` Funktionsdefinitionen +- [ ] Keine manuellen `findUnique` + throw Bloecke in Routern (ausser assistant-tools.ts) +- [ ] Keine duplizierten Vacation-Type/Status-Konstanten +- [ ] Keine manuellen 4-Query Timeline-Invalidierungsbloecke + +--- + +## Risiken & Offene Fragen + +1. **Wave 2+3 Dateikonflikt:** `vacation.ts`, `entitlement.ts`, `calculation-rules.ts` werden in Wave 2 UND Wave 3 editiert. Loesung: Ein Agent bearbeitet beide Waves sequentiell fuer diese Dateien. +2. **Wave 5 Type-Cast:** `useInvalidatePlanningViews` hat einen TypeScript-Cast fuer `allocation.listView`. Wenn der Cast nach Refactoring bricht, muss der tRPC-Output-Type geprueft werden. +3. **Wave 6 memo-Props:** `React.memo()` auf Panels erfordert stabile Prop-Referenzen. Wenn Inline-Callbacks als Props uebergeben werden, muss der Parent `useCallback` nutzen — pruefe `TimelineViewContent`. +4. **Wave 7 DB-Migration:** `db:push` auf Produktions-DB erfordert Maintenance-Window fuer Index-Erstellung. Auf Dev-DB unproblematisch. +5. **assistant-tools.ts:** 45 `findUnique` Stellen mit `{ error: "..." }` Return-Pattern — NICHT auf `findUniqueOrThrow` migrieren, da das Error-Handling grundlegend anders ist (Return vs Throw). + +--- + +## Metriken (erwartet) + +| Metrik | Vorher | Nachher | +|--------|--------|---------| +| Duplizierte Format-Funktionen | 12+ Inline | 0 (1 zentrale Lib + 1 API-Helfer) | +| Manuelle findUnique+throw | ~35 Stellen | 0 (alle via Helper) | +| Inline Prisma-Selects | ~20 Duplikate | 0 (via Shared Constants) | +| Duplizierte Status-Konstanten | 7 Stellen | 0 (1 zentrale Datei) | +| Manuelle Invalidierungsbloecke | 14+ Stellen | 0 (via Hooks) | +| Duplizierte Render-Funktionen | 3 Paare (6 total) | 3 Shared (renderHelpers.ts) | +| useTimelineDrag.ts Zeilen | 883 | ~800 | +| Fehlende DB-Composite-Indexes | 4 | 0 |