INSIGHT-MVP/docs/INSIGHT-CRM.md
Thomas Reitz 48df3c3144 feat(crm): Phase 1 backend schema expansion + frontend integration
Backend (CRM-Expert Phase 1):
- New enums: ContactSource, EntityStatus, CompanySize, OwnerRole,
  LostReason, EmailType, PhoneType
- Contact: add linkedinUrl, birthday, source, department, status
- Company: add vatId, taxId, tradeRegisterNumber, registerCourt,
  companySize, deliveryAddress, dataEnrichedAt/Source, status
- Deal: add lostReason + lostReasonText (required when status=LOST)
- Multi-value emails/phones tables (contact_emails, contact_phones)
- Owner m:n model (contact_owners, company_owners, deal_owners)
- Redis Pub/Sub CRM events (crm.contact.created, crm.deal.won, etc.)
- Activity due_soon scheduler (cron every 15 min)
- SQL migration with data migration for existing records

Frontend integration:
- types.ts: all new enums, interfaces, label maps
- api.ts: owner CRUD endpoints (add/remove for contacts/companies/deals)
- hooks.ts: 6 new owner mutation hooks
- ContactFormModal: LinkedIn, birthday, source, department, status fields
- ContactDetailPage: display new fields (LinkedIn, department, birthday,
  source, status badge)
- CompanyDetailPage: display vatId, taxId, trade register, company size,
  delivery address, data enrichment info
- DealFormModal: lost reason dropdown + text (shown when status=LOST)
- DealDetailPage: display lost reason with label
- CompaniesPage: EntityStatus-aware status dots (ACTIVE/INACTIVE/BLOCKED)
- ActivityType: add FOLLOWUP to all label maps

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 15:56:41 +01:00

1775 lines
76 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)
---
*Bitte neue Eintraege unten anfuegen. Format: `## YYYY-MM-DD | Absender: Betreff`*