# 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):
1. **Application Layer** — `checkDuplicateAssignment()` Funktion im Engine-Paket
2. **API Layer** — Validierung in den Mutations vor dem Create
3. **AI Assistant** — `create_allocation` und `fill_demand` Tools 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.ts`
  ```typescript
  interface 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 `existingAssignments` eine 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`

- [ ] **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:** `createAssignment` erweitern → `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`

- [ ] **Task 5:** `fillDemandRequirement` erweitern → `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.ts`
  - `create_allocation` Tool: Vor `createAssignment` Call, bestehende Assignments pruefen
  - `fill_demand` Tool: 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.ts` ausgefuehrt 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:unit` laeuft gruen (inkl. neue duplicate-check Tests)
- [ ] `pnpm --filter @planarchy/web exec tsc --noEmit` — keine neuen Errors
- [ ] **API:** `createAssignment` wirft CONFLICT wenn Resource bereits zum gleichen Projekt zugewiesen
- [ ] **API:** `fillDemandRequirement` wirft CONFLICT bei Duplikat
- [ ] **AI Assistant:** `create_allocation` gibt klare Fehlermeldung bei Duplikat
- [ ] **AI Assistant:** `fill_demand` gibt 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
1. **Soll der Check nur warnen oder blockieren?**
   → Empfehlung: API blockiert (CONFLICT), UI warnt (gelbe Box, Submit moeglich), AI blockiert
2. **Was passiert bei Updates/Shifts?**
   → excludeAssignmentId nutzen um die eigene Zuweisung auszuschliessen
3. **Welche Status zaehlen als "aktiv"?**
   → CONFIRMED, ACTIVE, PROPOSED — nicht CANCELLED, DRAFT
4. **Sollen verschiedene Rollen erlaubt sein?**
   → Vorschlag: Ja, aber mit Warning. Gleiche Rolle + gleiches Projekt = Block, verschiedene Rolle = Warning only
