INSIGHT-MVP/docs/INSIGHT-CRM.md
Thomas Reitz bfe672ec96 feat(crm): Vertragsdokumente — Datei-Upload Frontend + Backend-Briefing
Frontend (gegen API-Contract, bereit fuer Backend-Integration):
- ContractFile Interface in types.ts
- contractFilesApi (upload, list, download-as-blob, delete) in api.ts
- useContractFiles, useUploadContractFile, useDeleteContractFile in hooks.ts
- ContractsCard: Dokumente-Sektion im Edit-Modal
  - Dateiliste mit Icon (PDF/Word/Excel), Name, Groesse, Download, Loeschen
  - Upload-Button (PDF/DOC/DOCX/XLSX/XLS, max 25 MB)
  - Client-seitige Groessenvalidierung
  - Blob-Download via Axios (Auth-Header werden mitgesendet)

Backend-Briefing in INSIGHT-CRM.md:
- contract_files Tabelle + Prisma-Modell
- Datei-Speicherung (/app/uploads/contracts/{tenantId}/{contractId}/{uuid}-{name})
- 4 Endpoints (POST/GET/GET-download/DELETE)
- Sicherheitshinweise (Path Traversal, Tenant-Isolation, DSGVO)
- Docker-Volume Konfiguration

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 21:46:32 +01:00

3439 lines
134 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.1B.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.001.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 (0100% 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.