# 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()`, 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 |