diff --git a/docs/INSIGHT-CRM.md b/docs/INSIGHT-CRM.md index cf5a44b..79a339e 100644 --- a/docs/INSIGHT-CRM.md +++ b/docs/INSIGHT-CRM.md @@ -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`*