# Multi-Projekt-Architektur

Jeder Tenant kann mehrere Compliance-Projekte anlegen (z.B. verschiedene Produkte, Tochterunternehmen). CompanyProfile ist **pro Projekt** — nicht tenant-weit.

## Uebersicht

```mermaid
graph TD
    T[Tenant] --> P1[Projekt A: KI-Produkt X]
    T --> P2[Projekt B: SaaS API]
    T --> P3[Projekt C: Tochter GmbH]
    P1 --> S1[SDK State A]
    P2 --> S2[SDK State B]
    P3 --> S3[SDK State C]
    S1 --> CP1[CompanyProfile A]
    S2 --> CP2[CompanyProfile B]
    S3 --> CP3[CompanyProfile C]
```

## Datenmodell

### compliance_projects

| Spalte | Typ | Beschreibung |
|--------|-----|--------------|
| `id` | UUID | Primaerschluessel |
| `tenant_id` | VARCHAR(255) | Tenant-Zuordnung |
| `name` | VARCHAR(500) | Projektname |
| `description` | TEXT | Beschreibung |
| `customer_type` | VARCHAR(20) | `'new'` oder `'existing'` |
| `status` | VARCHAR(20) | `'active'`, `'archived'`, `'deleted'` |
| `project_version` | INTEGER | Versionszaehler |
| `completion_percentage` | INTEGER | Fortschritt (0-100) |
| `created_at` | TIMESTAMPTZ | Erstellungszeitpunkt |
| `updated_at` | TIMESTAMPTZ | Letzte Aenderung |
| `archived_at` | TIMESTAMPTZ | Archivierungszeitpunkt |

### sdk_states (erweitert)

- UNIQUE-Constraint auf `(tenant_id, project_id)` statt nur `tenant_id`
- `project_id UUID NOT NULL` — FK auf `compliance_projects(id) ON DELETE CASCADE`

### Daten-Ownership

| Daten | Scope | Speicherort |
|-------|-------|-------------|
| Firmenname, Rechtsform, DSB, Standorte | Pro Projekt | `sdk_states.state.companyProfile` |
| Projektname, Typ, Status | Projekt | `compliance_projects` |
| SDK State (VVT, DSFA, TOM, etc.) | Projekt | `sdk_states` (JSONB) |

## Backend API

Alle Endpoints sind tenant-isoliert via `X-Tenant-ID` Header.

### Endpoints

| Method | Endpoint | Beschreibung |
|--------|----------|--------------|
| GET | `/api/v1/projects` | Alle aktiven Projekte des Tenants |
| POST | `/api/v1/projects` | Neues Projekt erstellen |
| GET | `/api/v1/projects/{id}` | Einzelnes Projekt laden |
| PATCH | `/api/v1/projects/{id}` | Projekt aktualisieren |
| DELETE | `/api/v1/projects/{id}` | Projekt archivieren (Soft Delete) |

### Projekt erstellen

```json
POST /api/v1/projects
{
    "name": "KI-Produkt X",
    "description": "DSGVO-Compliance fuer Produkt X",
    "customer_type": "existing",
    "copy_from_project_id": "uuid-123"
}
```

**Response (201):**

```json
{
    "id": "uuid-new",
    "tenant_id": "uuid-tenant",
    "name": "KI-Produkt X",
    "description": "DSGVO-Compliance fuer Produkt X",
    "customer_type": "existing",
    "status": "active",
    "project_version": 1,
    "completion_percentage": 0,
    "created_at": "2026-03-09T12:00:00Z",
    "updated_at": "2026-03-09T12:00:00Z"
}
```

### Stammdaten-Kopie

Wenn `copy_from_project_id` angegeben, kopiert das Backend `companyProfile` aus dem Quell-State in den neuen State. Die kopierten Daten sind danach **unabhaengig editierbar**.

Anwendungsfall: Konzern mit Tochterfirmen — gleiche Rechtsform, aber unterschiedliche Adresse/Mitarbeiterzahl.

### Projekt archivieren

```
DELETE /api/v1/projects/{id}
```

Setzt `status='archived'` und `archived_at=NOW()`. Archivierte Projekte erscheinen nicht in der Standardliste (nur mit `?include_archived=true`).

## Frontend

### URL-Schema

```
/sdk                      → Projektliste (ProjectSelector)
/sdk?project={uuid}       → Dashboard im Projekt-Kontext
/sdk/vvt?project={uuid}   → VVT im Projekt-Kontext
/sdk/dsfa?project={uuid}  → DSFA im Projekt-Kontext
```

Alle internen Links enthalten automatisch `?project=`.

### Komponenten

| Komponente | Datei | Beschreibung |
|------------|-------|--------------|
| `ProjectSelector` | `components/sdk/ProjectSelector/ProjectSelector.tsx` | Projektliste + Erstellen-Dialog |
| `ProjectCard` | (gleiche Datei) | Einzelne Projektkarte |
| `CreateProjectDialog` | (gleiche Datei) | Modal fuer neues Projekt |

### State-Isolation (Multi-Tab)

- **BroadcastChannel:** `sdk-state-sync-{tenantId}-{projectId}` — pro Projekt
- **localStorage:** `ai-compliance-sdk-state-{tenantId}-{projectId}` — pro Projekt
- Tab A (Projekt X) und Tab B (Projekt Y) interferieren nicht

### SDKProvider

```typescript
<SDKProvider
  enableBackendSync={true}
  projectId={searchParams.get('project')}
>
  {children}
</SDKProvider>
```

### Context-Methoden

```typescript
const {
  createProject,    // (name, customerType) => Promise<ProjectInfo>
  listProjects,     // () => Promise<ProjectInfo[]>
  switchProject,    // (projectId) => void (navigiert zu ?project=)
  archiveProject,   // (projectId) => Promise<void>
  projectId,        // aktuelles Projekt-UUID
} = useSDK()
```

## Migration (039)

**Datei:** `backend-compliance/migrations/039_compliance_projects.sql`

1. Erstellt `compliance_projects` Tabelle
2. Entfernt `UNIQUE(tenant_id)` von `sdk_states`
3. Fuegt `project_id UUID` zu `sdk_states` hinzu
4. Migriert bestehende Daten (Default-Projekt pro existierendem State)
5. Setzt `project_id` auf `NOT NULL`

### Migration ausfuehren

```bash
ssh macmini "/usr/local/bin/docker exec bp-compliance-backend \
  psql \$COMPLIANCE_DATABASE_URL -f /app/migrations/039_compliance_projects.sql"
```

## Tests

```bash
# Backend-Tests
ssh macmini "/usr/local/bin/docker exec bp-compliance-backend \
  pytest tests/test_project_routes.py -v"
```

| Test | Beschreibung |
|------|--------------|
| `TestCreateProjectRequest` | Model-Validierung (Name, Defaults) |
| `TestUpdateProjectRequest` | Partial Update Model |
| `TestRowToResponse` | DB-Row-zu-Response Konvertierung |
| `TestCreateProjectCopiesProfile` | Copy-Request Model |
| `TestTenantIsolation` | SQL-Queries filtern nach tenant_id |
| `TestStateIsolation` | sdk_states + companyProfile pro Projekt |
