Files
CapaKraken/plan.md
T
2026-03-19 00:37:14 +01:00

259 lines
17 KiB
Markdown

# Refactor v2 — Code Optimization, De-duplication & Maintainability
## Anforderungsanalyse
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`.
Die Analyse identifiziert **7 unabhaengige Arbeitsstroeme (Waves)**, die von Agenten parallel
bearbeitet werden koennen, plus eine abschliessende Schema-Migration.
---
## Betroffene Pakete & Dateien
| 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 |
---
## Wave 1 — Centralize `formatMoney()` / `formatCents()` (Agent: format-consolidator)
**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.
**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<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)
```
**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 |