docs: add Phase 2 CRM backend briefing with Custom Fields, Import, Forecast, Enrichment

Adds comprehensive Phase 2 briefing for CRM backend expert:
- 2.1 Custom Fields System (schema, endpoints, DTOs, response format)
- 2.2 Contact Import (CSV, Excel, vCard with preview/mapping/execute flow)
- 2.3 Forecast Endpoint (weighted pipeline value + probability field)
- 2.4 Data Enrichment (Unternehmensregister + North Data API)
- 2.5 Permissions model (visibility filters, deferred priority)
- Phase 1 frontend integration status documentation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Thomas Reitz 2026-03-12 16:12:56 +01:00
parent 48df3c3144
commit f2c8444050

View file

@ -1772,4 +1772,424 @@ Siehe Plan-Datei fuer komplette Dateiliste. Wichtigste neue Dateien:
---
## 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`*