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

17 KiB

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.tsapps/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.tsapps/web/src/components/timeline/TimelineProjectPanel.tsx
  • 4.5 VacationModal.tsx — lokales VACATION_TYPE_LABELS (Lines ~13-18) entfernen, Import aus status-styles.tsapps/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