Engine (packages/engine): - New checkDuplicateAssignment() pure function: detects same resource assigned to same project with overlapping dates - 15 unit tests covering: overlap, no-overlap, cancelled, self-exclude, string dates, PROPOSED status Application layer (packages/application): - createAssignment: throws CONFLICT before DB write if duplicate found - fillDemandRequirement: same check before entering transaction AI Assistant (packages/api/router/assistant-tools.ts): - create_allocation: checks before creating, returns helpful error message - fill_demand: same check using demand's projectId UI (apps/web): - AllocationModal: amber warning when resource already assigned to selected project with overlapping dates (non-blocking) Database cleanup: - Found and merged 1 duplicate: Wong Wong on Porsche Taycan Sport Film (2 overlapping PROPOSED assignments merged into 1) Regression: 298 engine tests pass (283 + 15 new). TypeScript clean. Co-Authored-By: claude-flow <ruv@ruv.net>
8.5 KiB
Duplicate Assignment Prevention — Plan
Anforderungsanalyse
Problem: Ressourcen koennen demselben Projekt mehrfach zugewiesen werden mit ueberlappenden Zeitraeumen. Beispiel: Wong Wong ist zweimal fuer "Porsche Taycan Sport Film" am 15. April eingetragen.
Ursache: Weder die Application-Layer-Funktionen (createAssignment, fillDemandRequirement) noch die API-Router pruefen, ob dieselbe Resource bereits eine aktive Zuweisung zum selben Projekt im selben Zeitraum hat. Die bestehende validateAvailability prueft nur die Gesamt-Stunden (Overbooking), nicht Projekt-Duplikate.
Loesung: Duplicate-Check an 3 Stellen einfuegen (defense-in-depth):
- Application Layer —
checkDuplicateAssignment()Funktion im Engine-Paket - API Layer — Validierung in den Mutations vor dem Create
- AI Assistant —
create_allocationundfill_demandTools pruefen vor Ausfuehrung
Scope: Betrifft packages/engine, packages/application, packages/api, UI (Warnmeldung).
Betroffene Pakete & Dateien
| Paket | Dateien | Art der Aenderung |
|---|---|---|
packages/engine |
src/allocation/duplicate-check.ts |
create — Pure Funktion checkDuplicateAssignment() |
packages/engine |
src/index.ts |
edit — Export hinzufuegen |
packages/application |
src/use-cases/allocation/create-assignment.ts |
edit — Duplicate-Check vor DB-Write |
packages/application |
src/use-cases/allocation/fill-demand-requirement.ts |
edit — Duplicate-Check vor DB-Write |
packages/api |
src/router/allocation.ts |
edit — Duplicate-Check in create, createAssignment Mutations |
packages/api |
src/router/assistant-tools.ts |
edit — Check in create_allocation, fill_demand Tools |
packages/api |
src/router/timeline.ts |
edit — Check in batchShiftAllocations (falls Shift Duplikat erzeugt) |
apps/web |
src/components/allocations/AllocationModal.tsx |
edit — Warning anzeigen wenn Duplikat erkannt |
apps/web |
src/components/staffing/StaffingPanel.tsx |
edit — Warning im Assign-Formular |
packages/engine |
src/__tests__/duplicate-check.test.ts |
create — Unit Tests |
Task-Liste
Phase 1: Engine — Pure Duplicate-Check Funktion
-
Task 1: Duplicate-Check Funktion erstellen →
packages/engine/src/allocation/duplicate-check.tsinterface ExistingAssignment { id: string; resourceId: string; projectId: string; startDate: Date; endDate: Date; status: string; // nur CONFIRMED, ACTIVE, PROPOSED zaehlen } interface DuplicateCheckResult { isDuplicate: boolean; conflictingAssignment?: ExistingAssignment; message?: string; // z.B. "Resource Wong Wong is already assigned to Porsche Taycan (2026-03-01 to 2026-06-30)" } export function checkDuplicateAssignment( resourceId: string, projectId: string, startDate: Date, endDate: Date, existingAssignments: ExistingAssignment[], excludeAssignmentId?: string, // fuer Updates: eigene ID ausschliessen ): DuplicateCheckResult- Prueft: Gibt es in
existingAssignmentseine Zuweisung mit gleicher resourceId + gleicher projectId deren Zeitraum sich mit [startDate, endDate] ueberschneidet? - Ignoriert: CANCELLED Status, eigene ID (bei Updates)
- Overlap-Logik:
existingStart <= newEnd && existingEnd >= newStart
- Prueft: Gibt es in
-
Task 2: Unit Tests →
packages/engine/src/__tests__/duplicate-check.test.ts- Kein Duplikat: verschiedene Projekte
- Kein Duplikat: gleicher Projekt, aber nicht ueberlappend (vor/nach)
- Duplikat: gleicher Projekt, vollstaendig ueberlappend
- Duplikat: gleicher Projekt, teilweise ueberlappend
- Kein Duplikat: gleicher Projekt, aber CANCELLED
- Kein Duplikat: Update der eigenen Zuweisung (excludeAssignmentId)
-
Task 3: Export →
packages/engine/src/index.ts
Phase 2: Application Layer — Integration in Create-Flows
-
Task 4:
createAssignmenterweitern →packages/application/src/use-cases/allocation/create-assignment.ts- Nach dem Laden von
existingBookings(Zeile 101-106):checkDuplicateAssignment()aufrufen - Bei
isDuplicate: true:throw new TRPCError({ code: "CONFLICT", message: result.message }) - Bestehende Bookings bereits vorhanden — nur filtern auf gleichen
projectId
- Nach dem Laden von
-
Task 5:
fillDemandRequirementerweitern →packages/application/src/use-cases/allocation/fill-demand-requirement.ts- Vor dem Assignment-Create: gleicher Check
- DemandRequirement hat bereits
projectId— diesen nutzen
Phase 3: API + AI Assistant
- Task 6: AI Assistant Tools erweitern →
packages/api/src/router/assistant-tools.tscreate_allocationTool: VorcreateAssignmentCall, bestehende Assignments pruefenfill_demandTool: Gleicher Check- Bei Duplikat: Tool gibt klare Fehlermeldung zurueck statt Exception:
"Cannot assign: Wong Wong is already assigned to Porsche Taycan Sport Film from 2026-01-15 to 2026-06-30. Use update_allocation_status to modify the existing assignment instead."
Phase 4: UI Warnungen
-
Task 7: AllocationModal Warning →
apps/web/src/components/allocations/AllocationModal.tsx- Wenn User Resource + Project + Dates auswaehlt: pruefen ob Duplikat existiert
- Query:
trpc.allocation.listView({ projectId })— bereits geladen - Gelbe Warning-Box: "This resource is already assigned to this project from X to Y"
- Submit-Button nicht blockieren (Warning, nicht Error) — User kann bewusst doppelt buchen
-
Task 8: StaffingPanel Assign Warning →
apps/web/src/components/staffing/StaffingPanel.tsx- Im AssignForm: nach Project-Auswahl pruefen ob Resource bereits dort zugewiesen
- Gleiche Warning-Box wie AllocationModal
Phase 5: Bereinigung bestehender Duplikate
- Task 9: Cleanup-Script →
packages/db/scripts/deduplicate-assignments.ts- Findet alle Duplikate: gleiche resourceId + projectId mit ueberlappenden Dates
- Merged sie: behaelt die aeltere Zuweisung, entfernt die neuere (oder merged Zeitraeume)
- Dry-run Modus: zeigt was geaendert wuerde ohne zu aendern
- Kann via
pnpm --filter @planarchy/db exec tsx scripts/deduplicate-assignments.tsausgefuehrt werden
Abhaengigkeiten
Task 1 (Engine Funktion) → Task 2 (Tests) → Task 3 (Export)
↘
Task 3 → Task 4 + Task 5 (parallel, Application Layer)
Task 3 → Task 6 (AI Assistant)
Task 3 → Task 7 + Task 8 (parallel, UI Warnungen)
Task 9 (Cleanup) ist unabhaengig, kann jederzeit ausgefuehrt werden
- Tasks 4+5 koennen parallel (verschiedene Dateien)
- Tasks 6, 7, 8 koennen parallel (verschiedene Dateien)
- Task 9 sollte nach den anderen Tasks laufen (damit neue Duplikate verhindert werden)
Akzeptanzkriterien
pnpm test:unitlaeuft gruen (inkl. neue duplicate-check Tests)pnpm --filter @planarchy/web exec tsc --noEmit— keine neuen Errors- API:
createAssignmentwirft CONFLICT wenn Resource bereits zum gleichen Projekt zugewiesen - API:
fillDemandRequirementwirft CONFLICT bei Duplikat - AI Assistant:
create_allocationgibt klare Fehlermeldung bei Duplikat - AI Assistant:
fill_demandgibt klare Fehlermeldung bei Duplikat - UI: AllocationModal zeigt gelbe Warning bei erkanntem Duplikat
- UI: StaffingPanel AssignForm zeigt Warning bei Duplikat
- Cleanup: Bestehende Duplikate in der DB bereinigt
- Timeline: Wong Wong hat keine doppelten Strips mehr am 15. April
Risiken & offene Fragen
Risiken
- False Positives: Legitime Doppelbuchungen (z.B. verschiedene Rollen auf demselben Projekt) werden blockiert → Mitigation: Warning im UI, Error nur im API — User kann override-en, AI Assistant gibt Hinweis
- Race Condition: Zwei gleichzeitige Requests koennten beide den Check passieren → Mitigation: DB-Level unique constraint ist nicht moeglich (flexible Zeitraeume), aber Transaction-Isolation schuetzt
Offene Fragen
- Soll der Check nur warnen oder blockieren? → Empfehlung: API blockiert (CONFLICT), UI warnt (gelbe Box, Submit moeglich), AI blockiert
- Was passiert bei Updates/Shifts? → excludeAssignmentId nutzen um die eigene Zuweisung auszuschliessen
- Welche Status zaehlen als "aktiv"? → CONFIRMED, ACTIVE, PROPOSED — nicht CANCELLED, DRAFT
- Sollen verschiedene Rollen erlaubt sein? → Vorschlag: Ja, aber mit Warning. Gleiche Rolle + gleiches Projekt = Block, verschiedene Rolle = Warning only