chore: update plan and fix useMultiSelectIntersection types
Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
@@ -5,9 +5,13 @@
|
|||||||
|
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { LABEL_WIDTH } from "~/components/timeline/timelineConstants.js";
|
import { LABEL_WIDTH } from "~/components/timeline/timelineConstants.js";
|
||||||
import type { MultiSelectState, AllocDragMode } from "~/hooks/useTimelineDrag.js";
|
import type { MultiSelectState } from "~/hooks/useTimelineDrag.js";
|
||||||
import type { TimelineAssignmentEntry } from "~/components/timeline/TimelineContext.js";
|
import type {
|
||||||
import type { ViewMode, ResourceBrief } from "~/components/timeline/TimelineContext.js";
|
TimelineAssignmentEntry,
|
||||||
|
TimelineDemandEntry,
|
||||||
|
ViewMode,
|
||||||
|
ResourceBrief,
|
||||||
|
} from "~/components/timeline/TimelineContext.js";
|
||||||
|
|
||||||
interface ProjectGroup {
|
interface ProjectGroup {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -17,12 +21,6 @@ interface ProjectGroup {
|
|||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DemandEntry {
|
|
||||||
id: string;
|
|
||||||
startDate: Date | string;
|
|
||||||
endDate: Date | string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useMultiSelectIntersection({
|
export function useMultiSelectIntersection({
|
||||||
multiSelectState,
|
multiSelectState,
|
||||||
setMultiSelectState,
|
setMultiSelectState,
|
||||||
@@ -47,7 +45,7 @@ export function useMultiSelectIntersection({
|
|||||||
resources: ResourceBrief[];
|
resources: ResourceBrief[];
|
||||||
allocsByResource: Map<string, TimelineAssignmentEntry[]>;
|
allocsByResource: Map<string, TimelineAssignmentEntry[]>;
|
||||||
projectGroups: ProjectGroup[];
|
projectGroups: ProjectGroup[];
|
||||||
openDemandsByProject: Map<string, DemandEntry[]>;
|
openDemandsByProject: Map<string, TimelineDemandEntry[]>;
|
||||||
dates: Date[];
|
dates: Date[];
|
||||||
today: Date;
|
today: Date;
|
||||||
CELL_WIDTH: number;
|
CELL_WIDTH: number;
|
||||||
|
|||||||
@@ -1,321 +1,258 @@
|
|||||||
# Right-Click Multi-Selection on Timeline
|
# Refactor v2 — Code Optimization, De-duplication & Maintainability
|
||||||
|
|
||||||
## Anforderungsanalyse
|
## 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
|
## Betroffene Pakete & Dateien
|
||||||
|
|
||||||
| Paket | Dateien | Art der Änderung |
|
| Paket | Dateien | Art |
|
||||||
|-------|---------|-----------------:|
|
|-------|---------|-----|
|
||||||
| apps/web | `src/hooks/useTimelineDrag.ts` | edit — neuer `multiSelectState`, right-click drag handler |
|
| `apps/web` | `src/lib/format.ts`, 10+ Consumer-Dateien | edit |
|
||||||
| apps/web | `src/components/timeline/TimelineView.tsx` | edit — multiSelectState empfangen, FloatingActionBar rendern, batch-Actions verdrahten |
|
| `apps/web` | `src/lib/status-styles.ts`, `src/components/timeline/timelineConstants.ts`, 6 Consumer-Dateien | edit |
|
||||||
| apps/web | `src/components/timeline/FloatingActionBar.tsx` | create — schwebende Toolbar-Komponente |
|
| `apps/web` | `src/hooks/useInvalidatePlanningViews.ts`, 14 Consumer-Dateien | edit |
|
||||||
| apps/web | `src/components/timeline/BatchAssignPopover.tsx` | create — Projekt-Picker für Multi-Resource-Assignment |
|
| `apps/web` | `src/components/timeline/renderHelpers.ts` | create |
|
||||||
| apps/web | `src/components/timeline/TimelineResourcePanel.tsx` | edit — Selection-Overlay rendern, onContextMenu anpassen |
|
| `apps/web` | `src/components/timeline/TimelineResourcePanel.tsx`, `TimelineProjectPanel.tsx` | edit |
|
||||||
| apps/web | `src/components/timeline/TimelineProjectPanel.tsx` | edit — Selection-Overlay rendern, onContextMenu anpassen |
|
| `apps/web` | `src/components/timeline/TimelineView.tsx` | edit |
|
||||||
| packages/api | `src/router/timeline.ts` | edit — neuer `batchQuickAssign`-Endpoint |
|
| `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
|
## Wave 1 — Centralize `formatMoney()` / `formatCents()` (Agent: format-consolidator)
|
||||||
- **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
|
**Problem:** Zentralisierte Funktionen existieren in `apps/web/src/lib/format.ts`, aber 10+
|
||||||
Neuer State `MultiSelectState` im `useTimelineDrag`-Hook:
|
Stellen nutzen Inline-`(x / 100).toLocaleString("de-DE", ...)` statt der zentralen Imports.
|
||||||
|
Zusaetzlich gibt es 2 lokale `fmtEur()` Helfer in API-Routern.
|
||||||
|
|
||||||
```ts
|
**Dateien die keine Aenderung brauchen:**
|
||||||
interface MultiSelectState {
|
- `ShiftPreviewTooltip.tsx` — spezialisierter Delta-Formatter mit +/- Prefix (bleibt)
|
||||||
isSelecting: boolean;
|
|
||||||
// Rectangle coordinates (canvas-relative pixels)
|
### Tasks
|
||||||
startX: number;
|
|
||||||
startY: number;
|
- [ ] **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`
|
||||||
currentX: number;
|
- [ ] **1.2** Inline-Formatierung in `DemandPopover.tsx` (Lines ~146/152) durch `formatCents()` Import ersetzen → `apps/web/src/components/timeline/DemandPopover.tsx`
|
||||||
currentY: number;
|
- [ ] **1.3** Inline-Formatierung in `ResourceHoverCard.tsx` (Lines ~123/130) durch `formatCents()` Import ersetzen → `apps/web/src/components/timeline/ResourceHoverCard.tsx`
|
||||||
// Resolved after mouseUp:
|
- [ ] **1.4** Inline-Formatierung in `ProjectWizard.tsx` (Lines ~509-511) ersetzen → `apps/web/src/components/projects/ProjectWizard.tsx`
|
||||||
selectedAllocationIds: string[];
|
- [ ] **1.5** Inline-Formatierung in `ProjectAssignmentsTable.tsx` (Line ~130) ersetzen → `apps/web/src/components/projects/ProjectAssignmentsTable.tsx`
|
||||||
selectedResourceIds: string[]; // Resources within the rectangle (for empty-row assign)
|
- [ ] **1.6** Inline-Formatierung in `ProjectDemandsTable.tsx` (Lines ~135/138-139) ersetzen → `apps/web/src/components/projects/ProjectDemandsTable.tsx`
|
||||||
dateRange: { start: Date; end: Date } | null;
|
- [ ] **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<T>()`, 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
|
**Innerhalb der Waves:**
|
||||||
Die Selektion geschieht als **Rectangle Intersection**:
|
- Wave 1: Task 1.10 (API format-utils) kann parallel zu Tasks 1.1-1.9 (web components)
|
||||||
1. Während des Drag wird ein visuelles Rechteck gezeichnet (semi-transparenter blauer Rahmen)
|
- Wave 2: Alle Tasks unabhaengig (verschiedene Router-Dateien)
|
||||||
2. Bei mouseUp wird berechnet, welche Allocation-Blocks innerhalb des Rechtecks liegen (Pixel-basiert: Block-Position vs. Selection-Rect)
|
- Wave 4: Task 4.1-4.2 ZUERST (erstellt Exports), dann Tasks 4.3-4.7 parallel
|
||||||
3. Selektierte Blocks erhalten einen visuellen Highlight (z.B. `ring-2 ring-brand-500`)
|
- 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
|
||||||
**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.
|
|
||||||
|
**Dateikonflikte vermeiden:**
|
||||||
### Batch-API
|
- Wave 2 und Wave 3 editieren teilweise gleiche Router-Dateien (`vacation.ts`, `entitlement.ts`, `calculation-rules.ts`) — diese Tasks SEQUENTIELL innerhalb eines Agents ausfuehren
|
||||||
- **Delete:** `allocation.batchDelete` existiert bereits (max 100 IDs)
|
- Wave 4 und Wave 6 editieren beide Panel-Dateien — unterschiedliche Abschnitte, koennen aber sicherer sequentiell sein
|
||||||
- **Assign:** Neuer `timeline.batchQuickAssign`-Endpoint, der ein Array von `{ resourceId, projectId, startDate, endDate, hoursPerDay }` akzeptiert und in einer Transaktion erstellt
|
|
||||||
|
---
|
||||||
## Task-Liste
|
|
||||||
|
## Akzeptanzkriterien (Gesamt)
|
||||||
### Task 1: Multi-Select State im Drag-Hook
|
|
||||||
|
- [ ] `pnpm --filter @planarchy/web exec tsc --noEmit` — null Errors
|
||||||
- [ ] **Task 1a:** `MultiSelectState` Interface + Initial State definieren → Datei: `useTimelineDrag.ts`
|
- [ ] `pnpm --filter @planarchy/api exec tsc --noEmit` — null Errors
|
||||||
|
- [ ] `pnpm --filter @planarchy/engine exec vitest run` — alle Tests gruen
|
||||||
```ts
|
- [ ] `pnpm --filter @planarchy/staffing exec vitest run` — alle Tests gruen
|
||||||
export interface MultiSelectState {
|
- [ ] Dev-Server startet, Timeline rendert in beiden Views
|
||||||
isSelecting: boolean;
|
- [ ] Overbooking-Blink funktioniert
|
||||||
startX: number;
|
- [ ] Demand-Popover und Resource-Hover-Card funktionieren
|
||||||
startY: number;
|
- [ ] Keine duplizierten `formatMoney/formatCents/fmtEur` Funktionsdefinitionen
|
||||||
currentX: number;
|
- [ ] Keine manuellen `findUnique` + throw Bloecke in Routern (ausser assistant-tools.ts)
|
||||||
currentY: number;
|
- [ ] Keine duplizierten Vacation-Type/Status-Konstanten
|
||||||
selectedAllocationIds: string[];
|
- [ ] Keine manuellen 4-Query Timeline-Invalidierungsbloecke
|
||||||
selectedResourceIds: string[];
|
|
||||||
dateRange: { start: Date; end: Date } | null;
|
---
|
||||||
}
|
|
||||||
```
|
## Risiken & Offene Fragen
|
||||||
|
|
||||||
- [ ] **Task 1b:** Right-click drag handlers implementieren → Datei: `useTimelineDrag.ts`
|
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.
|
||||||
Neuer `onCanvasRightMouseDown` Handler:
|
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`.
|
||||||
- Prüfe `e.button === 2`
|
4. **Wave 7 DB-Migration:** `db:push` auf Produktions-DB erfordert Maintenance-Window fuer Index-Erstellung. Auf Dev-DB unproblematisch.
|
||||||
- `e.preventDefault()` (verhindert nativen Kontextmenü)
|
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).
|
||||||
- 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)
|
## Metriken (erwartet)
|
||||||
- Bei mouseUp mit Bewegung: Setze `isSelecting: false` aber behalte `selectedAllocationIds`/`selectedResourceIds` (werden vom Parent berechnet und reingesetzt)
|
|
||||||
|
| Metrik | Vorher | Nachher |
|
||||||
Neuer `onCanvasContextMenu` Handler:
|
|--------|--------|---------|
|
||||||
- Wird auf dem Canvas registriert, um `e.preventDefault()` global zu setzen (verhindert Browser-Kontextmenü)
|
| Duplizierte Format-Funktionen | 12+ Inline | 0 (1 zentrale Lib + 1 API-Helfer) |
|
||||||
|
| Manuelle findUnique+throw | ~35 Stellen | 0 (alle via Helper) |
|
||||||
Return-Werte erweitern um `multiSelectState`, `setMultiSelectState`, `onCanvasRightMouseDown`, `clearMultiSelect`.
|
| Inline Prisma-Selects | ~20 Duplikate | 0 (via Shared Constants) |
|
||||||
|
| Duplizierte Status-Konstanten | 7 Stellen | 0 (1 zentrale Datei) |
|
||||||
- [ ] **Task 1c:** `clearMultiSelect` Funktion → Datei: `useTimelineDrag.ts`
|
| Manuelle Invalidierungsbloecke | 14+ Stellen | 0 (via Hooks) |
|
||||||
|
| Duplizierte Render-Funktionen | 3 Paare (6 total) | 3 Shared (renderHelpers.ts) |
|
||||||
Setzt `multiSelectState` auf Initial zurück. Wird von ESC-Handler und FloatingActionBar genutzt.
|
| useTimelineDrag.ts Zeilen | 883 | ~800 |
|
||||||
|
| Fehlende DB-Composite-Indexes | 4 | 0 |
|
||||||
### 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 `<div>` im Canvas-Bereich:
|
|
||||||
```tsx
|
|
||||||
{multiSelectState.isSelecting && (
|
|
||||||
<div
|
|
||||||
className="absolute border-2 border-brand-500 bg-brand-500/10 pointer-events-none z-30 rounded"
|
|
||||||
style={{
|
|
||||||
left: Math.min(multiSelectState.startX, multiSelectState.currentX),
|
|
||||||
top: Math.min(multiSelectState.startY, multiSelectState.currentY),
|
|
||||||
width: Math.abs(multiSelectState.currentX - multiSelectState.startX),
|
|
||||||
height: Math.abs(multiSelectState.currentY - multiSelectState.startY),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
```
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|||||||
Reference in New Issue
Block a user