# INSIGHT CRM - Kommunikation Frontend <-> Backend

Dieses Dokument dient als Kommunikationskanal zwischen dem Frontend- und dem CRM-Backend-Entwickler.

---

## 2026-03-10 | Frontend: Erster Stand der CRM-Integration

### Was wurde umgesetzt

Das komplette CRM-Frontend-Modul ist implementiert und auf dem Server deployed (`feature/crm-service` Branch, Commit `c739dce`).

#### Neue Dateien (16 Dateien, ~4.800 Zeilen)

```
packages/frontend/src/crm/
  types.ts                              -- Zentrale Interfaces (alle Entitaeten + API-Wrapper)
  api.ts                                -- API-Funktionen (Axios, baseURL /api/v1/crm/*)
  hooks.ts                              -- React Query Hooks + Query Key Factory
  contacts/
    ContactsPage.tsx + .module.css      -- Liste mit Suche, Typ-Filter, Paginierung
    ContactFormModal.tsx                -- Create/Edit Modal (Person/Organisation)
    ContactDetailPage.tsx + .module.css -- 2-Spalten: Info+Deals links, Aktivitaeten rechts
  deals/
    DealsPage.tsx + .module.css         -- Liste mit Pipeline/Stage/Status-Filter
    DealFormModal.tsx                   -- Create/Edit mit Kontakt-Suche + Pipeline/Stage-Selektor
    DealDetailPage.tsx + .module.css    -- Detail mit Stage-Fortschrittsbalken
  pipelines/
    PipelinesPage.tsx + .module.css     -- Verwaltung mit klappbaren Cards + Stage-Management
  activities/
    ActivityFormModal.tsx               -- Formular fuer Notiz/Anruf/E-Mail/Meeting/Aufgabe
```

#### Geaenderte Dateien (3)

| Datei | Aenderung |
|-------|-----------|
| `src/shell/App.tsx` | 5 CRM-Routen (`/crm/contacts`, `/crm/contacts/:id`, `/crm/deals`, `/crm/deals/:id`, `/crm/pipelines`) |
| `src/shell/AppLayout.tsx` | CRM-Sektion in Sidebar (aufklappbar, 3 NavLinks mit SVG-Icons) |
| `vite.config.ts` | Proxy `/api/v1/crm` -> `localhost:3100` fuer lokale Entwicklung |

### Welche API-Endpoints werden genutzt

| Modul | Endpoints | Methoden |
|-------|-----------|----------|
| Contacts | `/crm/contacts`, `/crm/contacts/:id` | GET (list+detail), POST, PATCH, DELETE |
| Deals | `/crm/deals`, `/crm/deals/:id` | GET (list+detail), POST, PATCH, DELETE |
| Pipelines | `/crm/pipelines`, `/crm/pipelines/:id`, `/crm/pipelines/:id/stages`, `/crm/pipelines/:id/stages/:stageId` | GET (list+detail), POST, PATCH, DELETE |
| Activities | `/crm/activities`, `/crm/activities/:id` | GET (list), POST, PATCH, DELETE |

### Erwartete Response-Formate

**Liste (paginiert):**
```json
{
  "success": true,
  "data": [...],
  "pagination": { "page": 1, "pageSize": 25, "total": 42, "totalPages": 2 },
  "meta": { "timestamp": "..." }
}
```

**Einzelobjekt:**
```json
{
  "success": true,
  "data": { ... },
  "meta": { "timestamp": "..." }
}
```

**Fehler:**
```json
{
  "success": false,
  "error": { "code": "NOT_FOUND", "message": "...", "details": [] },
  "meta": { "timestamp": "..." }
}
```

### Annahmen / Abhaengigkeiten ans Backend

1. **Contact-Detail liefert Activities mit** -- `GET /crm/contacts/:id` gibt die letzten 10 Aktivitaeten im Feld `activities` zurueck. Das Frontend zeigt diese in der Timeline an.

2. **Deal-Detail liefert Relations mit** -- `GET /crm/deals/:id` gibt `pipeline`, `stage` und `contact` als verschachtelte Objekte zurueck.

3. **Pipeline-List liefert Stages mit** -- `GET /crm/pipelines` gibt jede Pipeline inkl. `stages[]` Array zurueck. Das Frontend nutzt diese fuer die Stage-Selektoren im Deal-Formular.

4. **Deal.value ist ein String** -- Decimal kommt als String vom Backend (z.B. `"24000.00"`). Das Frontend parst mit `parseFloat()`.

5. **Sortierung** -- Contacts: `createdAt`, `firstName`, `lastName`, `companyName`, `email`. Deals: `createdAt`, `title`, `value`, `expectedCloseDate`.

6. **Suche** -- Contacts: Substring-Match in `firstName`, `lastName`, `companyName`, `email`. Deals: Substring-Match in `title`.

### Bekannte Offene Punkte

- [ ] **Traefik HTTPS-Router fuer CRM**: Aktuell hat der CRM-Service nur einen HTTP-Router (`web` Entrypoint). Fuer HTTPS (`websecure`) muesste ein zweiter Router mit `tls=true` angelegt werden (wie bei `core-api-secure`).
- [ ] **Activity-Liste komplett laden**: Die Contact-Detail-Seite zeigt nur die letzten 10 Aktivitaeten (aus dem Contact-Objekt). Fuer eine "Alle anzeigen"-Funktion wuerde ein separater `GET /crm/activities?contactId=...` Aufruf benoetigt (Hook existiert bereits).
- [ ] **Kanban-Board fuer Deals**: Aktuell nur Tabellenansicht. Ein Drag-and-Drop Kanban-Board waere ein sinnvolles Feature fuer spaeter.
- [ ] **Pipeline-Stages bearbeiten**: Derzeit kann man Stages nur hinzufuegen und loeschen, nicht den Namen/Farbe/Reihenfolge aendern (kein PATCH-Endpoint fuer Stages vorhanden).

---

## 2026-03-10 | Backend: Antwort auf Frontend-Integrationsbericht

### Status der Annahmen

Alle 6 Annahmen des Frontend-Entwicklers sind korrekt und werden vom Backend unterstuetzt:

| # | Annahme | Status | Hinweise |
|---|---------|--------|----------|
| 1 | Contact-Detail liefert Activities mit | **Bestaetigt** | `GET /crm/contacts/:id` liefert `activities[]` (letzte 10, sortiert nach `createdAt desc`) |
| 2 | Deal-Detail liefert Relations mit | **Bestaetigt** | `GET /crm/deals/:id` liefert `pipeline` (inkl. aller Stages!), `stage`, `contact` |
| 3 | Pipeline-List liefert Stages mit | **Bestaetigt** | `GET /crm/pipelines` liefert jede Pipeline inkl. `stages[]` + `_count.deals` |
| 4 | Deal.value ist ein String | **Bestaetigt** | Decimal wird als String geliefert (z.B. `"24000"`) |
| 5 | Sortierung | **Bestaetigt** | Contacts: `createdAt`, `firstName`, `lastName`, `companyName`, `email`. Deals: `createdAt`, `updatedAt`, `title`, `value`, `expectedCloseDate` |
| 6 | Suche | **Bestaetigt** | Contacts: case-insensitive Substring in `firstName`, `lastName`, `companyName`, `email`. Deals: case-insensitive Substring in `title` |

### Erledigte Offene Punkte

- [x] **Traefik HTTPS-Router**: `crm-secure` Router angelegt mit `entrypoints=websecure`, `tls=true`, Priority 100. Deployed in Commit `c9e2c4a`.

- [x] **Pipeline-Stages bearbeiten**: Neuer Endpoint `PATCH /crm/pipelines/:id/stages/:stageId` hinzugefuegt. Akzeptiert:
  ```json
  {
    "name": "Neuer Name",     // optional, max 200
    "sortOrder": 2,            // optional, int >= 0
    "color": "#EF4444"         // optional, Hex #RRGGBB
  }
  ```

### Noch offene Punkte (Backend-Sicht)

- [ ] **Activity-Liste komplett laden**: `GET /crm/activities?contactId=...` ist bereits implementiert und funktioniert. Das Frontend kann den bestehenden Hook direkt nutzen.
- [ ] **Kanban-Board**: Backend-seitig kein Handlungsbedarf - `PATCH /crm/deals/:id` mit `{ stageId: "..." }` genuegt fuer Drag & Drop.

### Hinweis zu Pipeline-Detail bei Deals

`GET /crm/deals/:id` liefert die Pipeline **inklusive aller Stages** (nicht nur die aktuelle Stage). Das ist nuetzlich fuer den Stage-Fortschrittsbalken im `DealDetailPage.tsx`:

```json
{
  "pipeline": {
    "id": "...",
    "name": "Standard Sales",
    "stages": [
      { "id": "...", "name": "Qualifizierung", "sortOrder": 0, "color": "#3B82F6" },
      { "id": "...", "name": "Angebot", "sortOrder": 1, "color": "#F59E0B" },
      { "id": "...", "name": "Verhandlung", "sortOrder": 2, "color": "#EF4444" },
      { "id": "...", "name": "Abschluss", "sortOrder": 3, "color": "#10B981" }
    ]
  },
  "stage": { "id": "...", "name": "Angebot", "color": "#F59E0B" }
}
```

### Deployment-Info

- Branch: `feature/crm-service`, Commit: `c9e2c4a`
- Server: insight-dev-01 (172.20.10.59)
- Container: `insight-crm` (neu gebuildet und deployed)
- Swagger-Docs: http://172.20.10.59/api/v1/crm/docs/

---

## 2026-03-10 | Frontend: Update nach Backend-Feedback

### Umgesetzte Verbesserungen

Basierend auf der Backend-Antwort wurden folgende Aenderungen umgesetzt (Commit `0b78160`):

#### 1. Pipeline-Stages inline bearbeitbar

Die `PipelinesPage.tsx` nutzt jetzt den neuen `PATCH /crm/pipelines/:id/stages/:stageId` Endpoint:

- Jede Stage kann per **Doppelklick** oder **Stift-Icon** bearbeitet werden
- Inline-Formular mit Name-Input und Color-Picker
- Speichern mit Enter oder Haekchen, Abbrechen mit Escape oder X
- Neuer Hook: `useUpdateStage()` mit automatischer Query-Invalidierung

#### 2. DealDetailPage optimiert

`DealDetailPage.tsx` nutzt jetzt **direkt `deal.pipeline.stages`** aus dem Deal-Objekt fuer den Stage-Fortschrittsbalken. Der vorherige separate `usePipeline()` API-Call wurde entfernt.

#### 3. UI-Umbenennung: "Deals" -> "Vorgaenge"

Alle user-facing Strings wurden umbenannt:
- Sidebar: "Deals" -> "Vorgaenge"
- Seitentitel: "Deals" -> "Vorgaenge"
- Buttons: "Neuer Deal" -> "Neuer Vorgang"
- Modals: "Deal bearbeiten/loeschen" -> "Vorgang bearbeiten/loeschen"
- Fehlermeldungen und Leer-Zustaende angepasst

**Hinweis**: API-Pfade (`/crm/deals`), TypeScript-Typen (`Deal`, `DealStatus`) und Komponentennamen (`DealsPage`, `DealFormModal`) bleiben unveraendert — nur die UI-Texte wurden geaendert.

### Aktualisierte Offene Punkte

- [x] ~~Pipeline-Stages bearbeiten~~ — Frontend nutzt den neuen PATCH-Endpoint
- [x] ~~DealDetail separater Pipeline-Call~~ — Nutzt jetzt deal.pipeline.stages
- [ ] **Activity-Liste komplett laden** — Hook existiert, UI-Button "Alle anzeigen" fehlt noch
- [ ] **Kanban-Board fuer Vorgaenge** — Feature fuer spaeter geplant

### Deployment-Info

- Branch: `feature/crm-service`, Commit: `0b78160`
- Server: insight-dev-01 (172.20.10.59)
- Container: `insight-frontend` neu gebaut und deployed

---

## 2026-03-10 | Backend: Neues Company-Modul + Aenderungen an Contact und Deal

### Neue Entity: Company (Unternehmen)

Unternehmen sind jetzt als eigenstaendige Entity implementiert. Sie dienen als uebergeordnete Ebene fuer Kontakte und Vorgaenge. Ein Unternehmen kann mehrere Kontakte und Vorgaenge haben.

### Neue API-Endpoints

| Methode | Pfad | Beschreibung |
|---------|------|-------------|
| GET | `/crm/companies` | Liste (paginiert, filterbar, suchbar) |
| POST | `/crm/companies` | Unternehmen erstellen |
| GET | `/crm/companies/:id` | Detail (inkl. Kontakte + Vorgaenge) |
| PATCH | `/crm/companies/:id` | Unternehmen aktualisieren |
| DELETE | `/crm/companies/:id` | Unternehmen loeschen |

### Company-Objekt (Felder)

```typescript
interface Company {
  id: string;            // UUID
  tenantId: string;      // UUID
  name: string;          // Pflichtfeld, max 200
  industry?: string;     // max 100
  email?: string;        // max 255
  phone?: string;        // max 50
  website?: string;      // max 500
  street?: string;       // max 200
  zip?: string;          // max 20
  city?: string;         // max 100
  state?: string;        // max 100
  country?: string;      // Default "DE", 2-Zeichen ISO
  notes?: string;        // Freitext
  tags: string[];        // Default []
  isActive: boolean;     // Default true
  createdBy: string;     // UUID
  updatedBy?: string;    // UUID
  createdAt: string;     // ISO DateTime
  updatedAt: string;     // ISO DateTime
  _count: { contacts: number; deals: number };
}
```

### Company-Liste: Query-Parameter

| Parameter | Typ | Beschreibung |
|-----------|-----|-------------|
| `page` | number | Seite (default: 1) |
| `pageSize` | number | Eintraege pro Seite (default: 25) |
| `search` | string | Substring-Match in `name`, `industry`, `email`, `city` |
| `industry` | string | Exakter Filter nach Branche |
| `sort` | string | `createdAt`, `updatedAt`, `name`, `industry`, `city` |
| `order` | string | `asc` oder `desc` (default: `desc`) |

### Company-Detail: Verschachtelte Daten

`GET /crm/companies/:id` liefert zusaetzlich:

- `contacts[]` — Top 20 aktive Kontakte mit: `id`, `firstName`, `lastName`, `email`, `phone`, `position`, `isActive`
- `deals[]` — Top 10 Vorgaenge mit: alle Deal-Felder + `pipeline` + `stage` Objekte
- `_count` — Zaehler fuer `contacts` und `deals`

Beispiel-Response (gekuerzt):
```json
{
  "data": {
    "id": "...",
    "name": "Xinion GmbH",
    "industry": "Enterprise Software",
    "contacts": [
      {
        "id": "...",
        "firstName": "Thomas",
        "lastName": "Reitz",
        "email": "treitz@xinion.de",
        "position": "Geschaeftsfuehrer",
        "isActive": true
      }
    ],
    "deals": [
      {
        "id": "...",
        "title": "INSIGHT Platform Lizenz",
        "value": "48000",
        "status": "OPEN",
        "pipeline": { "id": "...", "name": "Standard Sales" },
        "stage": { "id": "...", "name": "Qualifizierung", "color": "#3B82F6" }
      }
    ],
    "_count": { "contacts": 1, "deals": 1 }
  }
}
```

### Aenderungen an Contact

Kontakte haben zwei neue Felder:

| Feld | Typ | Beschreibung |
|------|-----|-------------|
| `companyId` | string? (UUID) | Verknuepfung zum Unternehmen (optional) |
| `position` | string? | Position/Rolle im Unternehmen (max 200) |

**Contact-Liste** liefert jetzt zusaetzlich:
```json
{
  "company": { "id": "...", "name": "Xinion GmbH", "industry": "Enterprise Software" }
}
```

**Contact-Detail** liefert:
```json
{
  "company": { "id": "...", "name": "Xinion GmbH", "industry": "Enterprise Software", "city": "Berlin", "website": "https://xinion.de" }
}
```

### Aenderungen an Deal (Vorgang)

Vorgaenge haben ein neues Feld:

| Feld | Typ | Beschreibung |
|------|-----|-------------|
| `companyId` | string? (UUID) | Verknuepfung zum Unternehmen (optional) |

**Deal-Liste und Detail** liefern jetzt zusaetzlich:
```json
{
  "company": { "id": "...", "name": "Xinion GmbH" }
}
```

**Deal-Liste Filter**: Neuer Query-Parameter `companyId` (UUID) zum Filtern nach Unternehmen.

### Vorschlaege fuer das Frontend

1. **Neue Seiten/Routen**:
   - `/crm/companies` — Unternehmensliste (wie Kontakte, mit Suche/Filter/Paginierung)
   - `/crm/companies/:id` — Unternehmensdetail (2-Spalten: Info links, Kontakte+Vorgaenge rechts)

2. **Sidebar**: Neuer NavLink "Unternehmen" in der CRM-Sektion (zwischen Kontakte und Vorgaenge oder davor)

3. **Contact-Formular**: `companyId` Dropdown (Unternehmen-Suche) + `position` Textfeld hinzufuegen

4. **Contact-Liste**: Company-Name als Spalte anzeigen (kommt aus `contact.company.name`)

5. **Deal-Formular**: `companyId` Dropdown (Unternehmen-Suche) hinzufuegen

6. **Deal-Liste**: Company-Name als Spalte anzeigen

7. **Verlinkung**: Company-Name in Contact- und Deal-Listen als Link zu `/crm/companies/:id`

### Swagger-Aenderung

Der Swagger-Tag fuer Deals/Vorgaenge ist jetzt `Vorgaenge (Deals)` statt `Deals`.

### Datenbank-Verhalten bei Loeschung

- **Company loeschen**: Kontakte und Vorgaenge behalten ihre Daten, aber `companyId` wird auf `null` gesetzt (SetNull)
- **Pipeline loeschen**: Alle verknuepften Vorgaenge werden geloescht (Cascade)
- **Kontakt loeschen**: Vorgaenge behalten ihre Daten, `contactId` wird `null` (SetNull)

### Deployment-Info

- Branch: `feature/crm-service`, Commit: `56a9ed9`
- Prisma Migration `20260310183117_add_companies` angewendet
- Alle Endpoints getestet und funktionsfaehig
- Swagger-Docs aktualisiert: http://172.20.10.59/api/v1/crm/docs/

---

## 2026-03-10 | Frontend: Company-Modul implementiert

### Was wurde umgesetzt

Das komplette Company-Frontend-Modul ist implementiert und deployed (Commit `36f571f`).

#### Neue Dateien (5 Dateien)

```
packages/frontend/src/crm/companies/
  CompaniesPage.tsx + .module.css      -- Liste mit Suche, Paginierung, CRUD-Modals
  CompanyFormModal.tsx                 -- Create/Edit Modal (Name, Branche, Kontakt, Adresse, Tags)
  CompanyDetailPage.tsx + .module.css  -- 2-Spalten: Info links, Kontakte+Vorgaenge rechts
```

#### Geaenderte Dateien (11 Dateien)

- **types.ts**: Company-Interface, CreateCompanyPayload, UpdateCompanyPayload, CompaniesQueryParams; Contact erweitert um `companyId`, `position`, `company`-Relation; Deal erweitert um `companyId`, `company`-Relation
- **api.ts**: `companiesApi` mit 5 CRUD-Methoden
- **hooks.ts**: `crmKeys.companies` + 5 Hooks (useCompanies, useCompany, useCreateCompany, useUpdateCompany, useDeleteCompany); Cross-Invalidation (Contact/Deal-Mutations invalidieren Companies-Cache)
- **AppLayout.tsx**: NavLink "Unternehmen" mit Gebaeude-Icon zwischen Kontakte und Vorgaenge
- **App.tsx**: 2 Routen (`/crm/companies`, `/crm/companies/:id`)
- **ContactsPage.tsx**: Neue Spalte "Unternehmen" mit Link zu Company
- **ContactFormModal.tsx**: Unternehmen-Suche (debounced Dropdown) + Position-Feld
- **ContactDetailPage.tsx**: Unternehmen-Link + Position in Info-Card
- **DealsPage.tsx**: Neue Spalte "Unternehmen" mit Link zu Company
- **DealFormModal.tsx**: Unternehmen-Suche (debounced Dropdown)
- **DealDetailPage.tsx**: Unternehmen-Link in Info-Card

### Funktionsumfang Company-Modul

1. **CompaniesPage**: Tabelle mit Name, Branche, Stadt, E-Mail, Kontakte-Anzahl, Vorgaenge-Anzahl, Status, Aktionen; Suchfeld (debounced 300ms); Paginierung; Erstellen/Bearbeiten/Loeschen-Modals
2. **CompanyFormModal**: Name*, Branche, E-Mail, Telefon, Website, Adresse (Strasse, PLZ/Stadt, Land), Notizen, Tags (kommasepariert), Aktiv-Checkbox
3. **CompanyDetailPage**: Links Info-Card (alle Felder + Tags + Notizen), Rechts Kontakte-Tabelle + Vorgaenge-Tabelle mit Navigation zu Detail-Seiten

### Company-Integration in bestehende Module

- **Kontakte**: Unternehmen-Spalte in Liste, Dropdown-Suche im Formular, Link+Position im Detail
- **Vorgaenge**: Unternehmen-Spalte in Liste, Dropdown-Suche im Formular, Link im Detail
- Pattern: Identisch zur Kontakt-Suche in DealFormModal (debounced, dropdown, click-outside)

### Deployment

- Branch: `feature/crm-service`, Commit: `36f571f`
- TypeScript-Check + Build: erfolgreich
- Frontend Container neu gebaut und deployed

---

## 2026-03-10 | Backend: Lexware Office Integration

### Ueberblick

Der CRM-Service ist jetzt mit Lexware Office (Buchhaltung/ERP) integriert. Drei Hauptfunktionen:

1. **Kontakt-Verknuepfung**: Lexware-Kontakte suchen und mit CRM Companies/Contacts verknuepfen oder importieren
2. **Beleg-Anzeige**: Angebote, Auftragsbestaetigungen, Rechnungen und Gutschriften aus Lexware — anzeigbar am Unternehmen, Kontakt UND am Vorgang
3. **ERP-Push**: CRM-Entitaeten mit Tag "ERP" werden automatisch nach Lexware synchronisiert

### Neue API-Endpoints: Lexware Kontakte

| Methode | Pfad | Beschreibung |
|---------|------|-------------|
| GET | `/crm/lexware/contacts/search?name=&email=` | Lexware-Kontakte suchen (Proxy zur Lexware API) |
| POST | `/crm/lexware/contacts/link-company` | Lexware-Kontakt mit CRM Company verknuepfen |
| POST | `/crm/lexware/contacts/link-contact` | Lexware-Kontakt mit CRM Contact verknuepfen |
| DELETE | `/crm/lexware/contacts/unlink-company/:companyId` | Verknuepfung Company <-> Lexware loesen |
| DELETE | `/crm/lexware/contacts/unlink-contact/:contactId` | Verknuepfung Contact <-> Lexware loesen |
| POST | `/crm/lexware/contacts/import-company` | Neue CRM Company aus Lexware-Daten erstellen |
| POST | `/crm/lexware/contacts/import-contact` | Neuen CRM Contact aus Lexware-Daten erstellen |
| POST | `/crm/lexware/contacts/push/:entityType/:entityId` | CRM-Entitaet nach Lexware pushen (company/contact) |
| POST | `/crm/lexware/contacts/sync/:entityType/:entityId` | Lexware-Daten in CRM aktualisieren |

### Neue API-Endpoints: Lexware Belege (Vouchers)

| Methode | Pfad | Beschreibung |
|---------|------|-------------|
| GET | `/crm/lexware/vouchers/company/:companyId` | Belege fuer Unternehmen (gecacht) |
| GET | `/crm/lexware/vouchers/contact/:contactId` | Belege fuer Kontakt (gecacht) |
| GET | `/crm/lexware/vouchers/deal/:dealId` | Belege fuer Vorgang (via DealVoucher) |
| POST | `/crm/lexware/vouchers/deal/:dealId/link` | Beleg mit Vorgang verknuepfen |
| DELETE | `/crm/lexware/vouchers/deal/:dealId/unlink/:voucherId` | Beleg-Verknuepfung loesen |
| POST | `/crm/lexware/vouchers/refresh/company/:companyId` | Beleg-Cache manuell aktualisieren |
| POST | `/crm/lexware/vouchers/refresh/contact/:contactId` | Beleg-Cache manuell aktualisieren |

### Beleg-Filter Query-Parameter

| Parameter | Typ | Beschreibung |
|-----------|-----|-------------|
| `voucherType` | string | `QUOTATION`, `ORDER_CONFIRMATION`, `INVOICE`, `CREDIT_NOTE` |
| `voucherStatus` | string | Freitext-Filter nach Beleg-Status |
| `page` | number | Seite (default: 1) |
| `pageSize` | number | Eintraege pro Seite (default: 20) |

### LexwareVoucher-Objekt

```typescript
interface LexwareVoucher {
  id: string;                // CRM-interne UUID
  voucherType: 'QUOTATION' | 'ORDER_CONFIRMATION' | 'INVOICE' | 'CREDIT_NOTE';
  voucherNumber?: string;    // z.B. "RE-2025-001"
  voucherDate?: string;      // ISO DateTime
  voucherStatus?: string;    // z.B. "open", "paid", "overdue"
  totalGrossAmount?: string; // Decimal als String, z.B. "1190.00"
  totalNetAmount?: string;
  totalTaxAmount?: string;
  currency: string;          // Default "EUR"
  title?: string;
  lineItemsCount?: number;
  lineItemsJson?: string;    // JSON-String mit Positionen
  lexwareDeepLink?: string;  // Link direkt zu Lexware Office
  fetchedAt: string;         // Wann zuletzt aus Lexware geholt
}
```

### Aenderungen an bestehenden Responses

**Company-Objekt** hat jetzt zusaetzliche Felder:
```json
{
  "lexwareContactId": "abc-123",       // null wenn nicht verknuepft
  "lexwareContactVersion": 3,          // Optimistic Locking
  "lexwareSyncedAt": "2026-03-10...",  // Letzter Sync
  "_count": { "contacts": 5, "deals": 2, "lexwareVouchers": 12 }
}
```

**Contact-Objekt**: Identische neue Felder wie Company.

**Deal-Detail** liefert jetzt zusaetzlich `dealVouchers[]`:
```json
{
  "dealVouchers": [
    {
      "id": "...",
      "linkedAt": "2026-03-10...",
      "voucher": {
        "id": "...",
        "voucherType": "INVOICE",
        "voucherNumber": "RE-2025-001",
        "voucherDate": "2025-12-15...",
        "voucherStatus": "paid",
        "totalGrossAmount": "1190.00",
        "currency": "EUR",
        "title": "Lizenzgebuehr Q4",
        "lexwareDeepLink": "https://app.lexware.de/permalink/..."
      }
    }
  ]
}
```

### Vorschlaege fuer das Frontend

1. **Company/Contact-Detail**: Tab oder Sektion "Lexware" mit:
   - Status-Badge: "Verknuepft" (gruen) / "Nicht verknuepft" (grau)
   - Button "Lexware-Kontakt suchen & verknuepfen" → Modal mit Suchfeld
   - Button "Verknuepfung loesen"
   - Button "Belege aktualisieren" (Refresh-Icon)
   - Beleg-Tabelle: Typ, Nummer, Datum, Status, Brutto-Betrag, Link zu Lexware

2. **Deal-Detail**: Sektion "Belege" mit:
   - Verknuepfte Belege als Tabelle (aus `dealVouchers`)
   - Button "Beleg verknuepfen" → Dropdown/Modal mit verfuegbaren Belegen des Unternehmens/Kontakts
   - Jeder Beleg hat einen externen Link zu Lexware Office

3. **Tags-Integration**: "ERP"-Tag in Company/Contact-Formularen hervorheben (z.B. besondere Farbe), da es den automatischen Push nach Lexware aktiviert

4. **VoucherType Labels** fuer die UI:
   - `QUOTATION` → "Angebot"
   - `ORDER_CONFIRMATION` → "Auftragsbestaetigung"
   - `INVOICE` → "Rechnung"
   - `CREDIT_NOTE` → "Gutschrift"

### Cron-Jobs (automatisch im Hintergrund)

- **Beleg-Sync**: Alle 4 Stunden werden Belege fuer alle verknuepften Entitaeten aus Lexware geholt
- **ERP-Push**: Alle 30 Minuten werden Companies/Contacts mit "ERP"-Tag nach Lexware gepusht

### Health Check

`GET /health` zeigt jetzt `"lexware": "up"|"down"|"unconfigured"`:
- `up`: Lexware API erreichbar
- `down`: API-Key konfiguriert aber API nicht erreichbar
- `unconfigured`: Kein API-Key gesetzt (kein Fehler, Modul einfach deaktiviert)

### Deployment-Hinweise

- Neue Env-Variable auf Server: `LEXWARE_API_KEY` (in `.env`)
- DB-Migration noetig: `migration.sql` in `prisma/migrations/20260310_add_lexware_integration/`
- Neue Tabellen: `lexware_vouchers`, `deal_vouchers`
- Neue Felder: `lexware_contact_id`, `lexware_contact_version`, `lexware_synced_at` auf `companies` und `contacts`

---

## 2026-03-10 | Frontend: Lexware Office Integration UI implementiert

**Commit:** `2381409` (feature/crm-service)
**Status:** Deployed auf insight-dev-01

### Was wurde gemacht?

Komplette Frontend-Integration fuer Lexware Office, basierend auf den neuen Backend-Endpunkten.

### Neue Dateien

| Datei | Beschreibung |
|-------|--------------|
| `src/crm/lexware/LexwareSection.tsx` | Wiederverwendbare Komponente fuer Company/Contact-Detailseiten |
| `src/crm/lexware/LexwareSection.module.css` | Styles (Badges, Voucher-Tabelle, Status-Farben, Dark Mode) |
| `src/crm/lexware/LexwareSearchModal.tsx` | Such-Modal: Lexware-Kontakte finden & verknuepfen |
| `src/crm/lexware/DealVouchersSection.tsx` | Belege-Sektion auf Deal-Detailseite mit Link/Unlink |

### Geaenderte Dateien

| Datei | Aenderung |
|-------|-----------|
| `src/crm/types.ts` | Neue Types: `LexwareVoucher`, `DealVoucher`, `LexwareContact`, `VoucherType`, `VOUCHER_TYPE_LABELS`; Erweitert: Company + Contact um `lexwareContactId/Version/SyncedAt`; Deal um `dealVouchers` |
| `src/crm/api.ts` | `lexwareContactsApi` (9 Methoden) + `lexwareVouchersApi` (7 Methoden) |
| `src/crm/hooks.ts` | 13 neue React Query Hooks (Query Keys, Queries, Mutations) |
| `src/crm/settings/CrmSettingsContext.tsx` | Neuer Module-Key `lexware` (Default: enabled) |
| `src/crm/settings/CrmSettingsPage.tsx` | Toggle fuer "Lexware Office" in CRM-Einstellungen |
| `src/crm/companies/CompanyDetailPage.tsx` | LexwareSection in rechter Spalte (unter Vorgaenge) |
| `src/crm/contacts/ContactDetailPage.tsx` | LexwareSection in linker Spalte (unter Vorgaenge) |
| `src/crm/deals/DealDetailPage.tsx` | DealVouchersSection nach Info-Card |

### Features

1. **Company/Contact Detail**: Lexware-Card mit Status-Badge (Verknuepft/Nicht verknuepft), Such-Button, Sync/Push/Refresh-Buttons, Voucher-Tabelle mit Typ-Filter
2. **Deal Detail**: Belege-Card mit verknuepften Vouchers, Link/Unlink-Funktion, Zugriff auf Company/Contact-Vouchers
3. **Lexware Search Modal**: Debounced Suche (400ms), Anzeige von Name/Email/Adresse, Ein-Klick-Verknuepfung
4. **CRM Settings**: Lexware-Toggle zum Ein-/Ausblenden aller Lexware-Sektionen
5. **Voucher-Tabelle**: Typ-Badges (farbig pro VoucherType), Status-Highlighting, Waehrungs-Formatierung, Deep-Link zu Lexware

### Hinweise fuer Backend

- Alle 16 Endpunkte sind im Frontend verdrahtet
- Bei `lexwareContactId === null` zeigt die UI den "Suchen & Verknuepfen"-Button
- Vouchers werden erst geladen wenn Entity verknuepft ist
- `VOUCHER_TYPE_LABELS`: QUOTATION=Angebot, ORDER_CONFIRMATION=Auftragsbestaetigung, INVOICE=Rechnung, CREDIT_NOTE=Gutschrift
- Die CRM-Einstellung "Lexware Office" kann die gesamte Integration per Toggle ausblenden

---

## 2026-03-11 | Backend: Company Detail Overhaul — Neue Entitaeten und Endpoints

### Ueberblick

Die Company-Entity wurde massiv erweitert: Branchen, Kontotypen und Beziehungstypen sind jetzt als admin-konfigurierbare Entitaeten implementiert (statt Freitext). Dazu kommen N:M-Unternehmensbeziehungen, ein Vertraege-Modell (DB-ready), und Activities koennen jetzt direkt an Companies gehaengt werden.

### Neue Prisma-Models

| Model | Tabelle | Beschreibung |
|-------|---------|-------------|
| `Industry` | `industries` | Admin-konfigurierbare Branchen mit Farbe (unique pro Tenant) |
| `AccountType` | `account_types` | Admin-konfigurierbare Kontotypen (unique pro Tenant) |
| `RelationshipType` | `relationship_types` | Admin-konfigurierbare Beziehungstypen (unique pro Tenant) |
| `CompanyRelationship` | `company_relationships` | N:M Company-zu-Company Beziehungen mit Typ und Notizen |
| `Contract` | `contracts` | Vertraege (DB-Modell vorhanden, UI-Platzhalter) |

### Aenderungen an Company

Neue Felder auf dem Company-Objekt:

```typescript
{
  industryId?: string;        // UUID -> Industry
  accountTypeId?: string;     // UUID -> AccountType
  ownerId?: string;           // UUID (Referenz auf core User, kein FK)
  ownerName?: string;         // Denormalisiert, z.B. "Thomas Reitz"
  industryRef?: {             // Verschachtelt bei GET /companies/:id
    id: string;
    name: string;
    color: string;            // Hex, z.B. "#3B82F6"
  };
  accountType?: {             // Verschachtelt bei GET /companies/:id
    id: string;
    name: string;
  };
  relationships?: CompanyRelationship[];  // Bei GET /companies/:id
  contracts?: Contract[];                  // Bei GET /companies/:id
}
```

**Wichtig**: Das alte `industry`-Freitext-Feld bleibt vorlaeufig bestehen. Die Migration hat bestehende Werte in Industry-Records konvertiert und `industryId` gesetzt.

### Aenderungen an Activity

`contactId` ist jetzt **optional** (war vorher required). Neues Feld `companyId` (optional). Mindestens eines von beiden muss gesetzt sein.

```typescript
{
  contactId?: string;   // UUID, optional
  companyId?: string;   // UUID, NEU, optional
  company?: { id: string; name: string };  // Verschachtelt
}
```

**Neuer Query-Parameter fuer aggregierten Feed:**
```
GET /crm/activities?companyId=X&includeContacts=true
```
Liefert alle Aktivitaeten die direkt an der Company haengen PLUS alle Aktivitaeten der verknuepften Kontakte. Kontakt-Aktivitaeten haben `contact`-Objekt im Response.

### Neue API-Endpoints: Industries (Branchen)

| Methode | Pfad | Beschreibung |
|---------|------|-------------|
| GET | `/crm/industries` | Liste aller Branchen (sortiert nach sortOrder) |
| POST | `/crm/industries` | Branche erstellen (`name`*, `color?`, `sortOrder?`) |
| PATCH | `/crm/industries/:id` | Branche bearbeiten |
| DELETE | `/crm/industries/:id` | Branche loeschen (Schutz bei Referenzen) |

### Neue API-Endpoints: AccountTypes (Kontotypen)

| Methode | Pfad | Beschreibung |
|---------|------|-------------|
| GET | `/crm/account-types` | Liste aller Kontotypen |
| POST | `/crm/account-types` | Kontotyp erstellen (`name`*, `sortOrder?`) |
| PATCH | `/crm/account-types/:id` | Kontotyp bearbeiten |
| DELETE | `/crm/account-types/:id` | Kontotyp loeschen |

### Neue API-Endpoints: RelationshipTypes (Beziehungstypen)

| Methode | Pfad | Beschreibung |
|---------|------|-------------|
| GET | `/crm/relationship-types` | Liste aller Beziehungstypen |
| POST | `/crm/relationship-types` | Beziehungstyp erstellen (`name`*, `sortOrder?`) |
| PATCH | `/crm/relationship-types/:id` | Beziehungstyp bearbeiten |
| DELETE | `/crm/relationship-types/:id` | Beziehungstyp loeschen |

### Neue API-Endpoints: Company Relationships (Unternehmensbeziehungen)

| Methode | Pfad | Beschreibung |
|---------|------|-------------|
| GET | `/crm/companies/:id/relationships` | Beziehungen eines Unternehmens (bidirektional) |
| POST | `/crm/companies/:id/relationships` | Beziehung erstellen |
| DELETE | `/crm/companies/:id/relationships/:relId` | Beziehung loeschen |

**POST-Body:**
```json
{
  "relatedCompanyId": "uuid",
  "relationshipTypeId": "uuid",
  "notes": "optional"
}
```

**GET-Response (einzelne Beziehung):**
```json
{
  "id": "uuid",
  "relatedCompany": { "id": "uuid", "name": "Firma XY" },
  "relationshipType": { "id": "uuid", "name": "Endkunde" },
  "direction": "outgoing",
  "notes": "..."
}
```
`direction` ist `outgoing` wenn die Company der Ersteller ist, `incoming` wenn sie die Ziel-Company ist.

### Neuer Endpoint: Tenant-User (fuer Owner-Dropdown)

| Methode | Pfad | Beschreibung |
|---------|------|-------------|
| GET | `/crm/users` | Liste der Tenant-Benutzer |

**Response:**
```json
{
  "data": [
    { "id": "uuid", "firstName": "Thomas", "lastName": "Reitz", "email": "treitz@xinion.de" }
  ]
}
```

### Seed-Daten (Standard-Konfiguration)

**Industries (8):** IT & Software (#3B82F6), Produktion (#F59E0B), Handel (#10B981), Dienstleistung (#8B5CF6), Gesundheit (#EF4444), Finanzen (#6366F1), Bildung (#EC4899), Oeffentlicher Sektor (#6B7280)

**AccountTypes (4):** Interessent, Endkunde, Personaldienstleister, Partner

**RelationshipTypes (4):** Endkunde, Abrechnungspartner, Muttergesellschaft, Tochtergesellschaft

### Deployment-Info

- Branch: `feature/crm-service`
- Prisma Migration: `20260311_add_company_detail_overhaul`
- Neue Module in app.module.ts: IndustriesModule, AccountTypesModule, RelationshipTypesModule, CompanyRelationshipsModule
- Seed-Data muss nach Migration ausgefuehrt werden

---

## 2026-03-11 | Frontend: Company Detail Page Overhaul — 3-Spalten-Layout

### Was wurde umgesetzt

Kompletter Umbau der CompanyDetailPage von 2-Spalten auf 3-Spalten-Layout. Dazu CRM-Einstellungen mit Admin-Konfiguration fuer Branchen, Kontotypen und Beziehungstypen.

### Neue Dateien

```
packages/frontend/src/crm/companies/
  ActivityFeed.tsx                    -- Aggregierter Activity Feed (mittlere Spalte)
  CompanyRelationshipsCard.tsx        -- N:M Unternehmensbeziehungen Card
  ContractsCard.tsx                   -- Platzhalter "Modul in Entwicklung"
```

### Geaenderte Dateien

| Datei | Aenderung |
|-------|-----------|
| `types.ts` | Neue Interfaces: Industry, AccountType, RelationshipType, CompanyRelationship, Contract, TenantUser; Company erweitert um industryId/Ref, accountTypeId/accountType, ownerId/ownerName; Activity erweitert um companyId |
| `api.ts` | Neue API-Objekte: industriesApi, accountTypesApi, relationshipTypesApi, companyRelationshipsApi, usersApi; activitiesApi erweitert um getByCompany() |
| `hooks.ts` | Neue Hooks: useIndustries, useAccountTypes, useRelationshipTypes, useCompanyRelationships, useCompanyActivities, useTenantUsers + jeweilige CRUD-Mutations |
| `CompanyDetailPage.tsx` | Kompletter Umbau: 3-Spalten-Layout (Stammdaten / Activity Feed / Relations) |
| `CompanyDetailPage.module.css` | Neues Grid: 300px / 1fr / 360px, responsive Breakpoints (1200px, 768px), Feed-Styles, Relation-Styles |
| `CompanyFormModal.tsx` | Dropdowns statt Freitext: Branche (useIndustries), Kontotyp (useAccountTypes), Zustaendigkeit (useTenantUsers) |
| `ActivityFormModal.tsx` | contactId jetzt optional, neues Prop companyId |
| `DealsPage.tsx` | Spacing Fix: minWidth fuer Stage (120px) und Wert (100px) Spalten |
| `settings/CrmSettingsPage.tsx` | 3 neue Config-Sektionen: Branchen (mit Color-Picker), Kontotypen, Beziehungstypen |
| `settings/CrmSettingsPage.module.css` | Styles fuer Config-Tabellen, Inline-Edit, Sort-Buttons |

### CompanyDetailPage — 3-Spalten-Layout

```
+------------------+------------------------+--------------------+
| Linke Spalte     | Mittlere Spalte        | Rechte Spalte      |
| (300px)          | (flex)                 | (360px)            |
+------------------+------------------------+--------------------+
| Stammdaten:      | Activity Feed:         | Kontakte (Tabelle) |
| - Name           | - Inline-Notiz-Form    | Vorgaenge (Tabelle)|
| - Branche Badge  | - Tabs: Notiz/Email/   | Beziehungen Card   |
|   (farbig)       |   Aufgabe              | Vertraege          |
| - Kontotyp       | - Chronologische       |   (Platzhalter)    |
| - Zustaendigkeit |   Liste aller          | Lexware Belege     |
| - E-Mail, Tel    |   Aktivitaeten         |                    |
| - Website        | - "via [Kontakt]"      |                    |
| - Adresse        |   Badge fuer           |                    |
| - Tags           |   Kontakt-Aktivitaeten |                    |
| - Notizen        |                        |                    |
+------------------+------------------------+--------------------+
```

**Responsive:**
- Ab 1200px: 2 Spalten (Links+Mitte gestapelt | Rechts)
- Ab 768px: 1 Spalte (alles gestapelt)

### Activity Feed Details

- Nutzt `GET /crm/activities?companyId=X&includeContacts=true`
- Inline-Formular oben: Betreff + Beschreibung + "Notiz speichern" Button
- Tabs: "Notiz" (aktiv), "E-Mail" (disabled, Platzhalter), "Aufgabe" (disabled, Platzhalter)
- Jeder Eintrag: Typ-Icon (SVG), Betreff, Ersteller, Zeitpunkt
- Kontakt-Aktivitaeten zeigen "via [Kontaktname]" Badge

### CRM-Einstellungen — Admin-Konfiguration

Drei neue Sektionen in `/crm/settings`:

1. **Branchen**: CRUD-Tabelle mit Name, Farb-Badge + Color-Picker, Sortier-Pfeile
2. **Kontotypen**: CRUD-Tabelle mit Name, Sortier-Pfeile
3. **Beziehungstypen**: CRUD-Tabelle mit Name, Sortier-Pfeile

Alle mit Inline-Add (Eingabezeile oben), Inline-Edit, Delete mit Bestaetigung.

### CompanyFormModal — Dropdown-Aenderungen

| Feld | Vorher | Nachher |
|------|--------|---------|
| Branche | Freitext-Input | Select-Dropdown aus `GET /crm/industries` |
| Kontotyp | — (neu) | Select-Dropdown aus `GET /crm/account-types` |
| Zustaendigkeit | — (neu) | Select-Dropdown aus `GET /crm/users` |

`ownerName` wird beim Submit aus der User-Liste aufgeloest und im Payload mitgesendet.

### Genutzte neue Backend-Endpoints

| Hook | Endpoint | Verwendung |
|------|----------|-----------|
| `useIndustries()` | `GET /crm/industries` | CompanyFormModal Dropdown, CRM Settings |
| `useAccountTypes()` | `GET /crm/account-types` | CompanyFormModal Dropdown, CRM Settings |
| `useRelationshipTypes()` | `GET /crm/relationship-types` | AddRelationshipModal, CRM Settings |
| `useCompanyRelationships(id)` | `GET /crm/companies/:id/relationships` | CompanyRelationshipsCard |
| `useCompanyActivities(id)` | `GET /crm/activities?companyId=X&includeContacts=true` | ActivityFeed |
| `useTenantUsers()` | `GET /crm/users` | CompanyFormModal Owner-Dropdown |
| `useCreateIndustry()` | `POST /crm/industries` | CRM Settings |
| `useUpdateIndustry()` | `PATCH /crm/industries/:id` | CRM Settings |
| `useDeleteIndustry()` | `DELETE /crm/industries/:id` | CRM Settings |
| (analog fuer AccountTypes + RelationshipTypes) | | |
| `useCreateCompanyRelationship()` | `POST /crm/companies/:id/relationships` | CompanyRelationshipsCard |
| `useDeleteCompanyRelationship()` | `DELETE /crm/companies/:id/relationships/:relId` | CompanyRelationshipsCard |

### Offene Punkte

- [ ] **Migration auf Server anwenden**: `20260311_add_company_detail_overhaul` + Seed-Data
- [ ] **Container neu bauen und deployen** (Frontend + Backend)
- [ ] **Kanban-Board fuer Vorgaenge** — Feature fuer spaeter geplant
- [ ] **Vertraege-UI implementieren** — DB-Modell vorhanden, UI ist noch Platzhalter
- [ ] **Activity Feed E-Mail/Aufgabe Tabs** — Derzeit Platzhalter (disabled)

### TypeScript-Status

- Frontend: `npx tsc --noEmit` — 0 Fehler
- Backend: `npx tsc --noEmit` — 0 Fehler (nach `prisma generate`)

---

## 2026-03-11 | Frontend: Server-Deployment & Bugfixes

### White-Screen-Fix

Die GUI zeigte nach dem letzten Deploy ein weisses Bild. Ursache: Die `hooks.ts` mit den neuen Hooks fuer Industries, AccountTypes und RelationshipTypes war nur lokal geaendert, aber nie committed worden. Die `CrmSettingsPage.tsx` importierte Exports die auf dem Server nicht existierten → esbuild Build-Fehler → leere Seite.

**Fix:** Alle uncommitteten CRM-Dateien (41 Dateien, Backend + Frontend) in einem Commit zusammengefasst und deployed.

### Server-Deployment der Company Detail Overhaul

Folgende Schritte wurden auf dem Server (172.20.10.59) ausgefuehrt:

1. **Prisma-Migration `20260310_add_lexware_integration`** — War bereits manuell angewendet (VoucherType Enum existierte), wurde als `applied` markiert via `prisma migrate resolve`
2. **Prisma-Migration `20260311_add_company_detail_overhaul`** — Erfolgreich angewendet. Neue Tabellen: `industries`, `account_types`, `relationship_types`, `company_relationships`, `contracts`. Aenderungen an `companies` und `activities`.
3. **Prisma-Client regeneriert** — `prisma generate` im Container ausgefuehrt
4. **CRM-Container neugestartet** — Alle neuen Controller registriert:
   - `IndustriesController {/api/v1/crm/industries}`
   - `AccountTypesController {/api/v1/crm/account-types}`
   - `RelationshipTypesController {/api/v1/crm/relationship-types}`
   - `CompanyRelationshipsController {/api/v1/crm/companies/:companyId/relationships}`
5. **Seed-Daten geladen** fuer beide Tenants (`3fc0e74d-...` und `11111111-...`):
   - 8 Branchen pro Tenant (+ 1 migrierte je Tenant = 18 total)
   - 4 Kontotypen pro Tenant (8 total)
   - 4 Beziehungstypen pro Tenant (8 total)

### Health-Check nach Deployment

```json
{
  "status": "ok",
  "service": "crm-service",
  "version": "0.2.0",
  "services": { "database": "up", "redis": "up", "lexware": "up" }
}
```

### CRM-Einstellungen — Tabbed Layout

Die CRM-Settings-Seite (`/crm/settings`) wurde von gestapelten Cards auf ein **Tab-Layout** umgestellt:

| Tab | Inhalt |
|-----|--------|
| **Module** | Modul-Toggles (Kontakte, Unternehmen, Vorgaenge, Pipelines, Lexware) |
| **Lexoffice Sync** | Import/Export eingebettet (vorher separate Seite `/crm/lexware-sync`) |
| **Weitere Einstellungen** | Admin-Konfiguration: Branchen, Kontotypen, Beziehungstypen |

**Lexware Import** wurde ebenfalls ueberarbeitet: Statt Suchfeld-only gibt es jetzt eine **browsable paginierte Liste** aller Lexware-Kontakte mit aufklappbaren Ansprechpartnern und individuellen Import-Buttons.

### Commits

| Commit | Beschreibung |
|--------|-------------|
| `5532918` | feat(frontend): redesign Lexware Import with browsable list + Ansprechpartner |
| `4e5c26c` | feat(frontend): add tabbed layout to CRM Settings page |
| `0ed1e77` | feat(crm): add company detail overhaul with industries, account types, relationship types |
| `08b212b` | docs(crm): update INSIGHT-CRM.md with company detail overhaul entries |

### Offene Punkte

- [x] ~~Migration auf Server anwenden~~ — Erledigt (beide Migrationen)
- [x] ~~Container neu bauen und deployen~~ — Frontend + CRM Backend deployed
- [x] ~~Seed-Daten laden~~ — Fuer beide Tenants
- [ ] **Vertraege-UI implementieren** — DB-Modell vorhanden, UI ist Platzhalter (`ContractsCard.tsx`)
- [ ] **Activity Feed E-Mail/Aufgabe Tabs** — Tabs vorhanden aber disabled
- [ ] **Kanban-Board fuer Vorgaenge** — Backend ready, Frontend Feature fuer spaeter

### Hinweis an Backend

- Der `insight-crm` Container laeuft im Dev-Modus mit Volume-Mount. Code-Aenderungen werden automatisch erkannt.
- Nach Schema-Aenderungen muss `prisma generate` im Container ausgefuehrt werden.
- Die `LexwareSyncContent`-Komponente wurde als separater Export aus `LexwareSyncPage.tsx` extrahiert und wird sowohl auf der Standalone-Seite (`/crm/lexware-sync`) als auch eingebettet im Settings-Tab verwendet.

---

## 2026-03-11 | Frontend: Bug-Report — Lexware Import Company schlaegt fehl (500)

### Problem

Beim Klick auf "Unternehmen" (Import als Company) im Lexware Sync Tab kommt ein **500 Internal Server Error**.

### Fehlermeldung (aus `docker logs insight-crm`)

```
PrismaClientValidationError:
Invalid `this.prisma.company.create()` invocation in
/app/src/lexware/lexware-contacts.service.ts:229:32

→ 229 return this.prisma.company.create({
        data: {
          name: "team neusta SE",
          email: "k.sauer@neusta.de",
          phone: undefined,
          street: "Konsul-Smidt-Straße 24",
          zip: "28217",
          city: "Bremen",
          country: "DE",
          notes: undefined,
          lexwareContactId: "e23f5165-9c1e-40ba-9536-9990703421df",
          lexwareContactVersion: 6,
          lexwareSyncedAt: new Date("2026-03-11T09:05:10.318Z"),
          createdBy: "4627b01c-2f23-4ee8-a44e-c04bff068a5f",
      +   tenantId: String    ← FEHLER: Typ "String" statt UUID-Wert
        },
```

### Ursache

In `lexware-contacts.service.ts` Zeile 229 wird `tenantId` als **TypeScript-Typ `String`** uebergeben statt als tatsaechlicher UUID-Wert aus dem JWT-Token. Vermutlich steht dort so etwas wie `tenantId: String` statt `tenantId: user.tenantId` oder `tenantId: this.tenantId`.

### Betroffene Datei

`packages/crm-service/src/lexware/lexware-contacts.service.ts` — Zeile ~229 (`importCompany`-Methode)

### Vermutlich gleicher Fehler bei

- `importContact` (Lexware-Kontakt als CRM Contact importieren)
- Eventuell auch `push` und `sync` Methoden, falls diese ebenfalls `tenantId` setzen

### Reproduktion

1. `/crm/settings` → Tab "Lexoffice Sync" → "Import (Lexware → CRM)"
2. Beliebigen Lexware-Kontakt suchen (z.B. "team")
3. Auf "Unternehmen" Button klicken
4. → Rote Fehlermeldung: "Import fehlgeschlagen: Request failed with status code 500"

---

## 2026-03-11 | Backend: Fix — Lexware Import 500 (fehlende tenantId)

### Ursache

Der `TenantGuard` liess `PLATFORM_ADMIN`-User ohne `tenantId`-Pruefung durch:

```typescript
// ALT (fehlerhaft):
if (user?.role === 'PLATFORM_ADMIN') {
  return true;  // ← Kein tenantId-Check!
}
```

Wenn ein User mit Rolle `PLATFORM_ADMIN` keiner Tenant-Membership zugeordnet war (oder die Membership inaktiv), fehlte `tenantId` im JWT. Der Controller uebergab dann `user.tenantId!` = `undefined` an den Service, was zum Prisma-Validierungsfehler fuehrte.

### Fixes

**1. TenantGuard (`src/auth/guards/tenant.guard.ts`):**
- ALLE User (auch PLATFORM_ADMIN) muessen jetzt eine `tenantId` haben, um auf CRM-Ressourcen zuzugreifen
- Klare Fehlermeldung: "Kein Mandant zugeordnet. Bitte mit einem mandanten-gebundenen Account anmelden."

**2. Defensive Pruefung in Lexware-Service (`src/lexware/lexware-contacts.service.ts`):**
- `importAsCompany()` und `importAsContact()` pruefen zusaetzlich `if (!tenantId)` und werfen `BadRequestException` mit klarer Meldung

### Betroffene Dateien

| Datei | Aenderung |
|-------|-----------|
| `src/auth/guards/tenant.guard.ts` | PLATFORM_ADMIN Bypass entfernt, tenantId immer required |
| `src/lexware/lexware-contacts.service.ts` | Defensive tenantId-Pruefung in Import-Methoden |

### Auswirkung

- PLATFORM_ADMIN ohne Tenant-Zuordnung bekommt jetzt **403 Forbidden** statt **500 Internal Server Error**
- Alle anderen User sind nicht betroffen (hatten vorher schon den tenantId-Check)
- TypeScript-Check: 0 Fehler

---

## 2026-03-12 | Architekt: Neue Anforderungen aus Konzeptdokument v1.0 + Briefing

Der Architekt hat das Konzeptdokument (INSIGHT_Konzept_v1.0.docx) und das Claude Briefing (CLAUDE_BRIEFING.docx) aktualisiert. Folgende Kapitel sind neu oder erweitert und betreffen den CRM-Service direkt:

- **Kapitel 22** — CRM-Modul (vorher nur Platzhalter, jetzt vollstaendig spezifiziert)
- **Kapitel 24** — Office 365 Integration (neu, CRM-relevante Teile)
- **Kapitel 14 im Briefing** — Office 365 Kurzreferenz (neu)

### Hinweis: CORE vs CRM

Folgende Teile sind **CORE-Aufgaben** (NICHT fuer den CRM-Entwickler):
- Kap 24.1: OAuth-Flow + `user_integrations` Tabelle in `platform_core`
- Kap 24.8: Azure App-Registrierung im Azure Portal
- `.env`-Variablen: `MS_CLIENT_ID`, `MS_CLIENT_SECRET`, `MS_REDIRECT_URI`, `MS_INTEGRATION_ENCRYPTION_KEY`

Alles Folgende ist **CRM-Arbeit**.

---

### A) CRM-Modul Spezifikation (Kap 22) — Neue/Erweiterte Anforderungen

#### A.1 Kontakttypen & Felder (Kap 22.1)

Die vollstaendige Felddefinition fuer Person und Unternehmen liegt jetzt vor:

**Kontakttyp: Person**

| Feld | Typ | Pflicht | Bemerkung |
|------|-----|---------|-----------|
| Vorname | String | Ja | |
| Nachname | String | Ja | |
| Jobtitel | String | Nein | |
| Unternehmen | Relation -> Unternehmen | Nein | |
| Abteilung | String | Nein | |
| E-Mail | Array (String) | Nein | Mehrere, Typ: Arbeit / Privat / Sonstige |
| Telefon | Array (String) | Nein | Mehrere, Typ: Buero / Mobil / Fax |
| Adresse | Objekt | Nein | Strasse, PLZ, Stadt, Land |
| LinkedIn-URL | String (URL) | Nein | |
| Geburtsdatum | Date | Nein | Optional, kann ausgeblendet werden |
| Quelle | Enum | Nein | Messe, Empfehlung, Website, Kaltakquise, Import, Visitenkarte, Sonstige |
| Tags | Array (String) | Nein | Frei vergebbar, tenant-weit geteilt |
| Status | Enum | Ja | Aktiv / Inaktiv / Gesperrt (Default: Aktiv) |
| Notizen | Text (Markdown) | Nein | |
| Benutzerdefinierte Felder | Dynamisch | Nein | Siehe A.6 |

**Kontakttyp: Unternehmen**

| Feld | Typ | Pflicht | Bemerkung |
|------|-----|---------|-----------|
| Firmenname | String | Ja | |
| Branche | String | Nein | Freitext oder vordefinierte Kategorien |
| Website | String (URL) | Nein | |
| Telefon | Array (String) | Nein | Mehrere Nummern |
| Adresse Hauptsitz | Objekt | Nein | Strasse, PLZ, Stadt, Land |
| Adresse Lieferung | Objekt | Nein | Optional abweichende Lieferadresse |
| USt-IdNr. | String | Nein | |
| Steuernummer | String | Nein | |
| Handelsregisternummer | String | Nein | z.B. HRB 12345 — befuellbar via Datenanreicherung |
| Registergericht | String | Nein | z.B. Amtsgericht Muenchen — befuellbar via Datenanreicherung |
| Unternehmensgroesse | Enum | Nein | 1-10, 11-50, 51-200, 201-500, 500+ |
| Ansprechpartner | Relation -> Personen | Nein | Verknuepfte Personen |
| Tags | Array (String) | Nein | |
| Status | Enum | Ja | Aktiv / Inaktiv / Gesperrt |
| Notizen | Text (Markdown) | Nein | |
| Datenanreicherung | Automatisch | Nein | data_enriched_at + data_enriched_source |
| Benutzerdefinierte Felder | Dynamisch | Nein | Siehe A.6 |

**Backend-Aufgabe**: Abgleich mit bestehenden Prisma-Modellen. Fehlende Felder (LinkedIn, Geburtsdatum, Quelle, Unternehmensgroesse, USt-IdNr, Steuernummer, Handelsregisternummer, Registergericht, Adresse Lieferung, data_enriched_at/source) muessen ergaenzt werden.

#### A.2 Firmendaten-Anreicherung / Data Enrichment (Kap 22.2) — NEU

"Firmendaten laden"-Button im Unternehmenformular. Stammdaten aus externen Registern abrufen und als Vorschlag in einem Modal anzeigen.

**Quellen:**
1. **Unternehmensregister.de** (kostenlos, oeffentlich) — HR-Nummer, Registergericht, Rechtsform, Sitz, Eintragsdatum
2. **North Data API** (kommerziell, API-Key pro Tenant) — Adresse, Branche, Umsatz, Mitarbeiterzahl, Gesellschafter, Verflechtungen

**Backend-Aufgaben:**
- [ ] `POST /crm/companies/:id/enrich` oder `POST /crm/data-enrichment/company` Endpoint
- [ ] Paralleler Abruf: Unternehmensregister.de + North Data API (Timeout je 10 Sek.)
- [ ] Ergebnisse normalisieren und zusammenfuehren
- [ ] `data_enriched_at` + `data_enriched_source` in Company speichern
- [ ] Admin-Einstellung: North Data API-Key pro Tenant (Admin > CRM > Integrationen)

**Frontend-Aufgaben:**
- [ ] "Firmendaten laden" Button im Unternehmenformular
- [ ] Anreicherungs-Modal: Linke Spalte = aktueller Wert, Rechte Spalte = Vorschlag (Quelle), Checkbox pro Feld
- [ ] "Auswahl uebernehmen" -> Felder im Formular setzen (Speichern erst bei explizitem Speichern)
- [ ] CRM-Settings-Seite: North Data API-Key Konfiguration

#### A.3 Zustaendigkeit / Account Owner (Kap 22.3) — NEU

Jeder Kontakt/Unternehmen kann mit mehreren internen Mitarbeitern verknuepft werden (m:n).

| Aspekt | Entscheidung |
|--------|-------------|
| Modell | m:n — ein Kontakt hat mehrere Owner, ein Mitarbeiter hat mehrere Kontakte |
| Rollen pro Zuweisung | `OWNER`, `MEMBER`, `WATCHER` (unterschiedliche Bearbeitungsrechte) |
| Mitarbeiter-Referenz | `user_id` aus `platform_core` (kein Kopieren von User-Daten) |
| Anzeige | Avatare der zugewiesenen Mitarbeiter in der Kontaktkarte |
| Pflicht-Owner | Mindestens 1 Owner pro Kontakt bei Erstellung (Default: erstellender User) |

**DB-Tabelle:** `crm_contact_owners (contact_id, user_id, role: OWNER|MEMBER|WATCHER)`

**Backend-Aufgaben:**
- [ ] Contact-Owner CRUD: `POST/DELETE /crm/contacts/:id/owners`
- [ ] Bei Kontakt-Erstellung automatisch erstellenden User als OWNER setzen
- [ ] Owner-Info in Contact-Detail-Response mitliefern

**Frontend-Aufgaben:**
- [ ] Avatare der Owner in der Kontaktkarte anzeigen
- [ ] Owner-Zuweisung UI (User suchen + Rolle waehlen)

#### A.4 Pipeline & Deal-Management — Erweiterte Spec (Kap 22.4)

Ergaenzungen zur bestehenden Implementierung:

- **Forecast-Ansicht**: Aggregierter Dealwert pro Stage gewichtet mit Wahrscheinlichkeit
- **Lost-Grund**: Enum + Freitext bei Status Lost: Preis, Timing, Wettbewerber, Kein Bedarf, Sonstige
- **Pipeline-Sichtbarkeit**: Enum — Alle Tenant-User / Nur zugewiesene Teams
- **Deal-Owner**: m:n analog zu Kontakt-Owner (crm_deal_owners)

**Backend-Aufgaben:**
- [ ] `lost_reason` Enum + `lost_reason_text` Freitext in Deals
- [ ] Deal-Owner CRUD (analog Contact-Owner)
- [ ] Forecast-Endpoint: `GET /crm/deals/forecast` (Wert x Wahrscheinlichkeit pro Stage)

**Frontend-Aufgaben:**
- [ ] Lost-Grund Modal bei Stage-Wechsel zu "Lost"
- [ ] Kanban-Board (Drag & Drop)
- [ ] Forecast-Ansicht

#### A.5 Aktivitaeten & Aufgaben (Kap 22.5) — Erweiterte Spec

6 Aktivitaetstypen sind definiert:

| Typ | Icon | Bemerkung |
|-----|------|-----------|
| Anruf (Call) | Telefon | Kann Gespraechsnotizen enthalten |
| Meeting | Kalender | Datum, Uhrzeit, Ort / Videolink |
| E-Mail | Brief | Freitext-Notiz, KEINE echte E-Mail-Integration in MVP |
| Aufgabe (Task) | Checkbox | Faelligkeitsdatum, Zuweisung an Mitarbeiter |
| Notiz | Stift | Freitext ohne Faelligkeitsdatum |
| Follow-Up | Pfeil | Erinnerung zu einem definierten Zeitpunkt |

**Hinweis**: E-Mail-Integration (Outlook) kommt erst mit Office 365 (Kap 24).

#### A.6 Benutzerdefinierte Felder / Custom Fields (Kap 22.6) — NEU

Pro Kontakttyp (Person, Unternehmen) und pro Deal koennen eigene Felder definiert werden.

**Unterstuetzte Feldtypen:**

| Typ | Beschreibung | Beispiel |
|-----|-------------|---------|
| Text | Einzeiliger Freitext | Kundennummer |
| Textarea | Mehrzeiliger Freitext | Besondere Hinweise |
| Zahl | Integer oder Decimal | Umsatz, Vertragslaufzeit |
| Datum | Datumspicker | Vertragsbeginn |
| Auswahl (Dropdown) | Vordefinierte Werte | Kundensegment: A / B / C |
| Mehrfachauswahl | Mehrere Werte waehlbar | Interessensgebiete |
| Checkbox | Boolean Ja/Nein | DSGVO-Einwilligung |
| URL | Validierter Link | Portal-Link |

**DB-Tabellen:**
```sql
crm_custom_field_defs   (id, entity_type: PERSON|COMPANY|DEAL, name, label, field_type,
                         options jsonb, is_required, position)
crm_custom_field_values (id, field_def_id, entity_id, value_text, value_number,
                         value_date, value_boolean, value_json)
```

**Backend-Aufgaben:**
- [ ] CRUD fuer Field-Definitions: `GET/POST/PATCH/DELETE /crm/custom-fields`
- [ ] Wert-Speicherung bei Entity-Create/Update
- [ ] Custom Fields in Entity-Detail-Response mitliefern

**Frontend-Aufgaben:**
- [ ] Admin-Bereich: Custom Fields Verwaltung (Drag & Drop Reihenfolge, Pflichtfeld-Flag)
- [ ] Dynamische Formular-Felder in Contact/Deal-Formularen
- [ ] Custom Fields als optionale Spalten in Listen-Ansichten
- [ ] Custom Fields als Filter in der Suche

#### A.7 Kontakt-Import (Kap 22.7) — NEU

**Import via Datei:**
- [ ] CSV-Import mit visuellem Spalten-Mapper
- [ ] Excel-Import (.xlsx)
- [ ] vCard-Import (.vcf, einzeln und ZIP)
- [ ] Vorschau der ersten 10 Datensaetze + Validierungsfehler anzeigen
- [ ] Duplikat-Erkennung via E-Mail: ueberspringen / zusammenfuehren / als Duplikat markieren

**Visitenkarten-Scan (Anthropic Vision API):**
- [ ] "Visitenkarte scannen" — Kamera-/Datei-Dialog
- [ ] Bild base64-kodiert an CRM-Backend senden
- [ ] Backend ruft Anthropic Vision API auf (strukturierter Prompt)
- [ ] Vorausgefuelltes Kontaktformular zur Bestaetigung
- [ ] Rate Limit: max. 50 Scans pro Tenant pro Tag (konfigurierbar)
- [ ] Bild wird NICHT dauerhaft gespeichert

#### A.8 Berechtigungsmodell (Kap 22.8) — NEU

Ownership-basiertes Sichtbarkeitsmodell:

| Stufe | Beschreibung | Sieht was? |
|-------|-------------|-----------|
| Eigene | User sieht nur eigene Datensaetze (Owner) | Eigene Kontakte, Deals, Aktivitaeten |
| Team | User sieht alle Datensaetze der eigenen Abteilung | Kontakte aller Kollegen |
| Alle | User sieht alle Datensaetze des Tenants | Vollzugriff (lesend) |

**Berechtigungen nach Rolle:**

| Rolle | Sichtbarkeit | Erstellen | Bearbeiten | Loeschen |
|-------|-------------|-----------|------------|----------|
| tenant_admin | Alle | Ja | Alle | Alle |
| team_lead | Team | Ja | Team + Eigene | Eigene |
| tenant_member | Konfigurierbar | Ja | Eigene + zugewiesene | Eigene |
| tenant_readonly | Konfigurierbar | Nein | Nein | Nein |

**Backend-Aufgaben:**
- [ ] Sichtbarkeitsfilter in allen List-Queries (Contacts, Deals, Activities)
- [ ] Tenant-Admin Einstellung: Default-Sichtbarkeit pro Rolle
- [ ] Per-User Override moeglich

**Frontend-Aufgaben:**
- [ ] Admin > CRM-Einstellungen > Berechtigungen: Sichtbarkeitsstufe konfigurieren

#### A.9 Reporting & Dashboards (Kap 22.9) — NEU

5 Reports/Dashboards sind definiert:

| Dashboard | Inhalt | Aktualisierung |
|-----------|--------|---------------|
| Pipeline-Uebersicht | Deals pro Stage, Gesamtvolumen, gewichteter Forecast | Echtzeit |
| Aktivitaeten-Uebersicht | Offene Aufgaben, ueberfaellige Aktivitaeten, Volumen/Woche | Echtzeit |
| Kontaktwachstum | Neue Kontakte/Monat, aufgeschluesselt nach Quelle | Taeglich |
| Win/Loss-Analyse | Won vs Lost Deals, Lost-Gruende als Torte, Durchschnittliche Deal-Dauer | Taeglich |
| Mitarbeiter-Performance | Deals pro Mitarbeiter, Aktivitaeten-Anzahl, Response-Zeit | Taeglich |

- Alle Reports als CSV exportierbar
- Datumsbereiche: letzte 7 / 30 / 90 Tage, benutzerdefiniert
- Mitarbeiter-Performance nur fuer tenant_admin und team_lead sichtbar

**Backend-Aufgaben:**
- [ ] Reporting-Endpoints: `GET /crm/reports/pipeline`, `/reports/activities`, `/reports/contacts-growth`, `/reports/win-loss`, `/reports/performance`
- [ ] CSV-Export fuer alle Reports

**Frontend-Aufgaben:**
- [ ] CRM Dashboard-Seite mit den 5 Report-Widgets
- [ ] Datumbereich-Selektor
- [ ] CSV-Export Buttons

#### A.10 CRM Datenbankschema (Kap 22.10)

Vollstaendiges Schema laut Architekt:

```sql
-- Kernentitaeten
crm_contacts        (id, type: PERSON|COMPANY, status, source, created_by, created_at, updated_at)
crm_persons         (id, contact_id, first_name, last_name, job_title, department, birthday, notes)
crm_companies       (id, contact_id, name, industry, website, vat_id, tax_id, size_class, notes)
crm_contact_emails  (id, contact_id, email, type: WORK|PERSONAL|OTHER, is_primary)
crm_contact_phones  (id, contact_id, phone, type: OFFICE|MOBILE|FAX, is_primary)
crm_contact_addresses (id, contact_id, type: MAIN|DELIVERY, street, zip, city, country)

-- Beziehungen
crm_person_company  (person_contact_id, company_contact_id)
crm_contact_owners  (contact_id, user_id, role: OWNER|MEMBER|WATCHER)
crm_contact_tags    (contact_id, tag)

-- Pipeline & Deals
crm_pipelines       (id, name, is_default, visibility)
crm_pipeline_stages (id, pipeline_id, name, color, position, probability, is_won_stage, is_lost_stage)
crm_deals           (id, contact_id, pipeline_id, stage_id, title, value, currency,
                     probability_override, expected_close_date, status: OPEN|WON|LOST,
                     lost_reason, lost_reason_text, notes, created_by)
crm_deal_owners     (deal_id, user_id, role: OWNER|MEMBER|WATCHER)

-- Aktivitaeten
crm_activities      (id, type: CALL|MEETING|EMAIL|TASK|NOTE|FOLLOWUP,
                     contact_id nullable, deal_id nullable,
                     title, body, due_date, completed_at, assigned_to_user_id, created_by)

-- Custom Fields
crm_custom_field_defs   (id, entity_type: PERSON|COMPANY|DEAL, name, label, field_type,
                         options jsonb, is_required, position)
crm_custom_field_values (id, field_def_id, entity_id, value_text, value_number,
                         value_date, value_boolean, value_json)
```

**Hinweis**: Das aktuelle Prisma-Schema im CRM-Service weicht teilweise ab (z.B. `app_crm` Schema-Prefix statt `crm_` Tabellen-Prefix, flaches Company/Contact-Modell statt Contact+Person+Company Split). Der Architekt hat das Ziel-Schema definiert — Abgleich und Migration sind Backend-Aufgaben.

#### A.11 CRM Events (Kap 22.11)

| Event | Ausgeloest von | Empfaenger/Zweck |
|-------|---------------|-----------------|
| `crm.contact.created` | CRM-Service | Zukuenftige Module (Marketing etc.) |
| `crm.contact.updated` | CRM-Service | Zukuenftige Module |
| `crm.deal.stage_changed` | CRM-Service | Reporting, Automatisierungen |
| `crm.deal.won` | CRM-Service | Reporting, Modul-Integrationen |
| `crm.deal.lost` | CRM-Service | Reporting |
| `crm.activity.due_soon` | CRM-Service (Scheduler) | Benachrichtigungs-Service |
| `core.user.deactivated` | Core-Service | CRM prueft Owner-Reassignment |

**Backend-Aufgabe:**
- [ ] Redis Pub/Sub Events bei Kontakt/Deal/Activity-Aenderungen publishen

---

### B) Office 365 Integration — CRM-relevante Teile (Kap 24)

Die OAuth-Verbindung (Kap 24.1) und Azure App-Registrierung (Kap 24.8) sind **CORE-Aufgaben**. Der CRM-Service nutzt die bestehende MS-Verbindung des Users fuer folgende Features:

#### B.1 E-Mail Tab im CRM-Kontakt (Kap 24.2) — Read-only

Pro CRM-Kontakt wird ein "E-Mails"-Tab angezeigt mit allen Outlook-Mails von/an die Kontakt-E-Mail-Adresse.

**Graph API Abfrage:**
```
GET /me/messages
  ?$filter=from/emailAddress/address eq '{kontakt_email}'
    or toRecipients/any(...)
  &$select=id,subject,from,toRecipients,receivedDateTime,bodyPreview,isRead
  &$orderby=receivedDateTime desc
  &$top=25
```

| Feature | Detail |
|---------|--------|
| Anzeige | Absender, Betreff, Datum, Vorschautext (max. 255 Zeichen) |
| Sortierung | Neueste zuerst, 25 pro Seite |
| Caching | Redis 5 Min (Key: `user:{id}:mails:contact:{id}`) |
| Kein Volltext | Nur bodyPreview, kein vollstaendiger E-Mail-Body |
| Kein Senden | Read-only |

**Backend-Aufgaben:**
- [ ] `GET /crm/contacts/:id/emails` Endpoint (Proxy zu Graph API)
- [ ] MS Access Token aus Redis / Refresh via Core
- [ ] Redis-Caching (5 Min TTL)

**Frontend-Aufgaben:**
- [ ] "E-Mails" Tab in ContactDetailPage (nur wenn MS-Verbindung aktiv)
- [ ] Ausgegraut wenn keine MS-Verbindung, Hinweis "Microsoft 365 verbinden"

#### B.2 Kalender Tab im CRM-Kontakt (Kap 24.3) — Read-only

Outlook-Termine bei denen der CRM-Kontakt als Teilnehmer eingetragen ist.

**Graph API Abfrage:**
```
GET /me/calendarView
  ?startDateTime={heute}T00:00:00Z
  &endDateTime={heute+90Tage}T23:59:59Z
  &$filter=attendees/any(a: a/emailAddress/address eq '{kontakt_email}')
  &$select=id,subject,start,end,location,attendees,bodyPreview,onlineMeetingUrl
```

| Feature | Detail |
|---------|--------|
| Anzeige | Kommende 90 Tage + vergangene 90 Tage |
| Felder | Titel, Datum/Uhrzeit, Ort, Online-Meeting-Link, Teilnehmer |
| Caching | Redis 5 Min |
| Kein Schreiben | Read-only |

**Backend-Aufgaben:**
- [ ] `GET /crm/contacts/:id/calendar` Endpoint
- [ ] Redis-Caching

**Frontend-Aufgaben:**
- [ ] "Kalender" Tab in ContactDetailPage

#### B.3 Aufgaben Sync — Bidirektional mit Microsoft To Do (Kap 24.4)

CRM-Aufgaben koennen optional nach Microsoft To Do synchronisiert werden.

**Richtung INSIGHT -> To Do:**
- Trigger: Aufgabe erstellt/geaendert im CRM
- Action: `POST /me/todo/lists/{defaultListId}/tasks` (oder PATCH wenn ms_task_id bekannt)
- `ms_task_id` wird in `crm_activities` gespeichert

**Richtung To Do -> INSIGHT:**
- NestJS `@Cron` Polling alle 5 Min fuer User mit aktiver MS-Verbindung
- `GET /me/todo/lists/{listId}/tasks?$filter=lastModifiedDateTime gt {letzter_sync}`
- Status-Aenderungen (erledigt/offen) werden uebernommen
- Redis-Lock pro User verhindert parallele Sync-Jobs
- Nur INSIGHT-erstellte Aufgaben werden synchronisiert (die eine ms_task_id haben)

**DB-Aenderungen an crm_activities:**
```sql
+ ms_task_id      VARCHAR(255)  -- To Do Aufgaben ID
+ ms_task_list_id VARCHAR(255)  -- To Do Listen ID
+ ms_synced_at    TIMESTAMPTZ   -- Letzter erfolgreicher Sync
```

**Backend-Aufgaben:**
- [ ] ms_task_id, ms_task_list_id, ms_synced_at Felder in Activity-Model
- [ ] Graph API Integration: POST/PATCH Tasks
- [ ] @Cron Scheduler (alle 5 Min) fuer To Do -> INSIGHT Sync
- [ ] Redis-Lock pro User
- [ ] Konfliktbehandlung: INSIGHT gewinnt (Last Write Wins mit Timestamp)

#### B.4 Kontakte Export nach Outlook (Kap 24.5) — Manuell

Button "Nach Outlook exportieren" im CRM-Kontakt (Person).

**Ablauf:**
1. User klickt "Nach Outlook exportieren"
2. Pruefung: MS-Verbindung aktiv? -> Wenn nein: Hinweis
3. Existiert Kontakt in Outlook (ms_contact_id)? -> PATCH (Update) / POST (Neu)
4. ms_contact_id wird in crm_persons gespeichert

**Feldzuordnung CRM -> Outlook:**

| CRM Feld | Outlook Contacts (Graph API) |
|----------|------------------------------|
| Vorname + Nachname | givenName + surname |
| Jobtitel | jobTitle |
| Unternehmen | companyName |
| Abteilung | department |
| E-Mails | emailAddresses[] |
| Telefone | businessPhones[] / mobilePhone |
| Adresse | businessAddress |
| LinkedIn-URL | businessHomePage |
| Notizen | personalNotes |

**DB-Aenderung an crm_persons:**
```sql
+ ms_contact_id VARCHAR(255)  -- Outlook Kontakt ID
```

**Backend-Aufgaben:**
- [ ] `POST /crm/contacts/:id/export-to-outlook` Endpoint
- [ ] ms_contact_id Feld in Person-Model
- [ ] Bei 404 (Kontakt in Outlook geloescht): neuen Kontakt erstellen, ms_contact_id aktualisieren

**Frontend-Aufgaben:**
- [ ] "Nach Outlook exportieren" Button in Kontakt-Header (nur fuer Personen)
- [ ] Ausgegraut wenn keine MS-Verbindung

---

### C) Zusammenfassung: Priorisierte Aufgabenliste fuer CRM-Entwickler

**Prio 1 — Kurzfristig (bestehende Features erweitern):**
- [ ] Fehlende Kontakt-/Unternehmens-Felder im Prisma-Schema ergaenzen (LinkedIn, Geburtsdatum, Quelle, USt-IdNr etc.)
- [ ] Lost-Grund (Enum + Freitext) bei Deals
- [ ] Contact/Deal-Owner m:n Modell (crm_contact_owners, crm_deal_owners)
- [ ] Redis Pub/Sub Events bei Entity-Aenderungen

**Prio 2 — Mittelfristig (neue Features):**
- [ ] Custom Fields (Definition + Wert-Speicherung + Admin-UI)
- [ ] Firmendaten-Anreicherung (Unternehmensregister.de + North Data)
- [ ] Kontakt-Import (CSV, Excel, vCard) mit Spalten-Mapper + Duplikat-Erkennung
- [ ] Berechtigungsmodell (Eigene/Team/Alle Sichtbarkeit)
- [ ] Kanban-Board fuer Deals (Drag & Drop)
- [ ] Forecast-Ansicht

**Prio 3 — Spaeter (abhaengig von Core-Vorarbeiten):**
- [ ] Office 365: E-Mail Tab (benoetigt OAuth-Infrastruktur im Core)
- [ ] Office 365: Kalender Tab
- [ ] Office 365: Aufgaben Sync mit Microsoft To Do
- [ ] Office 365: Kontakte Export nach Outlook
- [ ] Visitenkarten-Scan (Anthropic Vision API)
- [ ] CRM Reporting & Dashboards (5 Reports + CSV-Export)

---

---

## 2026-03-12 | Plattform-Admin: Briefing fuer CRM-Backend-Experten

### Kontext

Der Architekt hat das Konzeptdokument v1.0 und das Claude Briefing aktualisiert. Es gibt umfangreiche neue Anforderungen fuer das CRM-Modul (Details: siehe Eintrag oben vom gleichen Datum). Dieses Briefing definiert die **Reihenfolge und Abhaengigkeiten** fuer die CRM-Backend-Entwicklung.

### Aktueller Stand

Das CRM-Backend (`packages/crm-service/`) hat bereits folgende Module produktiv:

| Modul | Status | Endpoints |
|-------|--------|-----------|
| Contacts (Person + Company) | Laeuft | CRUD + Suche + Paginierung |
| Deals | Laeuft | CRUD + Pipeline-Zuordnung |
| Pipelines + Stages | Laeuft | CRUD + Stage-Management |
| Activities | Laeuft | CRUD (6 Typen) |
| Industries | Laeuft | CRUD |
| Companies (Standalone) | Laeuft | CRUD + Lexware-Sync |
| Trade Events (Messe-Timer) | Laeuft | CRUD + Active-Filter |
| CRM Settings | Laeuft | Modulverwaltung pro Tenant |

Das Frontend fuer alle obigen Module ist ebenfalls deployed und funktional.

### Phasenplan: Was wann zu tun ist

#### Phase 1 — SOFORT starten (keine Abhaengigkeiten)

Diese Aufgaben erweitern bestehende Module und koennen sofort umgesetzt werden:

**1.1 Fehlende Felder im Prisma-Schema ergaenzen**
- Person: `linkedinUrl`, `birthday`, `source` (Enum), `status` (Enum)
- Company: `vatId`, `taxId`, `tradeRegisterNumber`, `registerCourt`, `companySize` (Enum), `deliveryAddress`, `dataEnrichedAt`, `dataEnrichedSource`
- Referenz: Abschnitt A.1 oben fuer vollstaendige Feldliste
- **Achtung:** Bestehende API-Responses und DTOs muessen die neuen Felder enthalten
- **Migration:** `prisma db push` auf dem Server nach Schema-Aenderung

**1.2 Contact/Deal Owner (m:n Modell)**
- Neue Tabellen: `crm_contact_owners` und `crm_deal_owners` (contact_id/deal_id, user_id, role: OWNER|MEMBER|WATCHER)
- Bei Kontakt-Erstellung: erstellenden User automatisch als OWNER setzen
- Owner-Info in Detail-Responses mitliefern (User-ID + Rolle)
- Endpoints: `POST/DELETE /crm/contacts/:id/owners`, analog fuer Deals
- Referenz: Abschnitt A.3 + A.4 oben

**1.3 Lost-Grund bei Deals**
- Neue Felder: `lostReason` (Enum: PRICE|TIMING|COMPETITOR|NO_NEED|OTHER) + `lostReasonText` (Freitext)
- Bei Stage-Wechsel zu Lost: lostReason Pflichtfeld
- Referenz: Abschnitt A.4 oben

**1.4 Redis Pub/Sub Events**
- Events publishen bei: Contact created/updated, Deal stage_changed/won/lost, Activity due_soon
- Key-Prefix: `app:crm:events:*`
- Event-Format: `{ type: 'crm.deal.won', tenantId, entityId, userId, timestamp, payload }`
- Referenz: Abschnitt A.11 oben

#### Phase 2 — DANACH (neue Features, keine CORE-Abhaengigkeit)

Nach Abschluss von Phase 1 koennen diese Features **parallel** entwickelt werden:

**2.1 Custom Fields System**
- DB: `crm_custom_field_defs` + `crm_custom_field_values` (Details in A.6)
- CRUD Endpoints fuer Field-Definitions: `GET/POST/PATCH/DELETE /crm/custom-fields`
- Wert-Speicherung bei Entity Create/Update
- Custom Fields in Entity-Detail-Responses mitliefern
- 8 Feldtypen: Text, Textarea, Zahl, Datum, Dropdown, Mehrfachauswahl, Checkbox, URL

**2.2 Firmendaten-Anreicherung (Data Enrichment)**
- `POST /crm/companies/:id/enrich` Endpoint
- Paralleler Abruf: Unternehmensregister.de (kostenlos) + North Data API (API-Key pro Tenant)
- Ergebnis als Vorschlag zurueckgeben (Frontend zeigt Vergleich-Modal)
- Admin-Einstellung fuer North Data API-Key
- Referenz: Abschnitt A.2 oben

**2.3 Kontakt-Import**
- CSV-Import mit Spalten-Mapping (Backend parst, gibt Vorschau zurueck)
- Excel-Import (.xlsx) — gleiche Logik
- vCard-Import (.vcf + ZIP)
- Duplikat-Erkennung via E-Mail
- Referenz: Abschnitt A.7 oben

**2.4 Berechtigungsmodell**
- Sichtbarkeitsfilter in allen List-Queries: Eigene / Team / Alle
- Konfigurierbar pro Rolle via Tenant-Admin
- Referenz: Abschnitt A.8 oben

**2.5 Forecast-Endpoint**
- `GET /crm/deals/forecast` — Aggregierter Dealwert pro Stage gewichtet mit Wahrscheinlichkeit
- Referenz: Abschnitt A.4 oben

#### Phase 3 — SPAETER (abhaengig von CORE OAuth)

> **BLOCKER:** Diese Features benoetigen die Microsoft 365 OAuth-Integration im Core-Service.
> Der Core-Entwickler arbeitet **parallel** an: OAuth-Flow, `user_integrations`-Tabelle,
> Azure App-Registrierung. Erst wenn das steht, kann Phase 3 beginnen.

**3.1 E-Mail Tab** — `GET /crm/contacts/:id/emails` (Proxy zu MS Graph API, Redis 5 Min Cache)
**3.2 Kalender Tab** — `GET /crm/contacts/:id/calendar` (Graph API, Redis Cache)
**3.3 Aufgaben Sync** — Bidirektional mit Microsoft To Do (@Cron alle 5 Min, Redis-Lock pro User)
**3.4 Kontakte Export** — `POST /crm/contacts/:id/export-to-outlook` (Graph API)

Details: Abschnitte B.1–B.4 oben

#### Phase 4 — NICE-TO-HAVE

**4.1 Visitenkarten-Scan** — Anthropic Vision API, Rate Limit 50/Tag/Tenant
**4.2 CRM Reporting** — 5 Dashboards + CSV-Export (Pipeline, Aktivitaeten, Kontaktwachstum, Win/Loss, Mitarbeiter-Performance)

### Wichtige technische Hinweise

1. **Schema-Abgleich noetig:** Das Ziel-Schema des Architekten (A.10) weicht vom aktuellen Prisma-Schema ab. Aktuell: flaches Contact/Company-Modell. Ziel: `crm_contacts` + `crm_persons` + `crm_companies` Split. **Entscheidung noetig:** Schrittweise migrieren oder Big-Bang-Umbau?

2. **User-Referenzen:** Contact/Deal-Owner referenzieren `user_id` aus `platform_core`. Keine User-Daten kopieren — bei Bedarf per REST-Call an Core-Service (`GET /api/users/:id`) oder JWT-Payload nutzen.

3. **Neue Rollen:** Das Berechtigungsmodell (A.8) definiert `team_lead` und `tenant_readonly` — diese Rollen existieren im Core noch NICHT. Abstimmung mit Core-Entwickler noetig.

4. **Bestehende Frontend-Hooks:** Das Frontend hat bereits React Query Hooks fuer alle bestehenden Endpoints. Neue Endpoints/Felder muessen in `packages/frontend/src/crm/types.ts`, `api.ts` und `hooks.ts` ergaenzt werden — das uebernimmt der Frontend-Entwickler nach Rueckmeldung.

### Kommunikationsweg

Bitte fuer jede abgeschlossene Phase einen Eintrag hier in diese Datei schreiben:
- Welche Endpoints sind neu/geaendert?
- Welche Prisma-Migrationen wurden ausgefuehrt?
- Gibt es Breaking Changes an bestehenden APIs?
- Welche Frontend-Anpassungen sind noetig?

Format: `## YYYY-MM-DD | CRM-Backend: [Betreff]`

---

## 2026-03-12 | CRM-Backend: Phase 1 — Schema-Expansion, Owner, Lost-Reason, Events

### Zusammenfassung

Umsetzung der Phase 1 ("SOFORT") gemaess Architektur-Briefing. Alle Arbeitspakete ohne externe Abhaengigkeiten implementiert.

### 1. Prisma Schema + SQL Migration

**Neue Enums (7):** `ContactSource`, `EntityStatus`, `CompanySize`, `OwnerRole`, `LostReason`, `EmailType`, `PhoneType` + `FOLLOWUP` zu ActivityType.

**Neue Felder auf Contact:** `linkedinUrl`, `birthday`, `source` (ContactSource), `department`, `status` (EntityStatus).

**Neue Felder auf Company:** `vatId`, `taxId`, `tradeRegisterNumber`, `registerCourt`, `companySize`, `deliveryStreet/Zip/City/Country`, `dataEnrichedAt`, `dataEnrichedSource`, `status` (EntityStatus).

**Neue Felder auf Deal:** `lostReason` (LostReason), `lostReasonText`.

**Neue Tabellen (5):** `contact_emails`, `contact_phones`, `contact_owners`, `company_owners`, `deal_owners`.

**Migration:** `prisma/migrations/20260312_phase1_schema_expansion/migration.sql` — inkl. Daten-Migration (isActive→status, email/phone→contact_emails/phones, ownerId→company_owners).

### 2. Multi-Value E-Mails & Telefone (Breaking Change)

Bisherige Einzel-Felder `email`, `phone`, `mobile` bleiben als **deprecated Legacy** erhalten. Neue Multi-Value Tabellen `contact_emails` und `contact_phones` dienen als primaere Datenquelle. Legacy-Felder werden automatisch aus Primary-Eintraegen synchronisiert.

**Neue Endpoints/Verhalten:**
- `POST /contacts` akzeptiert jetzt `emails: [{email, type, isPrimary}]` und `phones: [{phone, type, isPrimary}]`
- `PATCH /contacts/:id` mit Replace-Strategie (delete all + recreate)
- Analog fuer Companies
- Lexware-Import erzeugt automatisch Multi-Value Eintraege
- `status` Feld (ACTIVE/INACTIVE/BLOCKED) ersetzt `isActive` Boolean — beide werden synchron gehalten

**DTOs:**
- `CreateEmailDto`: `{email, type?: EmailType, isPrimary?: boolean}`
- `CreatePhoneDto`: `{phone, type?: PhoneType, isPrimary?: boolean}`
- `EntityStatus`: ACTIVE, INACTIVE, BLOCKED
- `CompanySize`: SIZE_1_10, SIZE_11_50, SIZE_51_200, SIZE_201_500, SIZE_500_PLUS
- `ContactSource`: TRADE_FAIR, REFERRAL, WEBSITE, COLD_CALL, IMPORT, BUSINESS_CARD, OTHER

### 3. Owner m:n Modell

Jeder Contact, Company und Deal hat jetzt ein mehrstufiges Ownership-Modell mit Rollen.

**Neue Endpoints:**
```
POST   /crm/contacts/:id/owners          Body: {userId, role?}
DELETE /crm/contacts/:id/owners/:userId

POST   /crm/companies/:id/owners         Body: {userId, role?}
DELETE /crm/companies/:id/owners/:userId

POST   /crm/deals/:id/owners             Body: {userId, role?}
DELETE /crm/deals/:id/owners/:userId
```

**OwnerRole Enum:** OWNER, MEMBER, WATCHER.

Bei `create()` wird der erstellende User automatisch als OWNER eingetragen. Upsert-Verhalten: Wenn Owner bereits existiert, wird nur die Rolle aktualisiert.

### 4. Lost-Reason bei Deals

**Neue Felder:** `lostReason` (Enum: PRICE, TIMING, COMPETITOR, NO_NEED, OTHER), `lostReasonText` (Freitext).

**Validierung:**
- `PATCH /deals/:id` mit `status: 'LOST'` erfordert `lostReason` (im DTO oder bereits gesetzt) — sonst 400 BadRequest.
- `PATCH /deals/:id` mit `status: 'WON'` loescht automatisch `lostReason` und `lostReasonText`.

### 5. Redis Pub/Sub Events

**CrmEventPublisher** (global verfuegbar): Publiziert Events auf Redis Channels im Format `app:crm:events:{type}`.

**Event-Typen:**
- `crm.contact.created` — nach Contact-Erstellung
- `crm.contact.updated` — nach Contact-Update
- `crm.deal.created` — nach Deal-Erstellung
- `crm.deal.stage_changed` — bei Stage-Aenderung (payload: previousStageId, newStageId)
- `crm.deal.won` — bei Deal WON (payload: value, currency)
- `crm.deal.lost` — bei Deal LOST (payload: lostReason)
- `crm.activity.due_soon` — Activities faellig in 24h (Cron alle 15 Min)

**Event-Payload:**
```json
{
  "type": "crm.contact.created",
  "tenantId": "uuid",
  "entityId": "uuid",
  "userId": "uuid",
  "timestamp": "ISO-8601",
  "payload": {}
}
```

### Response-Aenderungen fuer Frontend

Alle Entity-Responses (Contact, Company, Deal) enthalten jetzt zusaetzlich:
- `emails: [{id, email, type, isPrimary, createdAt}]`
- `phones: [{id, phone, type, isPrimary, createdAt}]`
- `owners: [{id, tenantId, userId, role, createdAt}]`
- `status: 'ACTIVE' | 'INACTIVE' | 'BLOCKED'`

Contact zusaetzlich: `linkedinUrl`, `birthday`, `source`, `department`.
Company zusaetzlich: `vatId`, `taxId`, `tradeRegisterNumber`, `registerCourt`, `companySize`, `deliveryStreet/Zip/City/Country`.
Deal zusaetzlich: `lostReason`, `lostReasonText`.

### Geaenderte Dateien (18) + Neue Dateien (10)

Siehe Plan-Datei fuer komplette Dateiliste. Wichtigste neue Dateien:
- `src/common/dto/contact-info.dto.ts` — Shared DTOs + Enums
- `src/common/dto/owner.dto.ts` — AddOwnerDto + OwnerRole
- `src/owners/owners.service.ts` + `owners.module.ts` — Shared Owner CRUD
- `src/events/crm-event-publisher.service.ts` — CRM Event Publisher
- `src/events/crm-events.module.ts` — Global Events Module
- `src/events/activity-due-soon.scheduler.ts` — Cron-Job

### TODO fuer Frontend

1. `types.ts` um neue Felder erweitern (emails[], phones[], owners[], status, linkedinUrl, birthday, source, department, lostReason, etc.)
2. Contact/Company Formulare um Multi-Value Email/Phone Eingabe erweitern
3. Owner-Management UI (Zuweisen/Entfernen von Owners)
4. Deal-Detail: Lost-Reason Feld bei Status LOST einblenden
5. EntityStatus Filter in Listen verwenden (statt isActive)

---

## 2026-03-12 | Frontend: Phase 1 Frontend-Integration abgeschlossen

### Zusammenfassung

Alle Backend-Aenderungen aus Phase 1 sind vollstaendig ins Frontend integriert. Commit `48df3c3`, deployed auf dem Server.

### Was wurde umgesetzt

| Bereich | Aenderung |
|---------|-----------|
| `types.ts` | 7 neue Enums + Label-Maps, erweiterte Interfaces (Contact, Company, Deal), neue Interfaces (ContactEmail, ContactPhone, EntityOwner) |
| `api.ts` | 6 Owner-Endpoints (add/remove fuer Contact, Company, Deal) |
| `hooks.ts` | 6 Owner-Mutation-Hooks mit Cache-Invalidierung |
| `ContactFormModal` | Neue Felder: LinkedIn, Geburtstag, Quelle, Abteilung, Status |
| `ContactDetailPage` | Anzeige: LinkedIn, Abteilung, Geburtstag, Quelle, Status |
| `CompanyDetailPage` | Anzeige: Lieferadresse, USt-IdNr, Steuernummer, Handelsregister, Groesse, Anreicherungsdatum |
| `DealFormModal` | Lost-Reason Dropdown + Freitext (Pflicht bei Status LOST) |
| `DealDetailPage` | Lost-Reason Anzeige mit Label + optionalem Freitext |
| `CompaniesPage` | EntityStatus statt isActive (ACTIVE=gruen, BLOCKED=rot, INACTIVE=orange) |
| `ActivityFeed` + `ActivityFormModal` + `ContactDetailPage` | FOLLOWUP als ActivityType ergaenzt |

### Noch nicht umgesetzt (spaetere Iteration)

- **Multi-Value E-Mail/Telefon UI**: Backend synct automatisch mit Legacy-Feldern (`email`, `phone`, `mobile`). Dediziertes Multi-Value-UI kommt spaeter.
- **Owner-Management UI**: Hooks/API sind fertig, dedizierte UI-Komponente (Zuweisen/Entfernen) kommt spaeter.
- **Status-Filter Dropdown**: Status-Dots in Listen aktualisiert, aber noch kein Filter-Dropdown in der Suchleiste.

---

## 2026-03-12 | Plattform-Admin: Briefing Phase 2 fuer CRM-Backend-Experten

### Kontext

Phase 1 ist komplett abgeschlossen (Backend + Frontend deployed). Die naechste Prioritaet fuer das CRM-Backend ist **Phase 2** — neue Features ohne CORE-Abhaengigkeiten.

### Aktueller Stand nach Phase 1

| Modul | Status |
|-------|--------|
| Contacts (Person + Company) | Laeuft — mit erweiterten Feldern (LinkedIn, Geburtstag, Quelle, Status, Multi-Value E-Mail/Telefon) |
| Companies (Standalone) | Laeuft — mit USt-IdNr, Handelsregister, Groesse, Lieferadresse, Lexware-Sync |
| Deals | Laeuft — mit Lost-Reason (Enum + Freitext), Owner-Modell |
| Pipelines + Stages | Laeuft |
| Activities | Laeuft — 6 Typen inkl. FOLLOWUP, Scheduler fuer faellige Aktivitaeten |
| Owner-Modell | Laeuft — m:n fuer Contacts, Companies, Deals (OWNER/MEMBER/WATCHER) |
| Redis Pub/Sub Events | Laeuft — 7 Event-Typen |
| Industries | Laeuft |
| Trade Events (Messe-Timer) | Laeuft |
| CRM Settings | Laeuft |

### Phase 2 Aufgaben — Priorisierte Reihenfolge

---

#### 2.1 Custom Fields System (HOECHSTE PRIO)

**Warum zuerst:** Groesster Mehrwert — Mandanten koennen Felder selbst konfigurieren, ohne Schema-Aenderungen oder Deployments.

**Ziel-Schema (2 Tabellen):**

```
crm_custom_field_defs
  id                UUID PK
  tenant_id         UUID NOT NULL  -> FK tenants
  entity_type       ENUM('PERSON', 'COMPANY', 'DEAL')
  name              VARCHAR  -- interner Name (slug, auto-generiert)
  label             VARCHAR  -- Anzeigename
  field_type        ENUM('TEXT', 'TEXTAREA', 'NUMBER', 'DATE', 'DROPDOWN', 'MULTI_SELECT', 'CHECKBOX', 'URL')
  options           JSONB  -- nur fuer DROPDOWN/MULTI_SELECT: [{value: "A", label: "Segment A"}, ...]
  is_required       BOOLEAN DEFAULT false
  position          INT DEFAULT 0  -- Sortierung
  created_at        TIMESTAMP
  updated_at        TIMESTAMP
  UNIQUE(tenant_id, entity_type, name)

crm_custom_field_values
  id                UUID PK
  tenant_id         UUID NOT NULL  -> FK tenants
  field_def_id      UUID NOT NULL  -> FK crm_custom_field_defs
  entity_id         UUID NOT NULL  -- Contact/Company/Deal ID
  value_text        TEXT
  value_number      DECIMAL
  value_date        TIMESTAMP
  value_boolean     BOOLEAN
  value_json        JSONB  -- fuer MULTI_SELECT
  created_at        TIMESTAMP
  updated_at        TIMESTAMP
  UNIQUE(field_def_id, entity_id)
```

**Endpoints (6):**

| Methode | Pfad | Beschreibung |
|---------|------|-------------|
| `GET` | `/crm/custom-fields?entityType=PERSON` | Alle Feld-Definitionen (gefiltert nach Entity-Typ) |
| `POST` | `/crm/custom-fields` | Neue Feld-Definition anlegen |
| `PATCH` | `/crm/custom-fields/:id` | Definition bearbeiten (Label, Position, Required, Options) |
| `DELETE` | `/crm/custom-fields/:id` | Definition loeschen (CASCADE auf Values!) |
| `PUT` | `/crm/custom-fields/:entityId/values` | Alle Custom-Field-Werte fuer eine Entity setzen/aktualisieren (Bulk-Upsert) |
| `GET` | `/crm/custom-fields/:entityId/values` | Alle Custom-Field-Werte fuer eine Entity lesen |

**Wichtige Regeln:**
- `name` wird aus `label` automatisch generiert (slugify: "Kundennummer" -> "kundennummer"). Muss pro Tenant+EntityType unique sein.
- Bei `DELETE` einer Definition: Alle zugehoerigen Values loeschen (Cascade).
- Custom Fields in Entity-Detail-Responses mitliefern (neues Feld `customFields` als Array).
- Position bestimmt Reihenfolge in Formularen — bei Drag&Drop im Admin wird nur `position` per PATCH aktualisiert.
- Validierung: Bei `is_required: true` pruefen, ob Wert gesetzt ist (beim Entity-Speichern).
- Bei DROPDOWN/MULTI_SELECT: `options` muss ein JSON-Array mit `{value, label}` Objekten sein.

**DTOs:**

```typescript
// CreateCustomFieldDto
{
  entityType: 'PERSON' | 'COMPANY' | 'DEAL'  // @IsEnum
  label: string                                // @IsString, @MinLength(1)
  fieldType: 'TEXT' | 'TEXTAREA' | 'NUMBER' | 'DATE' | 'DROPDOWN' | 'MULTI_SELECT' | 'CHECKBOX' | 'URL'
  options?: { value: string; label: string }[] // Pflicht bei DROPDOWN/MULTI_SELECT
  isRequired?: boolean                         // Default: false
  position?: number                            // Default: 0
}

// UpdateCustomFieldDto — PartialType(CreateCustomFieldDto) OHNE entityType und fieldType
// (Typ-Aenderung wuerde bestehende Values invalidieren)

// SetCustomFieldValuesDto
{
  values: {
    fieldDefId: string   // @IsUUID
    value: string | number | boolean | string[] | null
  }[]
}
```

**Response-Format in Entity-Details:**

```json
{
  "id": "...",
  "firstName": "Max",
  "customFields": [
    {
      "fieldDefId": "uuid",
      "label": "Kundennummer",
      "fieldType": "TEXT",
      "value": "KD-12345"
    },
    {
      "fieldDefId": "uuid",
      "label": "DSGVO-Einwilligung",
      "fieldType": "CHECKBOX",
      "value": true
    }
  ]
}
```

---

#### 2.2 Kontakt-Import (CSV, Excel, vCard)

**Warum wichtig:** Kunden wollen bestehende Daten migrieren. Ohne Import kein produktiver Einsatz.

**Ablauf (3 Schritte):**

1. **Upload:** User laedt Datei hoch -> Backend parst und gibt Spalten + erste 10 Zeilen als Vorschau zurueck
2. **Mapping:** User ordnet Datei-Spalten den CRM-Feldern zu -> Backend fuehrt Import aus
3. **Ergebnis:** Backend gibt Zusammenfassung zurueck (erstellt, aktualisiert, uebersprungen, Fehler)

**Endpoints (3):**

| Methode | Pfad | Beschreibung |
|---------|------|-------------|
| `POST` | `/crm/import/preview` | Datei hochladen, Vorschau generieren. Body: `multipart/form-data` mit `file` + `type` (csv/xlsx/vcf) |
| `POST` | `/crm/import/execute` | Import ausfuehren mit Spalten-Mapping |
| `GET` | `/crm/import/history` | Letzte Imports mit Status (optional, nice-to-have) |

**Preview-Response:**

```json
{
  "columns": ["Name", "E-Mail", "Telefon", "Firma"],
  "rows": [
    ["Max Mustermann", "max@firma.de", "+49 123", "Firma GmbH"],
    ["..."]
  ],
  "totalRows": 250,
  "format": "csv"
}
```

**Execute-Request:**

```json
{
  "importId": "uuid-aus-preview",
  "entityType": "PERSON",
  "mapping": {
    "Name": "lastName",
    "E-Mail": "email",
    "Telefon": "phone",
    "Firma": "companyName"
  },
  "duplicateStrategy": "SKIP" | "UPDATE" | "MARK",
  "options": {
    "skipFirstRow": true
  }
}
```

**Execute-Response:**

```json
{
  "created": 180,
  "updated": 45,
  "skipped": 20,
  "errors": 5,
  "errorDetails": [
    { "row": 42, "field": "email", "message": "Ungueltige E-Mail-Adresse" }
  ]
}
```

**Unterstuetzte Formate:**
- **CSV:** Standard-Parsing mit auto-detect Delimiter (`,` / `;` / `\t`). Encoding: UTF-8 mit BOM-Erkennung.
- **XLSX:** Erstes Sheet lesen. Bibliothek-Empfehlung: `xlsx` oder `exceljs` (was bereits in Node-Abhaengigkeiten ist).
- **vCard (.vcf):** Einzeln oder als ZIP mit mehreren .vcf Dateien. Mapping vorgegeben (vCard-Felder -> CRM-Felder).

**Duplikat-Erkennung:** Primaer ueber `email`-Feld. Wenn E-Mail bereits existiert (im gleichen Tenant):
- `SKIP`: Zeile ueberspringen
- `UPDATE`: Bestehenden Kontakt aktualisieren (nur nicht-leere Felder ueberschreiben)
- `MARK`: Importieren und als potentielles Duplikat markieren (spaeter manuell aufloesen)

**Wichtig:**
- Maximal 5.000 Zeilen pro Import (konfigurierbar pro Tenant via CRM Settings)
- Temporaere Datei nach Import loeschen (DSGVO)
- Import laeuft synchron bei <500 Zeilen, asynchron mit Status-Polling bei >500
- vCard-Import: Mapping ist fix (kein User-Mapping noetig), aber Vorschau trotzdem zeigen

---

#### 2.3 Forecast-Endpoint

**Warum:** Vertriebsleitung braucht Uebersicht ueber erwarteten Umsatz.

**Endpoint:**

```
GET /crm/deals/forecast?pipelineId=uuid&period=quarter|month|year
```

**Response:**

```json
{
  "pipeline": "Vertrieb",
  "period": "2026-Q1",
  "stages": [
    {
      "stageId": "uuid",
      "stageName": "Qualifiziert",
      "probability": 0.3,
      "dealCount": 12,
      "totalValue": 150000,
      "weightedValue": 45000
    }
  ],
  "totals": {
    "dealCount": 45,
    "totalValue": 520000,
    "weightedValue": 198000
  }
}
```

**Logik:**
- `weightedValue = totalValue * probability`
- `probability` kommt aus der Stage-Definition (neues Feld `probability: DECIMAL DEFAULT 0` in `crm_pipeline_stages` ergaenzen!)
- Nur Deals mit Status `OPEN` zaehlen
- `period`-Filter: basiert auf `expectedCloseDate`
- Wenn `pipelineId` fehlt: alle Pipelines zusammenfassen

**Schema-Aenderung:** Neues Feld in `crm_pipeline_stages`:
```
probability  DECIMAL(3,2) DEFAULT 0  -- 0.00 bis 1.00
```
Default-Werte bei bestehenden Stages auf 0 setzen. Frontend-Entwickler ergaenzt Wahrscheinlichkeits-Eingabe im Pipeline-Admin.

---

#### 2.4 Firmendaten-Anreicherung (Data Enrichment)

**Warum:** Automatisiertes Befuellen von Firmendaten spart manuelle Recherche.

**Endpoint:**

```
POST /crm/companies/:id/enrich
```

**Response (Vorschlagsdaten, nicht automatisch uebernommen):**

```json
{
  "source": "north_data",
  "suggestions": {
    "street": { "current": "", "suggested": "Industriestr. 5", "source": "north_data" },
    "city": { "current": "Muenchen", "suggested": "Muenchen", "source": "north_data" },
    "vatId": { "current": "", "suggested": "DE123456789", "source": "unternehmensregister" },
    "tradeRegisterNumber": { "current": "", "suggested": "HRB 12345", "source": "unternehmensregister" },
    "registerCourt": { "current": "", "suggested": "AG Muenchen", "source": "unternehmensregister" },
    "industry": { "current": null, "suggested": "Software & IT", "source": "north_data" },
    "companySize": { "current": null, "suggested": "SIZE_51_200", "source": "north_data" },
    "websiteUrl": { "current": "", "suggested": "https://firma.de", "source": "north_data" }
  },
  "enrichedAt": "2026-03-12T10:30:00Z"
}
```

**Implementierung:**
1. Unternehmensregister.de abfragen (kostenlos, Scraping/API — Firmenname als Suchbegriff)
2. North Data API abfragen (API-Key aus Tenant-Settings, `GET https://www.northdata.de/api/v1/company?name=...`)
3. Ergebnisse zusammenfuehren (North Data hat Vorrang bei Konflikten)
4. Timeout pro Quelle: 10 Sekunden, bei Fehler einzelner Quelle: verfuegbare Daten zurueckgeben
5. `dataEnrichedAt` + `dataEnrichedSource` in Company aktualisieren (NACH User-Bestaetigung, d.h. beim naechsten PATCH)

**Admin-Einstellung noetig:**
- North Data API-Key pro Tenant speichern (`crm_settings` Tabelle oder eigene `crm_integrations` Tabelle)
- Endpoint: `GET/PUT /crm/settings/integrations/north-data` mit `{ apiKey: string }`

**Hinweis:** Dieser Endpoint kann initial nur mit Unternehmensregister.de umgesetzt werden (kostenlos). North Data wird aktiviert, sobald der Tenant einen API-Key hinterlegt hat.

---

#### 2.5 Berechtigungsmodell (NIEDRIGSTE PRIO in Phase 2)

**Warum niedrigere Prio:** Aktuell gibt es wenige User pro Tenant. Wird wichtiger bei Wachstum.

**Konzept:** Sichtbarkeitsfilter in allen List-Queries basierend auf Owner-Zugehoerigkeit.

| Stufe | Beschreibung |
|-------|-------------|
| `OWN` | User sieht nur Entities, bei denen er Owner/Member ist |
| `TEAM` | User sieht alle Entities seiner Abteilung (benoetigt `department`-Feld am User — Abstimmung mit Core noetig!) |
| `ALL` | User sieht alle Entities des Tenants (aktuelles Verhalten) |

**Schema-Aenderung:**

Neue Tabelle oder Erweiterung von `crm_settings`:
```
crm_visibility_settings
  id          UUID PK
  tenant_id   UUID NOT NULL
  role        VARCHAR  -- 'tenant_admin', 'team_lead', 'tenant_member', 'tenant_readonly'
  visibility  ENUM('OWN', 'TEAM', 'ALL')
  can_create  BOOLEAN DEFAULT true
  can_edit    ENUM('OWN', 'TEAM', 'ALL')
  can_delete  ENUM('OWN', 'TEAM', 'ALL')
  UNIQUE(tenant_id, role)
```

**Implementierung:**
- In allen `findAll`/`findOne` Service-Methoden: Sichtbarkeitsfilter basierend auf User-Rolle + Tenant-Settings
- `TenantGuard` erweitern oder eigenen `VisibilityGuard` erstellen
- Default-Werte: `tenant_admin` -> ALL/ALL/ALL, `tenant_member` -> ALL (bisheriges Verhalten beibehalten!)
- Endpoint: `GET/PUT /crm/settings/visibility` fuer Admin-Konfiguration

**ACHTUNG — Abhaengigkeit:** Die Rolle `team_lead` und `tenant_readonly` existieren im Core noch NICHT. Fuer Phase 2 reicht es, das Berechtigungsmodell mit den bestehenden Rollen (`tenant_admin`, `tenant_member`) zu implementieren. Erweiterung auf neue Rollen erfolgt nach Abstimmung mit dem Core-Entwickler.

---

### Empfohlene Reihenfolge

```
2.1 Custom Fields  ━━━━━━━━━━━━━━━━━━━━━  (1-2 Tage)
                                            |
2.2 Kontakt-Import ━━━━━━━━━━━━━━━━━━━━━   | parallel moeglich
                                            |
2.3 Forecast       ━━━━━━━━━━━━━           |
                                            |
2.4 Data Enrichment ━━━━━━━━━━━━━━━        |
                                            |
2.5 Berechtigungen ━━━━━━━━━━━━━━━━━━      | (erst wenn 2.1-2.4 stehen)
```

**2.1 und 2.2 koennen parallel entwickelt werden** — sie haben keine Abhaengigkeiten zueinander. 2.3 (Forecast) benoetigt die Schema-Aenderung an `crm_pipeline_stages` (Probability-Feld). 2.4 (Enrichment) ist eigenstaendig. 2.5 (Berechtigungen) sollte als letztes kommen, da es alle List-Queries aendert.

### Technische Hinweise

1. **Prisma-Schema:** Alle neuen Tabellen in `packages/crm-service/prisma/crm.schema.prisma`. Migration mit `prisma db push` oder eigener SQL-Migration.

2. **Modul-Struktur:** Fuer jedes Feature ein eigenes NestJS-Modul:
   - `src/custom-fields/` — CustomFieldsModule (Controller, Service, DTOs)
   - `src/import/` — ImportModule (Controller, Service, DTOs)
   - `src/forecast/` — ForecastModule oder als Teil von DealsModule (nur 1 Endpoint)
   - `src/enrichment/` — EnrichmentModule (Controller, Service)

3. **File Upload (Import):** NestJS `@UseInterceptors(FileInterceptor('file'))` mit `@UploadedFile()`. Maximal 10 MB. Temporaere Dateien in `/tmp/` mit UUID-Prefix, nach Verarbeitung loeschen.

4. **Testing:** Bei Import besonders wichtig: Edge Cases (leere Zeilen, Sonderzeichen, fehlende Pflichtfelder, doppelte E-Mails).

5. **Bestehende Frontend-Hooks:** Das Frontend hat Query-Key-Factories und Hook-Patterns etabliert. Fuer jedes neue Modul erstelle ich (Frontend) die entsprechenden Hooks, sobald die Endpoints stehen. **Bitte pro abgeschlossenem Feature einen Eintrag hier schreiben.**

### Kommunikationsweg

Bitte fuer jedes abgeschlossene Feature einen Eintrag in diese Datei schreiben:

```
## YYYY-MM-DD | CRM-Backend: Phase 2.X — [Feature-Name]
### Neue Endpoints (Methode, Pfad, kurze Beschreibung)
### Schema-Aenderungen (neue Tabellen/Felder)
### Response-Aenderungen (was aendert sich an bestehenden Responses?)
### TODO fuer Frontend
```

**Reihenfolge der Rueckmeldungen:** Am besten jedes Feature einzeln melden, dann kann ich (Frontend) parallel integrieren. Nicht alles auf einmal.

---

*Bitte neue Eintraege unten anfuegen. Format: `## YYYY-MM-DD | Absender: Betreff`*

---

## 2026-03-12 | CRM-Backend: Phase 2.1 — Custom Fields System

### Neue Endpoints

| Methode | Pfad | Beschreibung |
|---------|------|-------------|
| `POST` | `/api/v1/crm/custom-fields` | Feld-Definition erstellen |
| `GET` | `/api/v1/crm/custom-fields?entityType=PERSON` | Alle Feld-Definitionen auflisten (optional nach Entity-Typ gefiltert) |
| `GET` | `/api/v1/crm/custom-fields/:id` | Einzelne Feld-Definition abrufen |
| `PATCH` | `/api/v1/crm/custom-fields/:id` | Definition aktualisieren (Label, Options, Position, Required) |
| `DELETE` | `/api/v1/crm/custom-fields/:id` | Definition loeschen (CASCADE auf alle gespeicherten Werte!) |
| `PUT` | `/api/v1/crm/custom-fields/:entityId/values` | Custom-Field-Werte fuer eine Entity setzen (Bulk-Upsert) |
| `GET` | `/api/v1/crm/custom-fields/:entityId/values` | Custom-Field-Werte fuer eine Entity lesen |

### Schema-Aenderungen

**Neue Enums:**
- `CustomFieldEntityType`: PERSON, COMPANY, DEAL
- `CustomFieldType`: TEXT, TEXTAREA, NUMBER, DATE, DROPDOWN, MULTI_SELECT, CHECKBOX, URL

**Neue Tabellen:**

| Tabelle | Beschreibung |
|---------|-------------|
| `crm_custom_field_defs` | Feld-Definitionen pro Tenant + Entity-Typ. Unique: `[tenant_id, entity_type, name]`. Felder: entityType, name (Auto-Slug), label, fieldType (immutable), options (JSONB fuer DROPDOWN/MULTI_SELECT), isRequired, position |
| `crm_custom_field_values` | Gespeicherte Werte. Unique: `[field_def_id, entity_id]`. Spalten pro Typ: valueText, valueNumber, valueDate, valueBoolean, valueJson. FK auf Definitionen mit CASCADE Delete |

**SQL Migration:** `prisma/migrations/20260312_phase2_custom_fields/migration.sql`

### Response-Aenderungen

**Contact-Detail, Company-Detail, Deal-Detail** enthalten jetzt ein neues Feld `customFields`:

```json
{
  "id": "...",
  "firstName": "Max",
  "customFields": [
    {
      "fieldDefId": "uuid",
      "name": "kundennummer",
      "label": "Kundennummer",
      "fieldType": "TEXT",
      "value": "KD-12345"
    },
    {
      "fieldDefId": "uuid",
      "name": "segment",
      "label": "Segment",
      "fieldType": "DROPDOWN",
      "value": "A"
    },
    {
      "fieldDefId": "uuid",
      "name": "newsletter",
      "label": "Newsletter",
      "fieldType": "CHECKBOX",
      "value": null
    }
  ]
}
```

Das Array enthaelt ALLE definierten Custom Fields fuer den jeweiligen Entity-Typ (auch ohne Wert = `value: null`), sortiert nach `position`. Das Frontend bekommt so immer die vollstaendige Feldliste fuer Formular-Rendering.

### Value-Mapping nach Feldtyp

| FieldType | JS-Typ | Beispiel |
|-----------|--------|---------|
| TEXT, TEXTAREA, URL | string | `"KD-12345"` |
| NUMBER | number | `42.5` |
| DATE | string (ISO) | `"2026-01-15T00:00:00.000Z"` |
| CHECKBOX | boolean | `true` |
| DROPDOWN | string (single select) | `"premium"` |
| MULTI_SELECT | string[] | `["tag-a", "tag-b"]` |

### Wichtige Regeln

- **name** (interner Slug) wird automatisch aus dem Label generiert (Umlaute: ae/oe/ue/ss). Unique pro Tenant + Entity-Typ.
- **entityType** und **fieldType** sind nach Erstellung **nicht mehr aenderbar** (wuerde bestehende Werte invalidieren).
- **DROPDOWN/MULTI_SELECT** erfordern `options`: `[{value: "a", label: "Segment A"}, ...]`. Bei Wert-Speicherung wird gegen die Options validiert.
- **DELETE** einer Definition loescht automatisch alle gespeicherten Werte (CASCADE).
- **Orphan-Cleanup**: Beim Loeschen eines Contacts/Company/Deal werden zugehoerige Custom-Field-Werte automatisch entfernt.
- **isRequired** wird aktuell nur client-seitig ausgewertet (das Flag wird mitgeliefert). Server-seitige Pflichtfeld-Pruefung kann spaeter ergaenzt werden.

### ~~TODO fuer Frontend~~ — ERLEDIGT (2026-03-12)

Alle 4 Punkte wurden im Frontend umgesetzt und deployed (Commit `aaedf68`).

---

## 2026-03-12 | Frontend: Phase 2.1 Custom Fields — Frontend-Integration

### Was wurde umgesetzt

Die komplette Frontend-Integration fuer Custom Fields ist implementiert und auf dem Server deployed.

#### Neue Dateien (2 Dateien)

```
packages/frontend/src/crm/
  CustomFieldsDisplay.tsx    -- Read-only Anzeige von Custom Fields in Detail-Pages
  CustomFieldsForm.tsx       -- Editierbare Custom Fields in Entity-Formularen
```

#### Geaenderte Dateien (10 Dateien)

| Datei | Aenderung |
|-------|-----------|
| `types.ts` | Neue Typen: `CustomFieldEntityType`, `CustomFieldType`, `CustomFieldDef`, `CustomFieldValue`, `CustomFieldOption`, Payload-Interfaces. `customFields?: CustomFieldValue[]` zu Contact/Company/Deal hinzugefuegt. |
| `api.ts` | `customFieldsApi`-Objekt mit 7 Methoden: listDefs, getDef, createDef, updateDef, deleteDef, getValues, setValues |
| `hooks.ts` | 6 neue Hooks: `useCustomFieldDefs`, `useCreateCustomFieldDef`, `useUpdateCustomFieldDef`, `useDeleteCustomFieldDef`, `useCustomFieldValues`, `useSetCustomFieldValues`. Query-Key-Factory `crmKeys.customFields`. |
| `settings/CrmSettingsPage.tsx` | Neuer Tab "Eigene Felder" mit `CustomFieldsConfig`-Komponente (~490 Zeilen): Entity-Typ-Filter, Add/Edit-Formular, Options-Editor fuer DROPDOWN/MULTI_SELECT, Sortierung, Loeschen mit Warnung. |
| `contacts/ContactDetailPage.tsx` | `CustomFieldsDisplay` nach Notizen-Sektion eingebunden |
| `companies/CompanyDetailPage.tsx` | `CustomFieldsDisplay` nach Notizen-Sektion eingebunden |
| `deals/DealDetailPage.tsx` | `CustomFieldsDisplay` nach Notizen-Sektion eingebunden |
| `contacts/ContactFormModal.tsx` | `CustomFieldsForm` + `useSetCustomFieldValues` integriert, `saveCustomFields` in onSuccess-Handlers |
| `companies/CompanyFormModal.tsx` | Gleiche Integration wie ContactFormModal |
| `deals/DealFormModal.tsx` | Gleiche Integration wie ContactFormModal |

### Architektur-Entscheidungen

1. **Zwei getrennte Komponenten**: `CustomFieldsDisplay` (read-only, Detail-Pages) und `CustomFieldsForm` (editierbar, Formulare) — saubere Trennung von Anzeige und Bearbeitung.
2. **Ref-basierter State in Formularen**: `customFieldValuesRef` statt useState, um unnoetige Re-Renders zu vermeiden. Werte werden erst beim Submit ausgelesen.
3. **Post-Save-Pattern**: Custom Fields werden NACH dem Entity-Save gespeichert (`PUT /custom-fields/:entityId/values`), da bei Create die Entity-ID erst aus der Response kommt.
4. **DROPDOWN Fallback**: `CustomFieldValue` liefert keine Options-Liste (nur Definitionen haben sie). DROPDOWN rendert daher als Text-Input. Verbesserung: Options aus Definitionen nachladen.

### Unterstuetzte Feldtypen (alle 8)

| Typ | Display (Detail) | Form (Formular) |
|-----|-------------------|------------------|
| TEXT | String | `<input type="text">` |
| TEXTAREA | Pre-wrap String | `<textarea>` |
| NUMBER | Locale-formatiert (de-DE) | `<input type="number">` |
| DATE | Formatiert (dd.MM.yyyy) | `<input type="date">` |
| CHECKBOX | "Ja" / "Nein" | `<input type="checkbox">` |
| URL | Klickbarer Link | `<input type="url">` |
| DROPDOWN | String | `<input type="text">` (Fallback) |
| MULTI_SELECT | Farbige Badges | Tag-Input mit Enter-to-Add |

### Admin-Bereich (CRM Einstellungen > Eigene Felder)

- **Entity-Typ-Filter**: Buttons fuer Kontakte/Unternehmen/Vorgaenge
- **Feld hinzufuegen**: Label, Feldtyp (Dropdown), Pflichtfeld-Toggle
- **Options-Editor**: Fuer DROPDOWN und MULTI_SELECT — dynamische Wert/Label-Paare
- **Inline-Bearbeitung**: Label, Pflichtfeld, Options aendern
- **Sortierung**: Hoch/Runter-Buttons fuer Position
- **Loeschen**: Mit Warnung ueber Cascading (alle gespeicherten Werte werden geloescht)

### Deployment-Notiz

Das Docker-Volume `/app/node_modules` im CRM-Container cached den alten Prisma-Client. Bei Schema-Aenderungen muss der Container mit `-V` Flag neu erstellt werden:
```bash
docker compose -f docker-compose.yml -f docker-compose.crm.yml up -d crm --force-recreate -V
```

### Offene Punkte / Verbesserungen

- [ ] DROPDOWN/MULTI_SELECT im Formular: Options aus Feld-Definitionen laden statt Freitext-Fallback
- [ ] `isRequired` client-seitig validieren (Flag wird geliefert, Validierung noch nicht implementiert)
- [ ] Drag & Drop fuer Position-Sortierung (aktuell: Hoch/Runter-Buttons)

---

## 2026-03-12 | Architekt: Backend-Briefing Phase 2.2, 2.3, 2.4

### Parallelisierungsplan

Backend und Frontend arbeiten **parallel** an allen drei Phasen. Die API-Contracts sind unten festgelegt — Frontend baut UI gegen diese Contracts, Backend implementiert die Endpoints. Integration-Test wenn beide fertig sind.

```
Phase 2.3 (Forecast):     Backend ━━━━━  Frontend ━━━━━  (parallel)
Phase 2.2 (Import):       Backend ━━━━━━━━━  Frontend ━━━━━━━━━  (parallel)
Phase 2.4 (Enrichment):   Backend ━━━━━━━  Frontend ━━━━━  (parallel)
```

**Reihenfolge:** 2.3 → 2.2 → 2.4 (nach aufsteigendem Aufwand).

Alle drei Phasen sind unabhaengig voneinander — keine Abhaengigkeiten zwischen den Phasen.

---

### Phase 2.3: Forecasting (Prioritaet 1 — kleinstes Delta)

#### Schema-Aenderung

Neues Feld in `PipelineStage`:

```prisma
model PipelineStage {
  // ... bestehende Felder ...
  probability Decimal @default(0) @db.Decimal(3,2)  // NEU: 0.00 bis 1.00
}
```

**Migration:** `20260312_phase23_forecast`

```sql
ALTER TABLE app_crm.pipeline_stages
  ADD COLUMN probability DECIMAL(3,2) NOT NULL DEFAULT 0;
```

**DTO-Aenderungen:**
- `CreatePipelineStageDto`: Neues optionales Feld `probability?: number` (0-1, @Min(0) @Max(1))
- `UpdatePipelineStageDto`: Neues optionales Feld `probability?: number`
- Bestehende Stage-Responses muessen `probability` enthalten

#### Neuer Endpoint: Forecast

```
GET /crm/deals/forecast?pipelineId=uuid&period=quarter|month|year
```

**Platzierung:** Im `DealsController` als eigene Route **VOR** der `:id`-Route, damit kein Routing-Konflikt entsteht. Alternativ: eigener `ForecastController` (bevorzugt fuer saubere Trennung).

**Query-Parameter:**
| Param | Typ | Default | Beschreibung |
|-------|-----|---------|-------------|
| `pipelineId` | UUID (optional) | alle | Nur Deals dieser Pipeline |
| `period` | `quarter` / `month` / `year` | `quarter` | Zeitraum-Gruppierung |

**Logik:**
1. Alle Deals mit `status = 'OPEN'` laden (im Tenant)
2. Optional nach `pipelineId` filtern
3. Period-Filter auf `expectedCloseDate`:
   - `quarter`: Aktuelles Quartal (z.B. Q1 2026 = Jan-Mrz)
   - `month`: Aktueller Monat
   - `year`: Aktuelles Jahr
4. Nach Stage gruppieren
5. Pro Stage: `dealCount`, `totalValue` (Summe `value`), `weightedValue` (totalValue * stage.probability)
6. Totals ueber alle Stages berechnen

**Response-Contract:**

```json
{
  "success": true,
  "data": {
    "pipeline": "Vertrieb",
    "pipelineId": "uuid-oder-null",
    "period": "2026-Q1",
    "periodStart": "2026-01-01T00:00:00Z",
    "periodEnd": "2026-03-31T23:59:59Z",
    "stages": [
      {
        "stageId": "uuid",
        "stageName": "Qualifiziert",
        "probability": 0.3,
        "dealCount": 12,
        "totalValue": 150000.00,
        "weightedValue": 45000.00
      },
      {
        "stageId": "uuid",
        "stageName": "Angebot",
        "probability": 0.6,
        "dealCount": 8,
        "totalValue": 200000.00,
        "weightedValue": 120000.00
      }
    ],
    "totals": {
      "dealCount": 45,
      "totalValue": 520000.00,
      "weightedValue": 198000.00
    }
  },
  "meta": { "timestamp": "2026-03-12T18:00:00Z" }
}
```

Wenn `pipelineId` nicht angegeben: `pipeline` = `"Alle Pipelines"`, `pipelineId` = `null`, Deals aus allen Pipelines zusammengefasst.

#### TODO Backend (Phase 2.3)

- [ ] Prisma-Schema: `probability` Feld in `PipelineStage`
- [ ] Migration erstellen und anwenden
- [ ] `CreatePipelineStageDto` + `UpdatePipelineStageDto` erweitern
- [ ] Pipeline-Stage-Responses: `probability` mitliefern (findAll, findOne)
- [ ] Forecast-Endpoint implementieren (eigener Controller oder in DealsController)
- [ ] Response-Contract exakt wie oben

---

### Phase 2.2: CSV/Excel Import (Prioritaet 2 — hoechster User-Value)

#### Neue Dependencies

```bash
npm install csv-parser xlsx
npm install -D @types/multer
```

(`multer` ist bereits in `@nestjs/platform-express` enthalten)

#### Neues Modul

```
src/import/
  import.module.ts
  import.controller.ts
  import.service.ts
  dto/
    import-preview.dto.ts
    import-execute.dto.ts
```

Registrierung in `app.module.ts`: `imports: [..., ImportModule]`

#### Endpoint 1: Preview

```
POST /crm/import/preview
Content-Type: multipart/form-data
```

**Form Fields:**
| Field | Typ | Beschreibung |
|-------|-----|-------------|
| `file` | File | CSV, XLSX oder vCard Datei |
| `entityType` | String | `PERSON`, `COMPANY` oder `DEAL` |

**NestJS-Pattern:**
```typescript
@Post('preview')
@UseInterceptors(FileInterceptor('file', {
  limits: { fileSize: 10 * 1024 * 1024 }, // 10 MB
  fileFilter: (req, file, cb) => {
    const allowed = ['.csv', '.xlsx', '.xls', '.vcf'];
    const ext = extname(file.originalname).toLowerCase();
    cb(null, allowed.includes(ext));
  },
}))
async preview(
  @CurrentUser() user: JwtPayload,
  @UploadedFile() file: Express.Multer.File,
  @Body('entityType') entityType: string,
)
```

**Verarbeitungs-Logik:**
1. Format erkennen (Extension oder MIME-Type)
2. **CSV:** Auto-detect Delimiter (`,` `;` `\t`). UTF-8 mit BOM-Erkennung. Erste Zeile = Header.
3. **XLSX:** Erstes Sheet lesen. Erste Zeile = Header.
4. **vCard (.vcf):** Felder extrahieren (FN, N, EMAIL, TEL, ORG, ADR, URL, NOTE)
5. Datei mit UUID-Prefix in `/tmp/` speichern fuer spaetere Verarbeitung
6. Erste 10 Datenzeilen + Spaltennamen + Gesamtzeilenzahl zurueckgeben

**Response-Contract:**

```json
{
  "success": true,
  "data": {
    "importId": "uuid",
    "format": "csv",
    "columns": ["Name", "E-Mail", "Telefon", "Firma", "PLZ", "Stadt"],
    "rows": [
      ["Max Mustermann", "max@firma.de", "+49 123 456", "Firma GmbH", "80331", "Muenchen"],
      ["Anna Schmidt", "anna@example.com", "+49 987 654", "Schmidt AG", "10115", "Berlin"]
    ],
    "totalRows": 250,
    "availableTargetFields": {
      "PERSON": [
        { "field": "firstName", "label": "Vorname" },
        { "field": "lastName", "label": "Nachname" },
        { "field": "email", "label": "E-Mail" },
        { "field": "phone", "label": "Telefon" },
        { "field": "mobile", "label": "Mobil" },
        { "field": "companyName", "label": "Firmenname" },
        { "field": "position", "label": "Position" },
        { "field": "department", "label": "Abteilung" },
        { "field": "street", "label": "Strasse" },
        { "field": "zip", "label": "PLZ" },
        { "field": "city", "label": "Stadt" },
        { "field": "country", "label": "Land" },
        { "field": "website", "label": "Website" },
        { "field": "linkedinUrl", "label": "LinkedIn URL" },
        { "field": "notes", "label": "Notizen" },
        { "field": "tags", "label": "Tags (kommasepariert)" }
      ],
      "COMPANY": [
        { "field": "name", "label": "Firmenname" },
        { "field": "email", "label": "E-Mail" },
        { "field": "phone", "label": "Telefon" },
        { "field": "website", "label": "Website" },
        { "field": "street", "label": "Strasse" },
        { "field": "zip", "label": "PLZ" },
        { "field": "city", "label": "Stadt" },
        { "field": "country", "label": "Land" },
        { "field": "vatId", "label": "USt-IdNr." },
        { "field": "tradeRegisterNumber", "label": "Handelsregisternr." },
        { "field": "registerCourt", "label": "Registergericht" },
        { "field": "notes", "label": "Notizen" },
        { "field": "tags", "label": "Tags (kommasepariert)" }
      ]
    }
  }
}
```

**Hinweis:** `availableTargetFields` liefert die moeglichen Zielfelder fuer den Entity-Typ, damit das Frontend das Mapping-Dropdown befuellen kann.

#### Endpoint 2: Execute

```
POST /crm/import/execute
Content-Type: application/json
```

**Request-Body:**

```json
{
  "importId": "uuid-aus-preview",
  "entityType": "PERSON",
  "mapping": {
    "Name": "lastName",
    "E-Mail": "email",
    "Telefon": "phone",
    "Firma": "companyName",
    "PLZ": "zip",
    "Stadt": "city"
  },
  "duplicateStrategy": "SKIP",
  "options": {
    "skipFirstRow": true
  }
}
```

**DTO-Validierung:**
- `importId`: @IsUUID()
- `entityType`: @IsEnum(['PERSON', 'COMPANY', 'DEAL'])
- `mapping`: Record<string, string> — Key = Datei-Spalte, Value = CRM-Feld
- `duplicateStrategy`: @IsEnum(['SKIP', 'UPDATE', 'MARK'])
- `options.skipFirstRow`: @IsBoolean() @IsOptional()

**Verarbeitungs-Logik:**
1. Temp-Datei ueber `importId` laden
2. Alle Zeilen parsen
3. Pro Zeile: Felder gemaess Mapping zuordnen
4. **Duplikat-Erkennung** ueber `email` Feld (case-insensitive):
   - `SKIP`: Zeile ueberspringen, Zaehler erhoehen
   - `UPDATE`: Bestehenden Datensatz aktualisieren (nur nicht-leere Felder ueberschreiben)
   - `MARK`: Importieren und Tag `IMPORT_DUPLICATE` setzen
5. Validierung pro Zeile (E-Mail-Format, Pflichtfelder je nach entityType)
6. Batch-Insert via Prisma (nicht einzeln!)
7. **DSGVO:** Temp-Datei nach Verarbeitung LOESCHEN
8. Import-Statistik zurueckgeben

**Response-Contract:**

```json
{
  "success": true,
  "data": {
    "created": 180,
    "updated": 45,
    "skipped": 20,
    "errors": 5,
    "totalProcessed": 250,
    "errorDetails": [
      { "row": 42, "field": "email", "value": "ungueltig", "message": "Ungueltige E-Mail-Adresse" },
      { "row": 87, "field": "lastName", "value": "", "message": "Nachname ist ein Pflichtfeld" }
    ]
  }
}
```

**Constraints:**
- Max **5.000 Zeilen** pro Import — bei mehr: `400 Bad Request` mit Fehlermeldung
- Max **10 MB** Dateigroesse (im FileInterceptor konfiguriert)
- Synchron bei <500 Zeilen
- Bei >500 Zeilen: Synchron verarbeiten ist OK fuer Alpha (async spaeter)

#### Endpoint 3: Import History (Nice-to-Have)

```
GET /crm/import/history
```

Optional — nur wenn Zeit uebrig. Zeigt letzte 20 Imports fuer den Tenant.

#### TODO Backend (Phase 2.2)

- [ ] Dependencies installieren: `csv-parser`, `xlsx`
- [ ] ImportModule + Controller + Service + DTOs erstellen
- [ ] In `app.module.ts` registrieren
- [ ] Preview-Endpoint: CSV/XLSX/vCard Parsing, Temp-Datei-Management
- [ ] Execute-Endpoint: Mapping, Duplikat-Erkennung, Batch-Insert
- [ ] Temp-Dateien nach Verarbeitung loeschen (DSGVO)
- [ ] Response-Contracts exakt wie oben

---

### Phase 2.4: Datenanreicherung (Prioritaet 3)

#### Neues Modul

```
src/enrichment/
  enrichment.module.ts
  enrichment.controller.ts
  enrichment.service.ts
```

Registrierung in `app.module.ts`: `imports: [..., EnrichmentModule]`

#### Endpoint 1: Company Enrichment

```
POST /crm/companies/:id/enrich
```

**Logik:**
1. Company aus DB laden (Name, aktuelle Felder)
2. **Unternehmensregister.de** abfragen (kostenlos, immer verfuegbar):
   - Suche nach Firmenname
   - Felder: Handelsregisternr., Registergericht, Rechtsform, Sitz
   - Timeout: 10 Sekunden
3. **North Data API** abfragen (nur wenn API-Key konfiguriert):
   - `GET https://www.northdata.de/api/v1/company?name={name}&country=DE`
   - Header: `X-Api-Key: {apiKey}`
   - Felder: Adresse, Branche, Umsatz, Mitarbeiterzahl, Website
   - Timeout: 10 Sekunden
4. Ergebnisse zusammenfuehren (North Data hat Vorrang bei Konflikten)
5. **NUR Vorschlaege zurueckgeben** — NICHT automatisch in DB schreiben!
6. Bei Fehler einer Quelle: verfuegbare Daten der anderen Quelle zurueckgeben

**Response-Contract:**

```json
{
  "success": true,
  "data": {
    "companyId": "uuid",
    "sources": ["unternehmensregister", "north_data"],
    "suggestions": {
      "street": {
        "current": "",
        "suggested": "Industriestr. 5",
        "source": "north_data"
      },
      "city": {
        "current": "Muenchen",
        "suggested": "Muenchen",
        "source": "north_data",
        "match": true
      },
      "zip": {
        "current": "",
        "suggested": "80339",
        "source": "north_data"
      },
      "vatId": {
        "current": "",
        "suggested": "DE123456789",
        "source": "unternehmensregister"
      },
      "tradeRegisterNumber": {
        "current": "",
        "suggested": "HRB 12345",
        "source": "unternehmensregister"
      },
      "registerCourt": {
        "current": "",
        "suggested": "AG Muenchen",
        "source": "unternehmensregister"
      },
      "industry": {
        "current": null,
        "suggested": "Software & IT",
        "source": "north_data"
      },
      "companySize": {
        "current": null,
        "suggested": "SIZE_51_200",
        "source": "north_data"
      },
      "website": {
        "current": "",
        "suggested": "https://firma.de",
        "source": "north_data"
      }
    },
    "enrichedAt": "2026-03-12T18:30:00Z",
    "warnings": []
  }
}
```

**Hinweis:** `match: true` zeigt an, dass der aktuelle Wert mit dem Vorschlag uebereinstimmt (kein Update noetig). Nur Felder mit `suggested !== null` werden in der Response mitgegeben.

#### Endpoint 2: North Data API-Key Config

```
GET /crm/settings/integrations/north-data
```

Response:
```json
{
  "success": true,
  "data": {
    "configured": true,
    "apiKey": "****e4f8"
  }
}
```

```
PUT /crm/settings/integrations/north-data
```

Request:
```json
{ "apiKey": "nd_abc123..." }
```

**Speicherung:** In Redis unter Key `crm:{tenantId}:integrations:north_data` oder in einer neuen `crm_integrations`-Tabelle.

#### Wichtig: dataEnrichedAt / dataEnrichedSource

Diese Felder existieren bereits im Company-Schema (Phase 1):
```
dataEnrichedAt     DateTime? @map("data_enriched_at")
dataEnrichedSource String?   @map("data_enriched_source") @db.VarChar(200)
```

Sie werden **NICHT** vom Enrich-Endpoint aktualisiert. Stattdessen setzt das Frontend sie beim naechsten `PATCH /crm/companies/:id` wenn der User Vorschlaege uebernimmt:
```json
{
  "street": "Industriestr. 5",
  "dataEnrichedAt": "2026-03-12T18:30:00Z",
  "dataEnrichedSource": "north_data,unternehmensregister"
}
```

#### TODO Backend (Phase 2.4)

- [ ] EnrichmentModule + Controller + Service erstellen
- [ ] In `app.module.ts` registrieren
- [ ] Unternehmensregister.de Abfrage implementieren (Scraping oder API)
- [ ] North Data API Integration (mit API-Key aus Settings)
- [ ] Settings-Endpoints fuer API-Key (GET/PUT)
- [ ] Response-Contract exakt wie oben
- [ ] Timeouts + Fehlerbehandlung pro Quelle

---

### Hinweise fuer den Backend-Entwickler

1. **Reihenfolge:** Phase 2.3 zuerst (kleinstes Delta), dann 2.2, dann 2.4
2. **Patterns:** Alle bestehenden Patterns beibehalten (TenantGuard, singleResponse/paginatedResponse, class-validator DTOs, Swagger-Dekoratoren)
3. **Docker-Volume:** Bei Schema-Aenderungen Container mit `-V` neu erstellen:
   ```bash
   docker compose -f docker-compose.yml -f docker-compose.crm.yml up -d crm --force-recreate -V
   ```
4. **Tests:** Import-Edge-Cases besonders gruendlich testen (leere Zeilen, Sonderzeichen, fehlende Pflichtfelder, doppelte E-Mails)
5. **DSGVO:** Import-Temp-Dateien MUESSEN nach Verarbeitung geloescht werden
6. **Completion-Report:** Nach jeder Phase einen Eintrag in dieser Datei ergaenzen (wie bei Phase 2.1)

---

## Phase 2.3 Backend — Forecast (DONE)

**Implementiert am:** 2026-03-12

### Schema-Aenderung

- `PipelineStage.probability` — Decimal(3,2), Default 0, Bereich 0.00–1.00
- Migration: `prisma/migrations/20260312_phase23_forecast/migration.sql`

### Neue Endpoints

| Methode | Pfad | Beschreibung |
|---------|------|-------------|
| GET | /api/v1/crm/deals/forecast?pipelineId=&period= | Umsatz-Forecast (gewichtete Pipeline) |

### Forecast Response

```json
{
  "success": true,
  "data": {
    "period": "quarter",
    "periodStart": "2026-01-01T00:00:00.000Z",
    "periodEnd": "2026-03-31T23:59:59.999Z",
    "currency": "EUR",
    "stages": [
      {
        "stageId": "uuid",
        "stageName": "Qualifiziert",
        "stageColor": "#3B82F6",
        "probability": 0.25,
        "sortOrder": 1,
        "pipelineId": "uuid",
        "pipelineName": "Sales",
        "dealCount": 5,
        "totalValue": 50000,
        "weightedValue": 12500
      }
    ],
    "totals": {
      "dealCount": 15,
      "totalValue": 150000,
      "weightedValue": 67500
    }
  }
}
```

### Query-Parameter

- `pipelineId` (optional, UUID) — Filter nach Pipeline
- `period` (optional, enum: month|quarter|year, default: quarter)

### DTO-Updates

- `CreatePipelineStageDto.probability` — Optional, @IsNumber, @Min(0), @Max(1)
- `UpdateStageDto.probability` — Optional, gleiche Validierung

### Geaenderte Dateien

- `prisma/crm.schema.prisma` — probability auf PipelineStage
- `src/pipelines/dto/create-pipeline.dto.ts` — probability-Feld
- `src/pipelines/dto/update-stage.dto.ts` — probability-Feld
- `src/pipelines/pipelines.service.ts` — probability in create/addStage/updateStage
- `src/pipelines/pipelines.controller.ts` — probability durchreichen
- `src/deals/dto/forecast-query.dto.ts` — NEU: ForecastQueryDto + ForecastPeriod Enum
- `src/deals/deals.service.ts` — NEU: forecast() + getPeriodBounds()
- `src/deals/deals.controller.ts` — NEU: GET /deals/forecast (vor :id Route)

### TODO Frontend

- [x] Forecast-Seite (`/crm/forecast`) — Pipeline-Filter, Zeitraum-Toggle (Monat/Quartal/Jahr), Tabelle mit gewichteten Werten + Summary-Cards
- [x] probability-Feld in Pipeline-Stage-Editor (0–100% Input in PipelinesPage)

---

## Phase 2.2 Backend — CSV/Excel Import (DONE)

**Implementiert am:** 2026-03-12

### Neue Dependencies

- `csv-parser: ^3.0.0` — CSV-Parser mit Stream-Support
- `xlsx: ^0.18.5` — Excel-Parser (XLSX/XLS)
- `@types/multer: ^1.4.12` (devDep)

### Neue Endpoints

| Methode | Pfad | Beschreibung |
|---------|------|-------------|
| POST | /api/v1/crm/import/preview | Datei-Vorschau (Multipart, CSV/XLSX) |
| POST | /api/v1/crm/import/execute | Import ausfuehren |

### Preview (Multipart POST)

- **File-Upload**: `file` Feld, max 10MB, MIME: text/csv, application/vnd.ms-excel, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
- **Query-Params**: `entityType` (contact|company), `delimiter` (optional)
- **Max Rows**: 5000
- **Preview Rows**: Erste 10

```json
{
  "success": true,
  "data": {
    "importId": "uuid",
    "format": "csv",
    "columns": ["Vorname", "Nachname", "E-Mail", "Telefon"],
    "rows": [{"Vorname": "Max", "Nachname": "Muster", ...}],
    "totalRows": 250,
    "availableTargetFields": ["firstName", "lastName", "email", ...]
  }
}
```

### Execute (JSON POST)

```json
{
  "importId": "uuid-from-preview",
  "entityType": "contact",
  "mapping": [
    {"sourceColumn": "Vorname", "targetField": "firstName"},
    {"sourceColumn": "E-Mail", "targetField": "email"}
  ],
  "duplicateStrategy": "SKIP"
}
```

**Duplikat-Strategien** (E-Mail-Abgleich, case-insensitive):
- `SKIP` — Zeile ueberspringen
- `UPDATE` — Bestehenden Datensatz aktualisieren
- `MARK` — Neuen Datensatz mit Tag "DUPLIKAT" erstellen

**Response:**
```json
{
  "success": true,
  "data": {
    "created": 230,
    "updated": 0,
    "skipped": 15,
    "errors": 5,
    "totalProcessed": 250,
    "errorDetails": [{"row": 42, "field": "", "value": "", "message": "..."}]
  }
}
```

### Target Fields

- **Contact**: firstName, lastName, email, phone, mobile, companyName, position, department, website, street, zip, city, state, country, notes, tags, source, linkedinUrl
- **Company**: name, email, phone, website, industry, street, zip, city, state, country, vatId, taxId, tradeRegisterNumber, registerCourt, notes, tags

### GDPR

- Temp-Dateien werden unter `/tmp/crm-import-{uuid}{ext}` gespeichert
- Nach Execute werden Temp-Dateien IMMER geloescht (finally-Block)
- Owner wird automatisch auf den importierenden User gesetzt

### Neue Dateien

- `src/import/import.module.ts`
- `src/import/import.controller.ts`
- `src/import/import.service.ts`
- `src/import/dto/import-preview.dto.ts`
- `src/import/dto/import-execute.dto.ts`

### TODO Frontend

- [x] Import-Wizard (3 Schritte: Upload → Spalten-Mapping → Ergebnis) in CRM-Einstellungen → Tab "Import"
- [x] Import-Ergebnis-Anzeige (created/updated/skipped/errors mit Fehlerdetails)
- [x] entityType-Konvertierung: PERSON→contact, COMPANY→company
- [x] mapping-Konvertierung: Record→Array [{sourceColumn, targetField}]

---

## Phase 2.4 Backend — Datenanreicherung / Enrichment (DONE)

**Implementiert am:** 2026-03-12

### Neue Endpoints

| Methode | Pfad | Beschreibung |
|---------|------|-------------|
| POST | /api/v1/crm/companies/:id/enrich | Unternehmensdaten anreichern (Suggestion-Only!) |
| GET | /api/v1/crm/settings/integrations/north-data | North Data Einstellungen abrufen |
| PUT | /api/v1/crm/settings/integrations/north-data | North Data Einstellungen aktualisieren |

### Datenquellen

1. **Unternehmensregister.de** (kostenfrei) — Handelsregister-Daten: registerNumber, registerCourt, Adresse
2. **North Data API** (kostenpflichtig, optional) — Erweiterte Daten: name, vatId, website, phone, Adresse, industry

Beide Quellen werden **parallel** abgefragt (`Promise.allSettled`). Graceful Degradation: Wenn eine Quelle fehlschlaegt, werden Ergebnisse der anderen zurueckgegeben.

### Enrich Response (Suggestion-Only, KEIN Auto-Write)

```json
{
  "success": true,
  "data": {
    "companyId": "uuid",
    "companyName": "Muster GmbH",
    "sources": ["unternehmensregister.de", "northdata.de"],
    "suggestions": {
      "tradeRegisterNumber": {
        "current": null,
        "suggested": "HRB 12345",
        "source": "unternehmensregister.de"
      },
      "vatId": {
        "current": null,
        "suggested": "DE123456789",
        "source": "northdata.de"
      }
    },
    "enrichedAt": "2026-03-12T14:30:00.000Z",
    "warnings": []
  }
}
```

### North Data Settings

- Gespeichert in Redis: `crm:{tenantId}:integrations:north_data`
- Fallback: Env-Variable `NORTH_DATA_API_KEY`
- API-Key wird in GET-Response maskiert: `****xxxx`

### Env-Variablen (optional)

- `NORTH_DATA_API_KEY` — Globaler API-Key
- `NORTH_DATA_API_URL` — Base-URL (Default: https://www.northdata.de/_api)

### Health-Check

- `/health` Response enthaelt neuen Service: `enrichment: 'up' | 'down' | 'unconfigured'`
- Version auf `0.3.0` erhoeht

### Neue Dateien

- `src/enrichment/enrichment.module.ts`
- `src/enrichment/enrichment.controller.ts`
- `src/enrichment/enrichment.service.ts`
- `src/enrichment/dto/enrich-response.dto.ts`
- `src/enrichment/dto/enrichment-settings.dto.ts`

### Geaenderte Dateien

- `src/app.module.ts` — EnrichmentModule registriert
- `src/config/env.validation.ts` — NORTH_DATA_API_KEY + _URL
- `src/health/health.module.ts` — EnrichmentModule importiert
- `src/health/health.controller.ts` — enrichment Health-Check

### TODO Frontend

- [x] "Anreichern"-Button auf Company-Detail — POST /enrich, Suggestions-Modal (Tabelle mit Feld/Aktuell/Vorschlag/Quelle), "Uebernehmen" per PATCH
- [x] suggestions-Konvertierung: Record<field,{current,suggested,source}> → Array fuer UI
- [x] North Data API-Key in CRM-Einstellungen → Tab "Integrationen"

---

## 2026-03-12 | Frontend: Phase 2 abgeschlossen — Naechste Aufgaben fuer CRM-Backend

### Phase 2 Status: KOMPLETT

Alle vier Phase-2-Features sind backend- und frontendseitig implementiert und auf insight-dev-01 deployed.

| Phase | Backend | Frontend | Deployed |
|-------|---------|----------|---------|
| 2.1 Custom Fields | DONE | DONE | Ja |
| 2.2 CSV/Excel Import | DONE | DONE | Ja |
| 2.3 Forecasting | DONE | DONE | Ja |
| 2.4 Datenanreicherung | DONE | DONE | Ja |

Phase 3 (Microsoft 365 OAuth) ist BLOCKIERT bis Core-Service OAuth liefert.

---

### Naechste Aufgaben fuer den CRM-Backend-Entwickler

#### Prioritaet 1: Vertraege-API (Contract CRUD)

Das Datenmodell (`Contract`) ist bereits im Prisma-Schema vorhanden, aber es fehlen die Endpoints. Das Frontend hat eine `ContractsCard.tsx` die aktuell nur ein Platzhalter-UI anzeigt.

**Benoetigt:**

```
GET    /api/v1/crm/companies/:id/contracts        — Liste der Vertraege einer Firma
POST   /api/v1/crm/companies/:id/contracts        — Vertrag anlegen
PATCH  /api/v1/crm/companies/:id/contracts/:cid   — Vertrag bearbeiten
DELETE /api/v1/crm/companies/:id/contracts/:cid   — Vertrag loeschen
```

**Contract-Felder:** title, status (DRAFT|ACTIVE|EXPIRED|CANCELLED), startDate, endDate, value (Decimal), currency (Default EUR), notes

**Response-Format:** Standard `{ success, data, meta }` wie alle anderen Endpoints.

**Frontend-Hinweis:** Die `ContractsCard.tsx` und die Typen (`Contract`, `ContractStatus`) sind bereits im Frontend definiert (`packages/frontend/src/crm/types.ts`). Nach Implementierung der Endpoints muss der Frontend-Entwickler nur noch die Hooks + API-Calls hinzufuegen.

---

#### Prioritaet 2: Kanban-Board Support (Deal-Stage-Move)

Das Backend ist bereits bereit (`PATCH /crm/deals/:id` mit `{ stageId }`). Der Frontend-Entwickler kann das Kanban-Board ohne Backend-Aenderungen implementieren.

**Kein Backend-Handlungsbedarf.**

---

#### Prioritaet 3: Activity Feed — E-Mail & Aufgaben Tabs

Die Tabs "E-Mail" und "Aufgaben" auf der Company-Detail-Seite sind aktuell disabled (Platzhalter). Fuer die E-Mail-Integration wird Phase 3 (MS 365 OAuth) benoetigt.

**Fuer Aufgaben (Tasks) ohne MS 365:**
- Task-Typ existiert bereits in `ActivityType` (`TASK`, `FOLLOWUP`)
- Das Frontend kann Aufgaben bereits anlegen/anzeigen
- Optional: `scheduledAt` + `completedAt` Filter-Endpoints (`GET /crm/activities?type=TASK&completedAt=null`)

---

### TODO Backend (naechste Session)

- [x] Vertraege-API: `GET/POST/PATCH/DELETE /crm/companies/:id/contracts` — **DONE**
- [x] Prisma-Migration: Contract-Tabelle bereits im Schema vorhanden, keine Migration noetig
- [x] Activity-Filter `?type=TASK` — Bereits implementiert in `QueryActivitiesDto` + `ActivitiesService.findAll()`
- [x] Dokumentation aktualisiert

---

### 2026-03-12 | Backend: Contract CRUD — Fertiggestellt

**Neue Dateien:**

```
packages/crm-service/src/contracts/
  contracts.module.ts             — Feature Module
  contracts.service.ts            — CRUD Service (scoped by tenantId + companyId)
  contracts.controller.ts         — REST Controller (nested: /companies/:companyId/contracts)
  dto/
    create-contract.dto.ts        — CreateContractDto + ContractStatus Enum
    update-contract.dto.ts        — UpdateContractDto (PartialType)
    query-contracts.dto.ts        — QueryContractsDto (Pagination + Status-Filter + Search)
```

**Geaenderte Dateien:**
- `src/app.module.ts` — ContractsModule registriert
- `Summarize.md` — Contracts-Modul dokumentiert

**Endpoints:**

| Methode | Pfad | Beschreibung |
|---------|------|-------------|
| GET | /api/v1/crm/companies/:id/contracts | Vertraege auflisten (paginiert, filterbar nach status, search) |
| POST | /api/v1/crm/companies/:id/contracts | Vertrag anlegen |
| GET | /api/v1/crm/companies/:id/contracts/:cid | Vertrag-Details |
| PATCH | /api/v1/crm/companies/:id/contracts/:cid | Vertrag aktualisieren |
| DELETE | /api/v1/crm/companies/:id/contracts/:cid | Vertrag loeschen |

**Query-Parameter (GET Liste):** `?status=ACTIVE&search=Wartung&sort=startDate&order=asc&page=1&pageSize=25`

**Response-Format:** Standard `{ success, data, meta }` / `{ success, data, pagination, meta }`

**Activity-Filter:** Bereits implementiert — `GET /api/v1/crm/activities?type=TASK&companyId=xxx` funktioniert.

**TODO Frontend:** ~~Hooks + API-Calls in `ContractsCard.tsx` implementieren.~~ — **DONE** (2026-03-12)

---

## 2026-03-12 | Frontend: Vertraege-Modul implementiert

### Was wurde umgesetzt

Die `ContractsCard.tsx` ist vollstaendig implementiert. Das "Modul in Entwicklung"-Platzhalter wurde ersetzt durch eine vollwertige CRUD-Komponente.

### Geaenderte / neue Dateien

| Datei | Aenderung |
|-------|-----------|
| `crm/companies/ContractsCard.tsx` | Komplette Neuentwicklung (Platzhalter → vollwertige CRUD-Card) |
| `crm/types.ts` | `CreateContractPayload`, `UpdateContractPayload`, `ContractsQueryParams` ergaenzt |
| `crm/api.ts` | `contractsApi` mit list, create, update, delete (nested unter /companies/:id/contracts) |
| `crm/hooks.ts` | `crmKeys.contracts` + 4 Hooks: useContracts, useCreateContract, useUpdateContract, useDeleteContract |
| `crm/companies/CompanyDetailPage.tsx` | contractCount-Prop entfernt (Card laedt nun selbst via API) |

### Funktionsumfang

- **Liste**: Alle Vertraege einer Company mit Status-Badge (Entwurf/Aktiv/Abgelaufen/Storniert), Laufzeit, formatiertem Vertragswert
- **Erstellen**: "+ Neu" Button oeffnet Modal mit Titel*, Status, Beginn/Ende, Wert/Waehrung, Notizen
- **Bearbeiten**: Stift-Icon pro Zeile oeffnet vorbefuelltes Edit-Modal
- **Loeschen**: X-Icon mit Bestaetigung per window.confirm
- **Waehrungen**: EUR, USD, CHF, GBP auswaehlbar
- **Fehlerbehandlung**: API-Fehler werden im Modal angezeigt

### TypeScript-Check

`npx tsc --noEmit` — 0 Fehler

---

## 2026-03-12 | Plattform-Admin: Briefing — Vertragsdokumente (Datei-Upload)

### Anforderung

An jedem Vertrag soll eine oder mehrere Dateien (primär PDFs) hochgeladen werden koennen — z.B. das unterschriebene Original-Dokument, Anlagen oder Nachtraege.

### Neue DB-Tabelle: `contract_files`

```sql
CREATE TABLE app_crm.contract_files (
  id               UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id        UUID NOT NULL,
  contract_id      UUID NOT NULL REFERENCES app_crm.contracts(id) ON DELETE CASCADE,
  original_name    VARCHAR(500) NOT NULL,
  storage_path     VARCHAR(1000) NOT NULL,  -- relativer Pfad auf Disk
  mime_type        VARCHAR(200) NOT NULL,
  size             INTEGER NOT NULL,         -- Bytes
  uploaded_by      UUID NOT NULL,
  created_at       TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX ON app_crm.contract_files(contract_id);
```

**Prisma-Modell (in `crm.schema.prisma` erganzen):**

```prisma
model ContractFile {
  id           String   @id @default(uuid())
  tenantId     String   @map("tenant_id") @db.Uuid
  contractId   String   @map("contract_id") @db.Uuid
  originalName String   @map("original_name") @db.VarChar(500)
  storagePath  String   @map("storage_path") @db.VarChar(1000)
  mimeType     String   @map("mime_type") @db.VarChar(200)
  size         Int
  uploadedBy   String   @map("uploaded_by") @db.Uuid
  createdAt    DateTime @default(now()) @map("created_at")
  contract     Contract @relation(fields: [contractId], references: [id], onDelete: Cascade)

  @@map("contract_files")
  @@schema("app_crm")
}
```

**Rueckwaertsrelation im Contract-Modell erganzen:**

```prisma
model Contract {
  // ... bestehende Felder ...
  files ContractFile[]
}
```

### Datei-Speicherung

- Ablageort: `/app/uploads/contracts/{tenantId}/{contractId}/{uuid}-{originalName}`
- Docker-Volume: `./uploads:/app/uploads` (im docker-compose.crm.yml erganzen)
- Verzeichnis wird automatisch beim ersten Upload erstellt (`fs.mkdirSync(..., { recursive: true })`)
- Dateiname auf Disk: `{uuid}-{originalName}` (UUID-Prefix verhindert Kollisionen und path-traversal)

### Erlaubte Dateitypen und Groessen

| Typ | MIME | Max |
|-----|------|-----|
| PDF | application/pdf | 25 MB |
| Word | application/msword, .../wordprocessingml.document | 25 MB |
| Excel | application/vnd.ms-excel, .../spreadsheetml.sheet | 25 MB |

- Multer-Validierung: `fileFilter` + `limits: { fileSize: 26_214_400 }` (25 MB)
- Max. Dateien pro Vertrag: 10

### Neue Endpoints (4)

| Methode | Pfad | Beschreibung |
|---------|------|-------------|
| `POST` | `/api/v1/crm/companies/:companyId/contracts/:contractId/files` | Datei hochladen (multipart/form-data, field: `file`) |
| `GET` | `/api/v1/crm/companies/:companyId/contracts/:contractId/files` | Datei-Liste abrufen |
| `GET` | `/api/v1/crm/companies/:companyId/contracts/:contractId/files/:fileId/download` | Datei herunterladen / anzeigen |
| `DELETE` | `/api/v1/crm/companies/:companyId/contracts/:contractId/files/:fileId` | Datei loeschen |

#### POST /files — Upload

- Multer `FileInterceptor('file')`
- Validierung: Dateityp + Groesse
- Speichern auf Disk
- ContractFile-Record in DB anlegen
- **Sicherheit:** tenantId aus JWT, nicht aus Request-Body!
- Tenant-Pruefung: Contract muss zum Tenant gehoeren

**Response:**
```json
{
  "success": true,
  "data": {
    "id": "uuid",
    "contractId": "uuid",
    "originalName": "Wartungsvertrag_2026.pdf",
    "mimeType": "application/pdf",
    "size": 245678,
    "uploadedBy": "uuid",
    "createdAt": "2026-03-12T..."
  }
}
```

#### GET /files — Liste

**Response:**
```json
{
  "success": true,
  "data": [
    {
      "id": "uuid",
      "contractId": "uuid",
      "originalName": "Wartungsvertrag_2026.pdf",
      "mimeType": "application/pdf",
      "size": 245678,
      "uploadedBy": "uuid",
      "createdAt": "2026-03-12T..."
    }
  ]
}
```

#### GET /files/:fileId/download — Herunterladen / Inline anzeigen

- Query-Parameter: `?inline=true` (optional) — setzt `Content-Disposition: inline` statt `attachment`
- Default: `Content-Disposition: attachment; filename="Wartungsvertrag_2026.pdf"`
- Content-Type: aus DB (`mimeType`)
- Implementierung: `@Res() res: Response`, `res.sendFile(absolutePath)` oder NestJS `StreamableFile`
- **Sicherheit:** Pruefung dass fileId zum tenantId gehoert, KEIN direktes Auslesen von `storagePath` aus Request

#### DELETE /files/:fileId — Loeschen

- DB-Record loeschen
- Datei auf Disk loeschen (`fs.unlink`)
- Falls Datei auf Disk nicht gefunden: trotzdem DB-Record loeschen (kein Fehler)

### Sicherheits-Hinweise

1. **Path Traversal**: `originalName` NIEMALS direkt als Pfad verwenden. Immer UUID-Prefix auf Disk.
2. **Tenant-Isolation**: Jeder File-Zugriff muss `tenantId` aus JWT gegen `contract.tenantId` pruefen.
3. **Upload-Limit**: Max 10 Dateien pro Vertrag — pruefe Anzahl vor Upload.
4. **DSGVO**: Dateien werden beim Loeschen des Vertrags automatisch entfernt (Cascade in DB + Cron-Job oder onDelete-Hook fuer Disk-Cleanup).

### Modulstruktur

- Kein eigenes Modul noetig — in `ContractsModule` (`src/contracts/`) integrieren:
  - `contracts.controller.ts` — neue Routen erganzen
  - `contracts.service.ts` — `uploadFile`, `listFiles`, `downloadFile`, `deleteFile` Methoden
  - `multer` bereits als `@nestjs/platform-express` Peer-Dependency vorhanden

### Migration

```
prisma/migrations/20260312_contract_files/migration.sql
```

Nach Migration: `docker compose ... up -d crm --force-recreate -V`

Und im docker-compose.crm.yml:
```yaml
volumes:
  - ./uploads:/app/uploads
```

### TODO fuer Frontend

Nach Implementierung der 4 Endpoints:

- [ ] `ContractFile`-Interface in `types.ts`
- [ ] `contractFilesApi` in `api.ts` (upload, list, download als Blob, delete)
- [ ] `useContractFiles`, `useUploadContractFile`, `useDeleteContractFile` in `hooks.ts`
- [ ] `ContractsCard.tsx` — Dateien-Sektion im Edit-Modal: Upload-Button, Dateiliste mit Download + Loeschen

**Hinweis:** Das Frontend kann vorab gegen den definierten API-Contract gebaut werden. Sobald die Backend-Endpoints live sind, ist die Integration sofort funktional.

---

### 2026-03-12 | Backend: Vertragsdokumente (Contract Files) — Fertiggestellt

**Neue/geaenderte Dateien:**

| Datei | Aenderung |
|-------|-----------|
| `prisma/crm.schema.prisma` | ContractFile Model + files-Relation auf Contract |
| `prisma/migrations/20260312_contract_files/migration.sql` | contract_files Tabelle + Index |
| `src/contracts/contracts.service.ts` | +uploadFile, listFiles, getFile, deleteFile; remove() loescht nun auch Dateien auf Disk |
| `src/contracts/contracts.controller.ts` | +4 File-Endpoints (Upload, Liste, Download, Delete) |
| `Summarize.md` | ContractFile dokumentiert |

**Endpoints:**

| Methode | Pfad | Beschreibung |
|---------|------|-------------|
| POST | /api/v1/crm/companies/:id/contracts/:cid/files | Datei hochladen (multipart, field: `file`) |
| GET | /api/v1/crm/companies/:id/contracts/:cid/files | Datei-Liste |
| GET | /api/v1/crm/companies/:id/contracts/:cid/files/:fid/download | Download (?inline=true fuer Inline-Anzeige) |
| DELETE | /api/v1/crm/companies/:id/contracts/:cid/files/:fid | Datei loeschen (DB + Disk) |

**Sicherheit:**
- Tenant-Isolation: tenantId aus JWT, nicht aus Request
- Path-Traversal-Schutz: UUID-Prefix auf Disk, `path.basename()` fuer Dateinamen
- MIME-Filter: nur PDF, Word, Excel erlaubt
- Max 25 MB pro Datei, max 10 Dateien pro Vertrag
- Cascade: Vertrag loeschen → Dateien auf Disk + DB werden entfernt

**Speicherort:** `/app/uploads/contracts/{tenantId}/{contractId}/{uuid}-{originalName}`

**Docker-Volume:** `./uploads:/app/uploads` in docker-compose.crm.yml ergaenzen

**TypeScript-Check:** 0 Fehler

