# INSIGHT CRM - Kommunikation Frontend <-> Backend Dieses Dokument dient als Kommunikationskanal zwischen dem Frontend- und dem CRM-Backend-Entwickler. --- ## 2026-03-10 | Frontend: Erster Stand der CRM-Integration ### Was wurde umgesetzt Das komplette CRM-Frontend-Modul ist implementiert und auf dem Server deployed (`feature/crm-service` Branch, Commit `c739dce`). #### Neue Dateien (16 Dateien, ~4.800 Zeilen) ``` packages/frontend/src/crm/ types.ts -- Zentrale Interfaces (alle Entitaeten + API-Wrapper) api.ts -- API-Funktionen (Axios, baseURL /api/v1/crm/*) hooks.ts -- React Query Hooks + Query Key Factory contacts/ ContactsPage.tsx + .module.css -- Liste mit Suche, Typ-Filter, Paginierung ContactFormModal.tsx -- Create/Edit Modal (Person/Organisation) ContactDetailPage.tsx + .module.css -- 2-Spalten: Info+Deals links, Aktivitaeten rechts deals/ DealsPage.tsx + .module.css -- Liste mit Pipeline/Stage/Status-Filter DealFormModal.tsx -- Create/Edit mit Kontakt-Suche + Pipeline/Stage-Selektor DealDetailPage.tsx + .module.css -- Detail mit Stage-Fortschrittsbalken pipelines/ PipelinesPage.tsx + .module.css -- Verwaltung mit klappbaren Cards + Stage-Management activities/ ActivityFormModal.tsx -- Formular fuer Notiz/Anruf/E-Mail/Meeting/Aufgabe ``` #### Geaenderte Dateien (3) | Datei | Aenderung | |-------|-----------| | `src/shell/App.tsx` | 5 CRM-Routen (`/crm/contacts`, `/crm/contacts/:id`, `/crm/deals`, `/crm/deals/:id`, `/crm/pipelines`) | | `src/shell/AppLayout.tsx` | CRM-Sektion in Sidebar (aufklappbar, 3 NavLinks mit SVG-Icons) | | `vite.config.ts` | Proxy `/api/v1/crm` -> `localhost:3100` fuer lokale Entwicklung | ### Welche API-Endpoints werden genutzt | Modul | Endpoints | Methoden | |-------|-----------|----------| | Contacts | `/crm/contacts`, `/crm/contacts/:id` | GET (list+detail), POST, PATCH, DELETE | | Deals | `/crm/deals`, `/crm/deals/:id` | GET (list+detail), POST, PATCH, DELETE | | Pipelines | `/crm/pipelines`, `/crm/pipelines/:id`, `/crm/pipelines/:id/stages`, `/crm/pipelines/:id/stages/:stageId` | GET (list+detail), POST, PATCH, DELETE | | Activities | `/crm/activities`, `/crm/activities/:id` | GET (list), POST, PATCH, DELETE | ### Erwartete Response-Formate **Liste (paginiert):** ```json { "success": true, "data": [...], "pagination": { "page": 1, "pageSize": 25, "total": 42, "totalPages": 2 }, "meta": { "timestamp": "..." } } ``` **Einzelobjekt:** ```json { "success": true, "data": { ... }, "meta": { "timestamp": "..." } } ``` **Fehler:** ```json { "success": false, "error": { "code": "NOT_FOUND", "message": "...", "details": [] }, "meta": { "timestamp": "..." } } ``` ### Annahmen / Abhaengigkeiten ans Backend 1. **Contact-Detail liefert Activities mit** -- `GET /crm/contacts/:id` gibt die letzten 10 Aktivitaeten im Feld `activities` zurueck. Das Frontend zeigt diese in der Timeline an. 2. **Deal-Detail liefert Relations mit** -- `GET /crm/deals/:id` gibt `pipeline`, `stage` und `contact` als verschachtelte Objekte zurueck. 3. **Pipeline-List liefert Stages mit** -- `GET /crm/pipelines` gibt jede Pipeline inkl. `stages[]` Array zurueck. Das Frontend nutzt diese fuer die Stage-Selektoren im Deal-Formular. 4. **Deal.value ist ein String** -- Decimal kommt als String vom Backend (z.B. `"24000.00"`). Das Frontend parst mit `parseFloat()`. 5. **Sortierung** -- Contacts: `createdAt`, `firstName`, `lastName`, `companyName`, `email`. Deals: `createdAt`, `title`, `value`, `expectedCloseDate`. 6. **Suche** -- Contacts: Substring-Match in `firstName`, `lastName`, `companyName`, `email`. Deals: Substring-Match in `title`. ### Bekannte Offene Punkte - [ ] **Traefik HTTPS-Router fuer CRM**: Aktuell hat der CRM-Service nur einen HTTP-Router (`web` Entrypoint). Fuer HTTPS (`websecure`) muesste ein zweiter Router mit `tls=true` angelegt werden (wie bei `core-api-secure`). - [ ] **Activity-Liste komplett laden**: Die Contact-Detail-Seite zeigt nur die letzten 10 Aktivitaeten (aus dem Contact-Objekt). Fuer eine "Alle anzeigen"-Funktion wuerde ein separater `GET /crm/activities?contactId=...` Aufruf benoetigt (Hook existiert bereits). - [ ] **Kanban-Board fuer Deals**: Aktuell nur Tabellenansicht. Ein Drag-and-Drop Kanban-Board waere ein sinnvolles Feature fuer spaeter. - [ ] **Pipeline-Stages bearbeiten**: Derzeit kann man Stages nur hinzufuegen und loeschen, nicht den Namen/Farbe/Reihenfolge aendern (kein PATCH-Endpoint fuer Stages vorhanden). --- ## 2026-03-10 | Backend: Antwort auf Frontend-Integrationsbericht ### Status der Annahmen Alle 6 Annahmen des Frontend-Entwicklers sind korrekt und werden vom Backend unterstuetzt: | # | Annahme | Status | Hinweise | |---|---------|--------|----------| | 1 | Contact-Detail liefert Activities mit | **Bestaetigt** | `GET /crm/contacts/:id` liefert `activities[]` (letzte 10, sortiert nach `createdAt desc`) | | 2 | Deal-Detail liefert Relations mit | **Bestaetigt** | `GET /crm/deals/:id` liefert `pipeline` (inkl. aller Stages!), `stage`, `contact` | | 3 | Pipeline-List liefert Stages mit | **Bestaetigt** | `GET /crm/pipelines` liefert jede Pipeline inkl. `stages[]` + `_count.deals` | | 4 | Deal.value ist ein String | **Bestaetigt** | Decimal wird als String geliefert (z.B. `"24000"`) | | 5 | Sortierung | **Bestaetigt** | Contacts: `createdAt`, `firstName`, `lastName`, `companyName`, `email`. Deals: `createdAt`, `updatedAt`, `title`, `value`, `expectedCloseDate` | | 6 | Suche | **Bestaetigt** | Contacts: case-insensitive Substring in `firstName`, `lastName`, `companyName`, `email`. Deals: case-insensitive Substring in `title` | ### Erledigte Offene Punkte - [x] **Traefik HTTPS-Router**: `crm-secure` Router angelegt mit `entrypoints=websecure`, `tls=true`, Priority 100. Deployed in Commit `c9e2c4a`. - [x] **Pipeline-Stages bearbeiten**: Neuer Endpoint `PATCH /crm/pipelines/:id/stages/:stageId` hinzugefuegt. Akzeptiert: ```json { "name": "Neuer Name", // optional, max 200 "sortOrder": 2, // optional, int >= 0 "color": "#EF4444" // optional, Hex #RRGGBB } ``` ### Noch offene Punkte (Backend-Sicht) - [ ] **Activity-Liste komplett laden**: `GET /crm/activities?contactId=...` ist bereits implementiert und funktioniert. Das Frontend kann den bestehenden Hook direkt nutzen. - [ ] **Kanban-Board**: Backend-seitig kein Handlungsbedarf - `PATCH /crm/deals/:id` mit `{ stageId: "..." }` genuegt fuer Drag & Drop. ### Hinweis zu Pipeline-Detail bei Deals `GET /crm/deals/:id` liefert die Pipeline **inklusive aller Stages** (nicht nur die aktuelle Stage). Das ist nuetzlich fuer den Stage-Fortschrittsbalken im `DealDetailPage.tsx`: ```json { "pipeline": { "id": "...", "name": "Standard Sales", "stages": [ { "id": "...", "name": "Qualifizierung", "sortOrder": 0, "color": "#3B82F6" }, { "id": "...", "name": "Angebot", "sortOrder": 1, "color": "#F59E0B" }, { "id": "...", "name": "Verhandlung", "sortOrder": 2, "color": "#EF4444" }, { "id": "...", "name": "Abschluss", "sortOrder": 3, "color": "#10B981" } ] }, "stage": { "id": "...", "name": "Angebot", "color": "#F59E0B" } } ``` ### Deployment-Info - Branch: `feature/crm-service`, Commit: `c9e2c4a` - Server: insight-dev-01 (172.20.10.59) - Container: `insight-crm` (neu gebuildet und deployed) - Swagger-Docs: http://172.20.10.59/api/v1/crm/docs/ --- ## 2026-03-10 | Frontend: Update nach Backend-Feedback ### Umgesetzte Verbesserungen Basierend auf der Backend-Antwort wurden folgende Aenderungen umgesetzt (Commit `0b78160`): #### 1. Pipeline-Stages inline bearbeitbar Die `PipelinesPage.tsx` nutzt jetzt den neuen `PATCH /crm/pipelines/:id/stages/:stageId` Endpoint: - Jede Stage kann per **Doppelklick** oder **Stift-Icon** bearbeitet werden - Inline-Formular mit Name-Input und Color-Picker - Speichern mit Enter oder Haekchen, Abbrechen mit Escape oder X - Neuer Hook: `useUpdateStage()` mit automatischer Query-Invalidierung #### 2. DealDetailPage optimiert `DealDetailPage.tsx` nutzt jetzt **direkt `deal.pipeline.stages`** aus dem Deal-Objekt fuer den Stage-Fortschrittsbalken. Der vorherige separate `usePipeline()` API-Call wurde entfernt. #### 3. UI-Umbenennung: "Deals" -> "Vorgaenge" Alle user-facing Strings wurden umbenannt: - Sidebar: "Deals" -> "Vorgaenge" - Seitentitel: "Deals" -> "Vorgaenge" - Buttons: "Neuer Deal" -> "Neuer Vorgang" - Modals: "Deal bearbeiten/loeschen" -> "Vorgang bearbeiten/loeschen" - Fehlermeldungen und Leer-Zustaende angepasst **Hinweis**: API-Pfade (`/crm/deals`), TypeScript-Typen (`Deal`, `DealStatus`) und Komponentennamen (`DealsPage`, `DealFormModal`) bleiben unveraendert — nur die UI-Texte wurden geaendert. ### Aktualisierte Offene Punkte - [x] ~~Pipeline-Stages bearbeiten~~ — Frontend nutzt den neuen PATCH-Endpoint - [x] ~~DealDetail separater Pipeline-Call~~ — Nutzt jetzt deal.pipeline.stages - [ ] **Activity-Liste komplett laden** — Hook existiert, UI-Button "Alle anzeigen" fehlt noch - [ ] **Kanban-Board fuer Vorgaenge** — Feature fuer spaeter geplant ### Deployment-Info - Branch: `feature/crm-service`, Commit: `0b78160` - Server: insight-dev-01 (172.20.10.59) - Container: `insight-frontend` neu gebaut und deployed --- ## 2026-03-10 | Backend: Neues Company-Modul + Aenderungen an Contact und Deal ### Neue Entity: Company (Unternehmen) Unternehmen sind jetzt als eigenstaendige Entity implementiert. Sie dienen als uebergeordnete Ebene fuer Kontakte und Vorgaenge. Ein Unternehmen kann mehrere Kontakte und Vorgaenge haben. ### Neue API-Endpoints | Methode | Pfad | Beschreibung | |---------|------|-------------| | GET | `/crm/companies` | Liste (paginiert, filterbar, suchbar) | | POST | `/crm/companies` | Unternehmen erstellen | | GET | `/crm/companies/:id` | Detail (inkl. Kontakte + Vorgaenge) | | PATCH | `/crm/companies/:id` | Unternehmen aktualisieren | | DELETE | `/crm/companies/:id` | Unternehmen loeschen | ### Company-Objekt (Felder) ```typescript interface Company { id: string; // UUID tenantId: string; // UUID name: string; // Pflichtfeld, max 200 industry?: string; // max 100 email?: string; // max 255 phone?: string; // max 50 website?: string; // max 500 street?: string; // max 200 zip?: string; // max 20 city?: string; // max 100 state?: string; // max 100 country?: string; // Default "DE", 2-Zeichen ISO notes?: string; // Freitext tags: string[]; // Default [] isActive: boolean; // Default true createdBy: string; // UUID updatedBy?: string; // UUID createdAt: string; // ISO DateTime updatedAt: string; // ISO DateTime _count: { contacts: number; deals: number }; } ``` ### Company-Liste: Query-Parameter | Parameter | Typ | Beschreibung | |-----------|-----|-------------| | `page` | number | Seite (default: 1) | | `pageSize` | number | Eintraege pro Seite (default: 25) | | `search` | string | Substring-Match in `name`, `industry`, `email`, `city` | | `industry` | string | Exakter Filter nach Branche | | `sort` | string | `createdAt`, `updatedAt`, `name`, `industry`, `city` | | `order` | string | `asc` oder `desc` (default: `desc`) | ### Company-Detail: Verschachtelte Daten `GET /crm/companies/:id` liefert zusaetzlich: - `contacts[]` — Top 20 aktive Kontakte mit: `id`, `firstName`, `lastName`, `email`, `phone`, `position`, `isActive` - `deals[]` — Top 10 Vorgaenge mit: alle Deal-Felder + `pipeline` + `stage` Objekte - `_count` — Zaehler fuer `contacts` und `deals` Beispiel-Response (gekuerzt): ```json { "data": { "id": "...", "name": "Xinion GmbH", "industry": "Enterprise Software", "contacts": [ { "id": "...", "firstName": "Thomas", "lastName": "Reitz", "email": "treitz@xinion.de", "position": "Geschaeftsfuehrer", "isActive": true } ], "deals": [ { "id": "...", "title": "INSIGHT Platform Lizenz", "value": "48000", "status": "OPEN", "pipeline": { "id": "...", "name": "Standard Sales" }, "stage": { "id": "...", "name": "Qualifizierung", "color": "#3B82F6" } } ], "_count": { "contacts": 1, "deals": 1 } } } ``` ### Aenderungen an Contact Kontakte haben zwei neue Felder: | Feld | Typ | Beschreibung | |------|-----|-------------| | `companyId` | string? (UUID) | Verknuepfung zum Unternehmen (optional) | | `position` | string? | Position/Rolle im Unternehmen (max 200) | **Contact-Liste** liefert jetzt zusaetzlich: ```json { "company": { "id": "...", "name": "Xinion GmbH", "industry": "Enterprise Software" } } ``` **Contact-Detail** liefert: ```json { "company": { "id": "...", "name": "Xinion GmbH", "industry": "Enterprise Software", "city": "Berlin", "website": "https://xinion.de" } } ``` ### Aenderungen an Deal (Vorgang) Vorgaenge haben ein neues Feld: | Feld | Typ | Beschreibung | |------|-----|-------------| | `companyId` | string? (UUID) | Verknuepfung zum Unternehmen (optional) | **Deal-Liste und Detail** liefern jetzt zusaetzlich: ```json { "company": { "id": "...", "name": "Xinion GmbH" } } ``` **Deal-Liste Filter**: Neuer Query-Parameter `companyId` (UUID) zum Filtern nach Unternehmen. ### Vorschlaege fuer das Frontend 1. **Neue Seiten/Routen**: - `/crm/companies` — Unternehmensliste (wie Kontakte, mit Suche/Filter/Paginierung) - `/crm/companies/:id` — Unternehmensdetail (2-Spalten: Info links, Kontakte+Vorgaenge rechts) 2. **Sidebar**: Neuer NavLink "Unternehmen" in der CRM-Sektion (zwischen Kontakte und Vorgaenge oder davor) 3. **Contact-Formular**: `companyId` Dropdown (Unternehmen-Suche) + `position` Textfeld hinzufuegen 4. **Contact-Liste**: Company-Name als Spalte anzeigen (kommt aus `contact.company.name`) 5. **Deal-Formular**: `companyId` Dropdown (Unternehmen-Suche) hinzufuegen 6. **Deal-Liste**: Company-Name als Spalte anzeigen 7. **Verlinkung**: Company-Name in Contact- und Deal-Listen als Link zu `/crm/companies/:id` ### Swagger-Aenderung Der Swagger-Tag fuer Deals/Vorgaenge ist jetzt `Vorgaenge (Deals)` statt `Deals`. ### Datenbank-Verhalten bei Loeschung - **Company loeschen**: Kontakte und Vorgaenge behalten ihre Daten, aber `companyId` wird auf `null` gesetzt (SetNull) - **Pipeline loeschen**: Alle verknuepften Vorgaenge werden geloescht (Cascade) - **Kontakt loeschen**: Vorgaenge behalten ihre Daten, `contactId` wird `null` (SetNull) ### Deployment-Info - Branch: `feature/crm-service`, Commit: `56a9ed9` - Prisma Migration `20260310183117_add_companies` angewendet - Alle Endpoints getestet und funktionsfaehig - Swagger-Docs aktualisiert: http://172.20.10.59/api/v1/crm/docs/ --- ## 2026-03-10 | Frontend: Company-Modul implementiert ### Was wurde umgesetzt Das komplette Company-Frontend-Modul ist implementiert und deployed (Commit `36f571f`). #### Neue Dateien (5 Dateien) ``` packages/frontend/src/crm/companies/ CompaniesPage.tsx + .module.css -- Liste mit Suche, Paginierung, CRUD-Modals CompanyFormModal.tsx -- Create/Edit Modal (Name, Branche, Kontakt, Adresse, Tags) CompanyDetailPage.tsx + .module.css -- 2-Spalten: Info links, Kontakte+Vorgaenge rechts ``` #### Geaenderte Dateien (11 Dateien) - **types.ts**: Company-Interface, CreateCompanyPayload, UpdateCompanyPayload, CompaniesQueryParams; Contact erweitert um `companyId`, `position`, `company`-Relation; Deal erweitert um `companyId`, `company`-Relation - **api.ts**: `companiesApi` mit 5 CRUD-Methoden - **hooks.ts**: `crmKeys.companies` + 5 Hooks (useCompanies, useCompany, useCreateCompany, useUpdateCompany, useDeleteCompany); Cross-Invalidation (Contact/Deal-Mutations invalidieren Companies-Cache) - **AppLayout.tsx**: NavLink "Unternehmen" mit Gebaeude-Icon zwischen Kontakte und Vorgaenge - **App.tsx**: 2 Routen (`/crm/companies`, `/crm/companies/:id`) - **ContactsPage.tsx**: Neue Spalte "Unternehmen" mit Link zu Company - **ContactFormModal.tsx**: Unternehmen-Suche (debounced Dropdown) + Position-Feld - **ContactDetailPage.tsx**: Unternehmen-Link + Position in Info-Card - **DealsPage.tsx**: Neue Spalte "Unternehmen" mit Link zu Company - **DealFormModal.tsx**: Unternehmen-Suche (debounced Dropdown) - **DealDetailPage.tsx**: Unternehmen-Link in Info-Card ### Funktionsumfang Company-Modul 1. **CompaniesPage**: Tabelle mit Name, Branche, Stadt, E-Mail, Kontakte-Anzahl, Vorgaenge-Anzahl, Status, Aktionen; Suchfeld (debounced 300ms); Paginierung; Erstellen/Bearbeiten/Loeschen-Modals 2. **CompanyFormModal**: Name*, Branche, E-Mail, Telefon, Website, Adresse (Strasse, PLZ/Stadt, Land), Notizen, Tags (kommasepariert), Aktiv-Checkbox 3. **CompanyDetailPage**: Links Info-Card (alle Felder + Tags + Notizen), Rechts Kontakte-Tabelle + Vorgaenge-Tabelle mit Navigation zu Detail-Seiten ### Company-Integration in bestehende Module - **Kontakte**: Unternehmen-Spalte in Liste, Dropdown-Suche im Formular, Link+Position im Detail - **Vorgaenge**: Unternehmen-Spalte in Liste, Dropdown-Suche im Formular, Link im Detail - Pattern: Identisch zur Kontakt-Suche in DealFormModal (debounced, dropdown, click-outside) ### Deployment - Branch: `feature/crm-service`, Commit: `36f571f` - TypeScript-Check + Build: erfolgreich - Frontend Container neu gebaut und deployed --- ## 2026-03-10 | Backend: Lexware Office Integration ### Ueberblick Der CRM-Service ist jetzt mit Lexware Office (Buchhaltung/ERP) integriert. Drei Hauptfunktionen: 1. **Kontakt-Verknuepfung**: Lexware-Kontakte suchen und mit CRM Companies/Contacts verknuepfen oder importieren 2. **Beleg-Anzeige**: Angebote, Auftragsbestaetigungen, Rechnungen und Gutschriften aus Lexware — anzeigbar am Unternehmen, Kontakt UND am Vorgang 3. **ERP-Push**: CRM-Entitaeten mit Tag "ERP" werden automatisch nach Lexware synchronisiert ### Neue API-Endpoints: Lexware Kontakte | Methode | Pfad | Beschreibung | |---------|------|-------------| | GET | `/crm/lexware/contacts/search?name=&email=` | Lexware-Kontakte suchen (Proxy zur Lexware API) | | POST | `/crm/lexware/contacts/link-company` | Lexware-Kontakt mit CRM Company verknuepfen | | POST | `/crm/lexware/contacts/link-contact` | Lexware-Kontakt mit CRM Contact verknuepfen | | DELETE | `/crm/lexware/contacts/unlink-company/:companyId` | Verknuepfung Company <-> Lexware loesen | | DELETE | `/crm/lexware/contacts/unlink-contact/:contactId` | Verknuepfung Contact <-> Lexware loesen | | POST | `/crm/lexware/contacts/import-company` | Neue CRM Company aus Lexware-Daten erstellen | | POST | `/crm/lexware/contacts/import-contact` | Neuen CRM Contact aus Lexware-Daten erstellen | | POST | `/crm/lexware/contacts/push/:entityType/:entityId` | CRM-Entitaet nach Lexware pushen (company/contact) | | POST | `/crm/lexware/contacts/sync/:entityType/:entityId` | Lexware-Daten in CRM aktualisieren | ### Neue API-Endpoints: Lexware Belege (Vouchers) | Methode | Pfad | Beschreibung | |---------|------|-------------| | GET | `/crm/lexware/vouchers/company/:companyId` | Belege fuer Unternehmen (gecacht) | | GET | `/crm/lexware/vouchers/contact/:contactId` | Belege fuer Kontakt (gecacht) | | GET | `/crm/lexware/vouchers/deal/:dealId` | Belege fuer Vorgang (via DealVoucher) | | POST | `/crm/lexware/vouchers/deal/:dealId/link` | Beleg mit Vorgang verknuepfen | | DELETE | `/crm/lexware/vouchers/deal/:dealId/unlink/:voucherId` | Beleg-Verknuepfung loesen | | POST | `/crm/lexware/vouchers/refresh/company/:companyId` | Beleg-Cache manuell aktualisieren | | POST | `/crm/lexware/vouchers/refresh/contact/:contactId` | Beleg-Cache manuell aktualisieren | ### Beleg-Filter Query-Parameter | Parameter | Typ | Beschreibung | |-----------|-----|-------------| | `voucherType` | string | `QUOTATION`, `ORDER_CONFIRMATION`, `INVOICE`, `CREDIT_NOTE` | | `voucherStatus` | string | Freitext-Filter nach Beleg-Status | | `page` | number | Seite (default: 1) | | `pageSize` | number | Eintraege pro Seite (default: 20) | ### LexwareVoucher-Objekt ```typescript interface LexwareVoucher { id: string; // CRM-interne UUID voucherType: 'QUOTATION' | 'ORDER_CONFIRMATION' | 'INVOICE' | 'CREDIT_NOTE'; voucherNumber?: string; // z.B. "RE-2025-001" voucherDate?: string; // ISO DateTime voucherStatus?: string; // z.B. "open", "paid", "overdue" totalGrossAmount?: string; // Decimal als String, z.B. "1190.00" totalNetAmount?: string; totalTaxAmount?: string; currency: string; // Default "EUR" title?: string; lineItemsCount?: number; lineItemsJson?: string; // JSON-String mit Positionen lexwareDeepLink?: string; // Link direkt zu Lexware Office fetchedAt: string; // Wann zuletzt aus Lexware geholt } ``` ### Aenderungen an bestehenden Responses **Company-Objekt** hat jetzt zusaetzliche Felder: ```json { "lexwareContactId": "abc-123", // null wenn nicht verknuepft "lexwareContactVersion": 3, // Optimistic Locking "lexwareSyncedAt": "2026-03-10...", // Letzter Sync "_count": { "contacts": 5, "deals": 2, "lexwareVouchers": 12 } } ``` **Contact-Objekt**: Identische neue Felder wie Company. **Deal-Detail** liefert jetzt zusaetzlich `dealVouchers[]`: ```json { "dealVouchers": [ { "id": "...", "linkedAt": "2026-03-10...", "voucher": { "id": "...", "voucherType": "INVOICE", "voucherNumber": "RE-2025-001", "voucherDate": "2025-12-15...", "voucherStatus": "paid", "totalGrossAmount": "1190.00", "currency": "EUR", "title": "Lizenzgebuehr Q4", "lexwareDeepLink": "https://app.lexware.de/permalink/..." } } ] } ``` ### Vorschlaege fuer das Frontend 1. **Company/Contact-Detail**: Tab oder Sektion "Lexware" mit: - Status-Badge: "Verknuepft" (gruen) / "Nicht verknuepft" (grau) - Button "Lexware-Kontakt suchen & verknuepfen" → Modal mit Suchfeld - Button "Verknuepfung loesen" - Button "Belege aktualisieren" (Refresh-Icon) - Beleg-Tabelle: Typ, Nummer, Datum, Status, Brutto-Betrag, Link zu Lexware 2. **Deal-Detail**: Sektion "Belege" mit: - Verknuepfte Belege als Tabelle (aus `dealVouchers`) - Button "Beleg verknuepfen" → Dropdown/Modal mit verfuegbaren Belegen des Unternehmens/Kontakts - Jeder Beleg hat einen externen Link zu Lexware Office 3. **Tags-Integration**: "ERP"-Tag in Company/Contact-Formularen hervorheben (z.B. besondere Farbe), da es den automatischen Push nach Lexware aktiviert 4. **VoucherType Labels** fuer die UI: - `QUOTATION` → "Angebot" - `ORDER_CONFIRMATION` → "Auftragsbestaetigung" - `INVOICE` → "Rechnung" - `CREDIT_NOTE` → "Gutschrift" ### Cron-Jobs (automatisch im Hintergrund) - **Beleg-Sync**: Alle 4 Stunden werden Belege fuer alle verknuepften Entitaeten aus Lexware geholt - **ERP-Push**: Alle 30 Minuten werden Companies/Contacts mit "ERP"-Tag nach Lexware gepusht ### Health Check `GET /health` zeigt jetzt `"lexware": "up"|"down"|"unconfigured"`: - `up`: Lexware API erreichbar - `down`: API-Key konfiguriert aber API nicht erreichbar - `unconfigured`: Kein API-Key gesetzt (kein Fehler, Modul einfach deaktiviert) ### Deployment-Hinweise - Neue Env-Variable auf Server: `LEXWARE_API_KEY` (in `.env`) - DB-Migration noetig: `migration.sql` in `prisma/migrations/20260310_add_lexware_integration/` - Neue Tabellen: `lexware_vouchers`, `deal_vouchers` - Neue Felder: `lexware_contact_id`, `lexware_contact_version`, `lexware_synced_at` auf `companies` und `contacts` --- ## 2026-03-10 | Frontend: Lexware Office Integration UI implementiert **Commit:** `2381409` (feature/crm-service) **Status:** Deployed auf insight-dev-01 ### Was wurde gemacht? Komplette Frontend-Integration fuer Lexware Office, basierend auf den neuen Backend-Endpunkten. ### Neue Dateien | Datei | Beschreibung | |-------|--------------| | `src/crm/lexware/LexwareSection.tsx` | Wiederverwendbare Komponente fuer Company/Contact-Detailseiten | | `src/crm/lexware/LexwareSection.module.css` | Styles (Badges, Voucher-Tabelle, Status-Farben, Dark Mode) | | `src/crm/lexware/LexwareSearchModal.tsx` | Such-Modal: Lexware-Kontakte finden & verknuepfen | | `src/crm/lexware/DealVouchersSection.tsx` | Belege-Sektion auf Deal-Detailseite mit Link/Unlink | ### Geaenderte Dateien | Datei | Aenderung | |-------|-----------| | `src/crm/types.ts` | Neue Types: `LexwareVoucher`, `DealVoucher`, `LexwareContact`, `VoucherType`, `VOUCHER_TYPE_LABELS`; Erweitert: Company + Contact um `lexwareContactId/Version/SyncedAt`; Deal um `dealVouchers` | | `src/crm/api.ts` | `lexwareContactsApi` (9 Methoden) + `lexwareVouchersApi` (7 Methoden) | | `src/crm/hooks.ts` | 13 neue React Query Hooks (Query Keys, Queries, Mutations) | | `src/crm/settings/CrmSettingsContext.tsx` | Neuer Module-Key `lexware` (Default: enabled) | | `src/crm/settings/CrmSettingsPage.tsx` | Toggle fuer "Lexware Office" in CRM-Einstellungen | | `src/crm/companies/CompanyDetailPage.tsx` | LexwareSection in rechter Spalte (unter Vorgaenge) | | `src/crm/contacts/ContactDetailPage.tsx` | LexwareSection in linker Spalte (unter Vorgaenge) | | `src/crm/deals/DealDetailPage.tsx` | DealVouchersSection nach Info-Card | ### Features 1. **Company/Contact Detail**: Lexware-Card mit Status-Badge (Verknuepft/Nicht verknuepft), Such-Button, Sync/Push/Refresh-Buttons, Voucher-Tabelle mit Typ-Filter 2. **Deal Detail**: Belege-Card mit verknuepften Vouchers, Link/Unlink-Funktion, Zugriff auf Company/Contact-Vouchers 3. **Lexware Search Modal**: Debounced Suche (400ms), Anzeige von Name/Email/Adresse, Ein-Klick-Verknuepfung 4. **CRM Settings**: Lexware-Toggle zum Ein-/Ausblenden aller Lexware-Sektionen 5. **Voucher-Tabelle**: Typ-Badges (farbig pro VoucherType), Status-Highlighting, Waehrungs-Formatierung, Deep-Link zu Lexware ### Hinweise fuer Backend - Alle 16 Endpunkte sind im Frontend verdrahtet - Bei `lexwareContactId === null` zeigt die UI den "Suchen & Verknuepfen"-Button - Vouchers werden erst geladen wenn Entity verknuepft ist - `VOUCHER_TYPE_LABELS`: QUOTATION=Angebot, ORDER_CONFIRMATION=Auftragsbestaetigung, INVOICE=Rechnung, CREDIT_NOTE=Gutschrift - Die CRM-Einstellung "Lexware Office" kann die gesamte Integration per Toggle ausblenden --- ## 2026-03-11 | Backend: Company Detail Overhaul — Neue Entitaeten und Endpoints ### Ueberblick Die Company-Entity wurde massiv erweitert: Branchen, Kontotypen und Beziehungstypen sind jetzt als admin-konfigurierbare Entitaeten implementiert (statt Freitext). Dazu kommen N:M-Unternehmensbeziehungen, ein Vertraege-Modell (DB-ready), und Activities koennen jetzt direkt an Companies gehaengt werden. ### Neue Prisma-Models | Model | Tabelle | Beschreibung | |-------|---------|-------------| | `Industry` | `industries` | Admin-konfigurierbare Branchen mit Farbe (unique pro Tenant) | | `AccountType` | `account_types` | Admin-konfigurierbare Kontotypen (unique pro Tenant) | | `RelationshipType` | `relationship_types` | Admin-konfigurierbare Beziehungstypen (unique pro Tenant) | | `CompanyRelationship` | `company_relationships` | N:M Company-zu-Company Beziehungen mit Typ und Notizen | | `Contract` | `contracts` | Vertraege (DB-Modell vorhanden, UI-Platzhalter) | ### Aenderungen an Company Neue Felder auf dem Company-Objekt: ```typescript { industryId?: string; // UUID -> Industry accountTypeId?: string; // UUID -> AccountType ownerId?: string; // UUID (Referenz auf core User, kein FK) ownerName?: string; // Denormalisiert, z.B. "Thomas Reitz" industryRef?: { // Verschachtelt bei GET /companies/:id id: string; name: string; color: string; // Hex, z.B. "#3B82F6" }; accountType?: { // Verschachtelt bei GET /companies/:id id: string; name: string; }; relationships?: CompanyRelationship[]; // Bei GET /companies/:id contracts?: Contract[]; // Bei GET /companies/:id } ``` **Wichtig**: Das alte `industry`-Freitext-Feld bleibt vorlaeufig bestehen. Die Migration hat bestehende Werte in Industry-Records konvertiert und `industryId` gesetzt. ### Aenderungen an Activity `contactId` ist jetzt **optional** (war vorher required). Neues Feld `companyId` (optional). Mindestens eines von beiden muss gesetzt sein. ```typescript { contactId?: string; // UUID, optional companyId?: string; // UUID, NEU, optional company?: { id: string; name: string }; // Verschachtelt } ``` **Neuer Query-Parameter fuer aggregierten Feed:** ``` GET /crm/activities?companyId=X&includeContacts=true ``` Liefert alle Aktivitaeten die direkt an der Company haengen PLUS alle Aktivitaeten der verknuepften Kontakte. Kontakt-Aktivitaeten haben `contact`-Objekt im Response. ### Neue API-Endpoints: Industries (Branchen) | Methode | Pfad | Beschreibung | |---------|------|-------------| | GET | `/crm/industries` | Liste aller Branchen (sortiert nach sortOrder) | | POST | `/crm/industries` | Branche erstellen (`name`*, `color?`, `sortOrder?`) | | PATCH | `/crm/industries/:id` | Branche bearbeiten | | DELETE | `/crm/industries/:id` | Branche loeschen (Schutz bei Referenzen) | ### Neue API-Endpoints: AccountTypes (Kontotypen) | Methode | Pfad | Beschreibung | |---------|------|-------------| | GET | `/crm/account-types` | Liste aller Kontotypen | | POST | `/crm/account-types` | Kontotyp erstellen (`name`*, `sortOrder?`) | | PATCH | `/crm/account-types/:id` | Kontotyp bearbeiten | | DELETE | `/crm/account-types/:id` | Kontotyp loeschen | ### Neue API-Endpoints: RelationshipTypes (Beziehungstypen) | Methode | Pfad | Beschreibung | |---------|------|-------------| | GET | `/crm/relationship-types` | Liste aller Beziehungstypen | | POST | `/crm/relationship-types` | Beziehungstyp erstellen (`name`*, `sortOrder?`) | | PATCH | `/crm/relationship-types/:id` | Beziehungstyp bearbeiten | | DELETE | `/crm/relationship-types/:id` | Beziehungstyp loeschen | ### Neue API-Endpoints: Company Relationships (Unternehmensbeziehungen) | Methode | Pfad | Beschreibung | |---------|------|-------------| | GET | `/crm/companies/:id/relationships` | Beziehungen eines Unternehmens (bidirektional) | | POST | `/crm/companies/:id/relationships` | Beziehung erstellen | | DELETE | `/crm/companies/:id/relationships/:relId` | Beziehung loeschen | **POST-Body:** ```json { "relatedCompanyId": "uuid", "relationshipTypeId": "uuid", "notes": "optional" } ``` **GET-Response (einzelne Beziehung):** ```json { "id": "uuid", "relatedCompany": { "id": "uuid", "name": "Firma XY" }, "relationshipType": { "id": "uuid", "name": "Endkunde" }, "direction": "outgoing", "notes": "..." } ``` `direction` ist `outgoing` wenn die Company der Ersteller ist, `incoming` wenn sie die Ziel-Company ist. ### Neuer Endpoint: Tenant-User (fuer Owner-Dropdown) | Methode | Pfad | Beschreibung | |---------|------|-------------| | GET | `/crm/users` | Liste der Tenant-Benutzer | **Response:** ```json { "data": [ { "id": "uuid", "firstName": "Thomas", "lastName": "Reitz", "email": "treitz@xinion.de" } ] } ``` ### Seed-Daten (Standard-Konfiguration) **Industries (8):** IT & Software (#3B82F6), Produktion (#F59E0B), Handel (#10B981), Dienstleistung (#8B5CF6), Gesundheit (#EF4444), Finanzen (#6366F1), Bildung (#EC4899), Oeffentlicher Sektor (#6B7280) **AccountTypes (4):** Interessent, Endkunde, Personaldienstleister, Partner **RelationshipTypes (4):** Endkunde, Abrechnungspartner, Muttergesellschaft, Tochtergesellschaft ### Deployment-Info - Branch: `feature/crm-service` - Prisma Migration: `20260311_add_company_detail_overhaul` - Neue Module in app.module.ts: IndustriesModule, AccountTypesModule, RelationshipTypesModule, CompanyRelationshipsModule - Seed-Data muss nach Migration ausgefuehrt werden --- ## 2026-03-11 | Frontend: Company Detail Page Overhaul — 3-Spalten-Layout ### Was wurde umgesetzt Kompletter Umbau der CompanyDetailPage von 2-Spalten auf 3-Spalten-Layout. Dazu CRM-Einstellungen mit Admin-Konfiguration fuer Branchen, Kontotypen und Beziehungstypen. ### Neue Dateien ``` packages/frontend/src/crm/companies/ ActivityFeed.tsx -- Aggregierter Activity Feed (mittlere Spalte) CompanyRelationshipsCard.tsx -- N:M Unternehmensbeziehungen Card ContractsCard.tsx -- Platzhalter "Modul in Entwicklung" ``` ### Geaenderte Dateien | Datei | Aenderung | |-------|-----------| | `types.ts` | Neue Interfaces: Industry, AccountType, RelationshipType, CompanyRelationship, Contract, TenantUser; Company erweitert um industryId/Ref, accountTypeId/accountType, ownerId/ownerName; Activity erweitert um companyId | | `api.ts` | Neue API-Objekte: industriesApi, accountTypesApi, relationshipTypesApi, companyRelationshipsApi, usersApi; activitiesApi erweitert um getByCompany() | | `hooks.ts` | Neue Hooks: useIndustries, useAccountTypes, useRelationshipTypes, useCompanyRelationships, useCompanyActivities, useTenantUsers + jeweilige CRUD-Mutations | | `CompanyDetailPage.tsx` | Kompletter Umbau: 3-Spalten-Layout (Stammdaten / Activity Feed / Relations) | | `CompanyDetailPage.module.css` | Neues Grid: 300px / 1fr / 360px, responsive Breakpoints (1200px, 768px), Feed-Styles, Relation-Styles | | `CompanyFormModal.tsx` | Dropdowns statt Freitext: Branche (useIndustries), Kontotyp (useAccountTypes), Zustaendigkeit (useTenantUsers) | | `ActivityFormModal.tsx` | contactId jetzt optional, neues Prop companyId | | `DealsPage.tsx` | Spacing Fix: minWidth fuer Stage (120px) und Wert (100px) Spalten | | `settings/CrmSettingsPage.tsx` | 3 neue Config-Sektionen: Branchen (mit Color-Picker), Kontotypen, Beziehungstypen | | `settings/CrmSettingsPage.module.css` | Styles fuer Config-Tabellen, Inline-Edit, Sort-Buttons | ### CompanyDetailPage — 3-Spalten-Layout ``` +------------------+------------------------+--------------------+ | Linke Spalte | Mittlere Spalte | Rechte Spalte | | (300px) | (flex) | (360px) | +------------------+------------------------+--------------------+ | Stammdaten: | Activity Feed: | Kontakte (Tabelle) | | - Name | - Inline-Notiz-Form | Vorgaenge (Tabelle)| | - Branche Badge | - Tabs: Notiz/Email/ | Beziehungen Card | | (farbig) | Aufgabe | Vertraege | | - Kontotyp | - Chronologische | (Platzhalter) | | - Zustaendigkeit | Liste aller | Lexware Belege | | - E-Mail, Tel | Aktivitaeten | | | - Website | - "via [Kontakt]" | | | - Adresse | Badge fuer | | | - Tags | Kontakt-Aktivitaeten | | | - Notizen | | | +------------------+------------------------+--------------------+ ``` **Responsive:** - Ab 1200px: 2 Spalten (Links+Mitte gestapelt | Rechts) - Ab 768px: 1 Spalte (alles gestapelt) ### Activity Feed Details - Nutzt `GET /crm/activities?companyId=X&includeContacts=true` - Inline-Formular oben: Betreff + Beschreibung + "Notiz speichern" Button - Tabs: "Notiz" (aktiv), "E-Mail" (disabled, Platzhalter), "Aufgabe" (disabled, Platzhalter) - Jeder Eintrag: Typ-Icon (SVG), Betreff, Ersteller, Zeitpunkt - Kontakt-Aktivitaeten zeigen "via [Kontaktname]" Badge ### CRM-Einstellungen — Admin-Konfiguration Drei neue Sektionen in `/crm/settings`: 1. **Branchen**: CRUD-Tabelle mit Name, Farb-Badge + Color-Picker, Sortier-Pfeile 2. **Kontotypen**: CRUD-Tabelle mit Name, Sortier-Pfeile 3. **Beziehungstypen**: CRUD-Tabelle mit Name, Sortier-Pfeile Alle mit Inline-Add (Eingabezeile oben), Inline-Edit, Delete mit Bestaetigung. ### CompanyFormModal — Dropdown-Aenderungen | Feld | Vorher | Nachher | |------|--------|---------| | Branche | Freitext-Input | Select-Dropdown aus `GET /crm/industries` | | Kontotyp | — (neu) | Select-Dropdown aus `GET /crm/account-types` | | Zustaendigkeit | — (neu) | Select-Dropdown aus `GET /crm/users` | `ownerName` wird beim Submit aus der User-Liste aufgeloest und im Payload mitgesendet. ### Genutzte neue Backend-Endpoints | Hook | Endpoint | Verwendung | |------|----------|-----------| | `useIndustries()` | `GET /crm/industries` | CompanyFormModal Dropdown, CRM Settings | | `useAccountTypes()` | `GET /crm/account-types` | CompanyFormModal Dropdown, CRM Settings | | `useRelationshipTypes()` | `GET /crm/relationship-types` | AddRelationshipModal, CRM Settings | | `useCompanyRelationships(id)` | `GET /crm/companies/:id/relationships` | CompanyRelationshipsCard | | `useCompanyActivities(id)` | `GET /crm/activities?companyId=X&includeContacts=true` | ActivityFeed | | `useTenantUsers()` | `GET /crm/users` | CompanyFormModal Owner-Dropdown | | `useCreateIndustry()` | `POST /crm/industries` | CRM Settings | | `useUpdateIndustry()` | `PATCH /crm/industries/:id` | CRM Settings | | `useDeleteIndustry()` | `DELETE /crm/industries/:id` | CRM Settings | | (analog fuer AccountTypes + RelationshipTypes) | | | | `useCreateCompanyRelationship()` | `POST /crm/companies/:id/relationships` | CompanyRelationshipsCard | | `useDeleteCompanyRelationship()` | `DELETE /crm/companies/:id/relationships/:relId` | CompanyRelationshipsCard | ### Offene Punkte - [ ] **Migration auf Server anwenden**: `20260311_add_company_detail_overhaul` + Seed-Data - [ ] **Container neu bauen und deployen** (Frontend + Backend) - [ ] **Kanban-Board fuer Vorgaenge** — Feature fuer spaeter geplant - [ ] **Vertraege-UI implementieren** — DB-Modell vorhanden, UI ist noch Platzhalter - [ ] **Activity Feed E-Mail/Aufgabe Tabs** — Derzeit Platzhalter (disabled) ### TypeScript-Status - Frontend: `npx tsc --noEmit` — 0 Fehler - Backend: `npx tsc --noEmit` — 0 Fehler (nach `prisma generate`) --- ## 2026-03-11 | Frontend: Server-Deployment & Bugfixes ### White-Screen-Fix Die GUI zeigte nach dem letzten Deploy ein weisses Bild. Ursache: Die `hooks.ts` mit den neuen Hooks fuer Industries, AccountTypes und RelationshipTypes war nur lokal geaendert, aber nie committed worden. Die `CrmSettingsPage.tsx` importierte Exports die auf dem Server nicht existierten → esbuild Build-Fehler → leere Seite. **Fix:** Alle uncommitteten CRM-Dateien (41 Dateien, Backend + Frontend) in einem Commit zusammengefasst und deployed. ### Server-Deployment der Company Detail Overhaul Folgende Schritte wurden auf dem Server (172.20.10.59) ausgefuehrt: 1. **Prisma-Migration `20260310_add_lexware_integration`** — War bereits manuell angewendet (VoucherType Enum existierte), wurde als `applied` markiert via `prisma migrate resolve` 2. **Prisma-Migration `20260311_add_company_detail_overhaul`** — Erfolgreich angewendet. Neue Tabellen: `industries`, `account_types`, `relationship_types`, `company_relationships`, `contracts`. Aenderungen an `companies` und `activities`. 3. **Prisma-Client regeneriert** — `prisma generate` im Container ausgefuehrt 4. **CRM-Container neugestartet** — Alle neuen Controller registriert: - `IndustriesController {/api/v1/crm/industries}` - `AccountTypesController {/api/v1/crm/account-types}` - `RelationshipTypesController {/api/v1/crm/relationship-types}` - `CompanyRelationshipsController {/api/v1/crm/companies/:companyId/relationships}` 5. **Seed-Daten geladen** fuer beide Tenants (`3fc0e74d-...` und `11111111-...`): - 8 Branchen pro Tenant (+ 1 migrierte je Tenant = 18 total) - 4 Kontotypen pro Tenant (8 total) - 4 Beziehungstypen pro Tenant (8 total) ### Health-Check nach Deployment ```json { "status": "ok", "service": "crm-service", "version": "0.2.0", "services": { "database": "up", "redis": "up", "lexware": "up" } } ``` ### CRM-Einstellungen — Tabbed Layout Die CRM-Settings-Seite (`/crm/settings`) wurde von gestapelten Cards auf ein **Tab-Layout** umgestellt: | Tab | Inhalt | |-----|--------| | **Module** | Modul-Toggles (Kontakte, Unternehmen, Vorgaenge, Pipelines, Lexware) | | **Lexoffice Sync** | Import/Export eingebettet (vorher separate Seite `/crm/lexware-sync`) | | **Weitere Einstellungen** | Admin-Konfiguration: Branchen, Kontotypen, Beziehungstypen | **Lexware Import** wurde ebenfalls ueberarbeitet: Statt Suchfeld-only gibt es jetzt eine **browsable paginierte Liste** aller Lexware-Kontakte mit aufklappbaren Ansprechpartnern und individuellen Import-Buttons. ### Commits | Commit | Beschreibung | |--------|-------------| | `5532918` | feat(frontend): redesign Lexware Import with browsable list + Ansprechpartner | | `4e5c26c` | feat(frontend): add tabbed layout to CRM Settings page | | `0ed1e77` | feat(crm): add company detail overhaul with industries, account types, relationship types | | `08b212b` | docs(crm): update INSIGHT-CRM.md with company detail overhaul entries | ### Offene Punkte - [x] ~~Migration auf Server anwenden~~ — Erledigt (beide Migrationen) - [x] ~~Container neu bauen und deployen~~ — Frontend + CRM Backend deployed - [x] ~~Seed-Daten laden~~ — Fuer beide Tenants - [ ] **Vertraege-UI implementieren** — DB-Modell vorhanden, UI ist Platzhalter (`ContractsCard.tsx`) - [ ] **Activity Feed E-Mail/Aufgabe Tabs** — Tabs vorhanden aber disabled - [ ] **Kanban-Board fuer Vorgaenge** — Backend ready, Frontend Feature fuer spaeter ### Hinweis an Backend - Der `insight-crm` Container laeuft im Dev-Modus mit Volume-Mount. Code-Aenderungen werden automatisch erkannt. - Nach Schema-Aenderungen muss `prisma generate` im Container ausgefuehrt werden. - Die `LexwareSyncContent`-Komponente wurde als separater Export aus `LexwareSyncPage.tsx` extrahiert und wird sowohl auf der Standalone-Seite (`/crm/lexware-sync`) als auch eingebettet im Settings-Tab verwendet. --- ## 2026-03-11 | Frontend: Bug-Report — Lexware Import Company schlaegt fehl (500) ### Problem Beim Klick auf "Unternehmen" (Import als Company) im Lexware Sync Tab kommt ein **500 Internal Server Error**. ### Fehlermeldung (aus `docker logs insight-crm`) ``` PrismaClientValidationError: Invalid `this.prisma.company.create()` invocation in /app/src/lexware/lexware-contacts.service.ts:229:32 → 229 return this.prisma.company.create({ data: { name: "team neusta SE", email: "k.sauer@neusta.de", phone: undefined, street: "Konsul-Smidt-Straße 24", zip: "28217", city: "Bremen", country: "DE", notes: undefined, lexwareContactId: "e23f5165-9c1e-40ba-9536-9990703421df", lexwareContactVersion: 6, lexwareSyncedAt: new Date("2026-03-11T09:05:10.318Z"), createdBy: "4627b01c-2f23-4ee8-a44e-c04bff068a5f", + tenantId: String ← FEHLER: Typ "String" statt UUID-Wert }, ``` ### Ursache In `lexware-contacts.service.ts` Zeile 229 wird `tenantId` als **TypeScript-Typ `String`** uebergeben statt als tatsaechlicher UUID-Wert aus dem JWT-Token. Vermutlich steht dort so etwas wie `tenantId: String` statt `tenantId: user.tenantId` oder `tenantId: this.tenantId`. ### Betroffene Datei `packages/crm-service/src/lexware/lexware-contacts.service.ts` — Zeile ~229 (`importCompany`-Methode) ### Vermutlich gleicher Fehler bei - `importContact` (Lexware-Kontakt als CRM Contact importieren) - Eventuell auch `push` und `sync` Methoden, falls diese ebenfalls `tenantId` setzen ### Reproduktion 1. `/crm/settings` → Tab "Lexoffice Sync" → "Import (Lexware → CRM)" 2. Beliebigen Lexware-Kontakt suchen (z.B. "team") 3. Auf "Unternehmen" Button klicken 4. → Rote Fehlermeldung: "Import fehlgeschlagen: Request failed with status code 500" --- ## 2026-03-11 | Backend: Fix — Lexware Import 500 (fehlende tenantId) ### Ursache Der `TenantGuard` liess `PLATFORM_ADMIN`-User ohne `tenantId`-Pruefung durch: ```typescript // ALT (fehlerhaft): if (user?.role === 'PLATFORM_ADMIN') { return true; // ← Kein tenantId-Check! } ``` Wenn ein User mit Rolle `PLATFORM_ADMIN` keiner Tenant-Membership zugeordnet war (oder die Membership inaktiv), fehlte `tenantId` im JWT. Der Controller uebergab dann `user.tenantId!` = `undefined` an den Service, was zum Prisma-Validierungsfehler fuehrte. ### Fixes **1. TenantGuard (`src/auth/guards/tenant.guard.ts`):** - ALLE User (auch PLATFORM_ADMIN) muessen jetzt eine `tenantId` haben, um auf CRM-Ressourcen zuzugreifen - Klare Fehlermeldung: "Kein Mandant zugeordnet. Bitte mit einem mandanten-gebundenen Account anmelden." **2. Defensive Pruefung in Lexware-Service (`src/lexware/lexware-contacts.service.ts`):** - `importAsCompany()` und `importAsContact()` pruefen zusaetzlich `if (!tenantId)` und werfen `BadRequestException` mit klarer Meldung ### Betroffene Dateien | Datei | Aenderung | |-------|-----------| | `src/auth/guards/tenant.guard.ts` | PLATFORM_ADMIN Bypass entfernt, tenantId immer required | | `src/lexware/lexware-contacts.service.ts` | Defensive tenantId-Pruefung in Import-Methoden | ### Auswirkung - PLATFORM_ADMIN ohne Tenant-Zuordnung bekommt jetzt **403 Forbidden** statt **500 Internal Server Error** - Alle anderen User sind nicht betroffen (hatten vorher schon den tenantId-Check) - TypeScript-Check: 0 Fehler --- ## 2026-03-12 | Architekt: Neue Anforderungen aus Konzeptdokument v1.0 + Briefing Der Architekt hat das Konzeptdokument (INSIGHT_Konzept_v1.0.docx) und das Claude Briefing (CLAUDE_BRIEFING.docx) aktualisiert. Folgende Kapitel sind neu oder erweitert und betreffen den CRM-Service direkt: - **Kapitel 22** — CRM-Modul (vorher nur Platzhalter, jetzt vollstaendig spezifiziert) - **Kapitel 24** — Office 365 Integration (neu, CRM-relevante Teile) - **Kapitel 14 im Briefing** — Office 365 Kurzreferenz (neu) ### Hinweis: CORE vs CRM Folgende Teile sind **CORE-Aufgaben** (NICHT fuer den CRM-Entwickler): - Kap 24.1: OAuth-Flow + `user_integrations` Tabelle in `platform_core` - Kap 24.8: Azure App-Registrierung im Azure Portal - `.env`-Variablen: `MS_CLIENT_ID`, `MS_CLIENT_SECRET`, `MS_REDIRECT_URI`, `MS_INTEGRATION_ENCRYPTION_KEY` Alles Folgende ist **CRM-Arbeit**. --- ### A) CRM-Modul Spezifikation (Kap 22) — Neue/Erweiterte Anforderungen #### A.1 Kontakttypen & Felder (Kap 22.1) Die vollstaendige Felddefinition fuer Person und Unternehmen liegt jetzt vor: **Kontakttyp: Person** | Feld | Typ | Pflicht | Bemerkung | |------|-----|---------|-----------| | Vorname | String | Ja | | | Nachname | String | Ja | | | Jobtitel | String | Nein | | | Unternehmen | Relation -> Unternehmen | Nein | | | Abteilung | String | Nein | | | E-Mail | Array (String) | Nein | Mehrere, Typ: Arbeit / Privat / Sonstige | | Telefon | Array (String) | Nein | Mehrere, Typ: Buero / Mobil / Fax | | Adresse | Objekt | Nein | Strasse, PLZ, Stadt, Land | | LinkedIn-URL | String (URL) | Nein | | | Geburtsdatum | Date | Nein | Optional, kann ausgeblendet werden | | Quelle | Enum | Nein | Messe, Empfehlung, Website, Kaltakquise, Import, Visitenkarte, Sonstige | | Tags | Array (String) | Nein | Frei vergebbar, tenant-weit geteilt | | Status | Enum | Ja | Aktiv / Inaktiv / Gesperrt (Default: Aktiv) | | Notizen | Text (Markdown) | Nein | | | Benutzerdefinierte Felder | Dynamisch | Nein | Siehe A.6 | **Kontakttyp: Unternehmen** | Feld | Typ | Pflicht | Bemerkung | |------|-----|---------|-----------| | Firmenname | String | Ja | | | Branche | String | Nein | Freitext oder vordefinierte Kategorien | | Website | String (URL) | Nein | | | Telefon | Array (String) | Nein | Mehrere Nummern | | Adresse Hauptsitz | Objekt | Nein | Strasse, PLZ, Stadt, Land | | Adresse Lieferung | Objekt | Nein | Optional abweichende Lieferadresse | | USt-IdNr. | String | Nein | | | Steuernummer | String | Nein | | | Handelsregisternummer | String | Nein | z.B. HRB 12345 — befuellbar via Datenanreicherung | | Registergericht | String | Nein | z.B. Amtsgericht Muenchen — befuellbar via Datenanreicherung | | Unternehmensgroesse | Enum | Nein | 1-10, 11-50, 51-200, 201-500, 500+ | | Ansprechpartner | Relation -> Personen | Nein | Verknuepfte Personen | | Tags | Array (String) | Nein | | | Status | Enum | Ja | Aktiv / Inaktiv / Gesperrt | | Notizen | Text (Markdown) | Nein | | | Datenanreicherung | Automatisch | Nein | data_enriched_at + data_enriched_source | | Benutzerdefinierte Felder | Dynamisch | Nein | Siehe A.6 | **Backend-Aufgabe**: Abgleich mit bestehenden Prisma-Modellen. Fehlende Felder (LinkedIn, Geburtsdatum, Quelle, Unternehmensgroesse, USt-IdNr, Steuernummer, Handelsregisternummer, Registergericht, Adresse Lieferung, data_enriched_at/source) muessen ergaenzt werden. #### A.2 Firmendaten-Anreicherung / Data Enrichment (Kap 22.2) — NEU "Firmendaten laden"-Button im Unternehmenformular. Stammdaten aus externen Registern abrufen und als Vorschlag in einem Modal anzeigen. **Quellen:** 1. **Unternehmensregister.de** (kostenlos, oeffentlich) — HR-Nummer, Registergericht, Rechtsform, Sitz, Eintragsdatum 2. **North Data API** (kommerziell, API-Key pro Tenant) — Adresse, Branche, Umsatz, Mitarbeiterzahl, Gesellschafter, Verflechtungen **Backend-Aufgaben:** - [ ] `POST /crm/companies/:id/enrich` oder `POST /crm/data-enrichment/company` Endpoint - [ ] Paralleler Abruf: Unternehmensregister.de + North Data API (Timeout je 10 Sek.) - [ ] Ergebnisse normalisieren und zusammenfuehren - [ ] `data_enriched_at` + `data_enriched_source` in Company speichern - [ ] Admin-Einstellung: North Data API-Key pro Tenant (Admin > CRM > Integrationen) **Frontend-Aufgaben:** - [ ] "Firmendaten laden" Button im Unternehmenformular - [ ] Anreicherungs-Modal: Linke Spalte = aktueller Wert, Rechte Spalte = Vorschlag (Quelle), Checkbox pro Feld - [ ] "Auswahl uebernehmen" -> Felder im Formular setzen (Speichern erst bei explizitem Speichern) - [ ] CRM-Settings-Seite: North Data API-Key Konfiguration #### A.3 Zustaendigkeit / Account Owner (Kap 22.3) — NEU Jeder Kontakt/Unternehmen kann mit mehreren internen Mitarbeitern verknuepft werden (m:n). | Aspekt | Entscheidung | |--------|-------------| | Modell | m:n — ein Kontakt hat mehrere Owner, ein Mitarbeiter hat mehrere Kontakte | | Rollen pro Zuweisung | `OWNER`, `MEMBER`, `WATCHER` (unterschiedliche Bearbeitungsrechte) | | Mitarbeiter-Referenz | `user_id` aus `platform_core` (kein Kopieren von User-Daten) | | Anzeige | Avatare der zugewiesenen Mitarbeiter in der Kontaktkarte | | Pflicht-Owner | Mindestens 1 Owner pro Kontakt bei Erstellung (Default: erstellender User) | **DB-Tabelle:** `crm_contact_owners (contact_id, user_id, role: OWNER|MEMBER|WATCHER)` **Backend-Aufgaben:** - [ ] Contact-Owner CRUD: `POST/DELETE /crm/contacts/:id/owners` - [ ] Bei Kontakt-Erstellung automatisch erstellenden User als OWNER setzen - [ ] Owner-Info in Contact-Detail-Response mitliefern **Frontend-Aufgaben:** - [ ] Avatare der Owner in der Kontaktkarte anzeigen - [ ] Owner-Zuweisung UI (User suchen + Rolle waehlen) #### A.4 Pipeline & Deal-Management — Erweiterte Spec (Kap 22.4) Ergaenzungen zur bestehenden Implementierung: - **Forecast-Ansicht**: Aggregierter Dealwert pro Stage gewichtet mit Wahrscheinlichkeit - **Lost-Grund**: Enum + Freitext bei Status Lost: Preis, Timing, Wettbewerber, Kein Bedarf, Sonstige - **Pipeline-Sichtbarkeit**: Enum — Alle Tenant-User / Nur zugewiesene Teams - **Deal-Owner**: m:n analog zu Kontakt-Owner (crm_deal_owners) **Backend-Aufgaben:** - [ ] `lost_reason` Enum + `lost_reason_text` Freitext in Deals - [ ] Deal-Owner CRUD (analog Contact-Owner) - [ ] Forecast-Endpoint: `GET /crm/deals/forecast` (Wert x Wahrscheinlichkeit pro Stage) **Frontend-Aufgaben:** - [ ] Lost-Grund Modal bei Stage-Wechsel zu "Lost" - [ ] Kanban-Board (Drag & Drop) - [ ] Forecast-Ansicht #### A.5 Aktivitaeten & Aufgaben (Kap 22.5) — Erweiterte Spec 6 Aktivitaetstypen sind definiert: | Typ | Icon | Bemerkung | |-----|------|-----------| | Anruf (Call) | Telefon | Kann Gespraechsnotizen enthalten | | Meeting | Kalender | Datum, Uhrzeit, Ort / Videolink | | E-Mail | Brief | Freitext-Notiz, KEINE echte E-Mail-Integration in MVP | | Aufgabe (Task) | Checkbox | Faelligkeitsdatum, Zuweisung an Mitarbeiter | | Notiz | Stift | Freitext ohne Faelligkeitsdatum | | Follow-Up | Pfeil | Erinnerung zu einem definierten Zeitpunkt | **Hinweis**: E-Mail-Integration (Outlook) kommt erst mit Office 365 (Kap 24). #### A.6 Benutzerdefinierte Felder / Custom Fields (Kap 22.6) — NEU Pro Kontakttyp (Person, Unternehmen) und pro Deal koennen eigene Felder definiert werden. **Unterstuetzte Feldtypen:** | Typ | Beschreibung | Beispiel | |-----|-------------|---------| | Text | Einzeiliger Freitext | Kundennummer | | Textarea | Mehrzeiliger Freitext | Besondere Hinweise | | Zahl | Integer oder Decimal | Umsatz, Vertragslaufzeit | | Datum | Datumspicker | Vertragsbeginn | | Auswahl (Dropdown) | Vordefinierte Werte | Kundensegment: A / B / C | | Mehrfachauswahl | Mehrere Werte waehlbar | Interessensgebiete | | Checkbox | Boolean Ja/Nein | DSGVO-Einwilligung | | URL | Validierter Link | Portal-Link | **DB-Tabellen:** ```sql crm_custom_field_defs (id, entity_type: PERSON|COMPANY|DEAL, name, label, field_type, options jsonb, is_required, position) crm_custom_field_values (id, field_def_id, entity_id, value_text, value_number, value_date, value_boolean, value_json) ``` **Backend-Aufgaben:** - [ ] CRUD fuer Field-Definitions: `GET/POST/PATCH/DELETE /crm/custom-fields` - [ ] Wert-Speicherung bei Entity-Create/Update - [ ] Custom Fields in Entity-Detail-Response mitliefern **Frontend-Aufgaben:** - [ ] Admin-Bereich: Custom Fields Verwaltung (Drag & Drop Reihenfolge, Pflichtfeld-Flag) - [ ] Dynamische Formular-Felder in Contact/Deal-Formularen - [ ] Custom Fields als optionale Spalten in Listen-Ansichten - [ ] Custom Fields als Filter in der Suche #### A.7 Kontakt-Import (Kap 22.7) — NEU **Import via Datei:** - [ ] CSV-Import mit visuellem Spalten-Mapper - [ ] Excel-Import (.xlsx) - [ ] vCard-Import (.vcf, einzeln und ZIP) - [ ] Vorschau der ersten 10 Datensaetze + Validierungsfehler anzeigen - [ ] Duplikat-Erkennung via E-Mail: ueberspringen / zusammenfuehren / als Duplikat markieren **Visitenkarten-Scan (Anthropic Vision API):** - [ ] "Visitenkarte scannen" — Kamera-/Datei-Dialog - [ ] Bild base64-kodiert an CRM-Backend senden - [ ] Backend ruft Anthropic Vision API auf (strukturierter Prompt) - [ ] Vorausgefuelltes Kontaktformular zur Bestaetigung - [ ] Rate Limit: max. 50 Scans pro Tenant pro Tag (konfigurierbar) - [ ] Bild wird NICHT dauerhaft gespeichert #### A.8 Berechtigungsmodell (Kap 22.8) — NEU Ownership-basiertes Sichtbarkeitsmodell: | Stufe | Beschreibung | Sieht was? | |-------|-------------|-----------| | Eigene | User sieht nur eigene Datensaetze (Owner) | Eigene Kontakte, Deals, Aktivitaeten | | Team | User sieht alle Datensaetze der eigenen Abteilung | Kontakte aller Kollegen | | Alle | User sieht alle Datensaetze des Tenants | Vollzugriff (lesend) | **Berechtigungen nach Rolle:** | Rolle | Sichtbarkeit | Erstellen | Bearbeiten | Loeschen | |-------|-------------|-----------|------------|----------| | tenant_admin | Alle | Ja | Alle | Alle | | team_lead | Team | Ja | Team + Eigene | Eigene | | tenant_member | Konfigurierbar | Ja | Eigene + zugewiesene | Eigene | | tenant_readonly | Konfigurierbar | Nein | Nein | Nein | **Backend-Aufgaben:** - [ ] Sichtbarkeitsfilter in allen List-Queries (Contacts, Deals, Activities) - [ ] Tenant-Admin Einstellung: Default-Sichtbarkeit pro Rolle - [ ] Per-User Override moeglich **Frontend-Aufgaben:** - [ ] Admin > CRM-Einstellungen > Berechtigungen: Sichtbarkeitsstufe konfigurieren #### A.9 Reporting & Dashboards (Kap 22.9) — NEU 5 Reports/Dashboards sind definiert: | Dashboard | Inhalt | Aktualisierung | |-----------|--------|---------------| | Pipeline-Uebersicht | Deals pro Stage, Gesamtvolumen, gewichteter Forecast | Echtzeit | | Aktivitaeten-Uebersicht | Offene Aufgaben, ueberfaellige Aktivitaeten, Volumen/Woche | Echtzeit | | Kontaktwachstum | Neue Kontakte/Monat, aufgeschluesselt nach Quelle | Taeglich | | Win/Loss-Analyse | Won vs Lost Deals, Lost-Gruende als Torte, Durchschnittliche Deal-Dauer | Taeglich | | Mitarbeiter-Performance | Deals pro Mitarbeiter, Aktivitaeten-Anzahl, Response-Zeit | Taeglich | - Alle Reports als CSV exportierbar - Datumsbereiche: letzte 7 / 30 / 90 Tage, benutzerdefiniert - Mitarbeiter-Performance nur fuer tenant_admin und team_lead sichtbar **Backend-Aufgaben:** - [ ] Reporting-Endpoints: `GET /crm/reports/pipeline`, `/reports/activities`, `/reports/contacts-growth`, `/reports/win-loss`, `/reports/performance` - [ ] CSV-Export fuer alle Reports **Frontend-Aufgaben:** - [ ] CRM Dashboard-Seite mit den 5 Report-Widgets - [ ] Datumbereich-Selektor - [ ] CSV-Export Buttons #### A.10 CRM Datenbankschema (Kap 22.10) Vollstaendiges Schema laut Architekt: ```sql -- Kernentitaeten crm_contacts (id, type: PERSON|COMPANY, status, source, created_by, created_at, updated_at) crm_persons (id, contact_id, first_name, last_name, job_title, department, birthday, notes) crm_companies (id, contact_id, name, industry, website, vat_id, tax_id, size_class, notes) crm_contact_emails (id, contact_id, email, type: WORK|PERSONAL|OTHER, is_primary) crm_contact_phones (id, contact_id, phone, type: OFFICE|MOBILE|FAX, is_primary) crm_contact_addresses (id, contact_id, type: MAIN|DELIVERY, street, zip, city, country) -- Beziehungen crm_person_company (person_contact_id, company_contact_id) crm_contact_owners (contact_id, user_id, role: OWNER|MEMBER|WATCHER) crm_contact_tags (contact_id, tag) -- Pipeline & Deals crm_pipelines (id, name, is_default, visibility) crm_pipeline_stages (id, pipeline_id, name, color, position, probability, is_won_stage, is_lost_stage) crm_deals (id, contact_id, pipeline_id, stage_id, title, value, currency, probability_override, expected_close_date, status: OPEN|WON|LOST, lost_reason, lost_reason_text, notes, created_by) crm_deal_owners (deal_id, user_id, role: OWNER|MEMBER|WATCHER) -- Aktivitaeten crm_activities (id, type: CALL|MEETING|EMAIL|TASK|NOTE|FOLLOWUP, contact_id nullable, deal_id nullable, title, body, due_date, completed_at, assigned_to_user_id, created_by) -- Custom Fields crm_custom_field_defs (id, entity_type: PERSON|COMPANY|DEAL, name, label, field_type, options jsonb, is_required, position) crm_custom_field_values (id, field_def_id, entity_id, value_text, value_number, value_date, value_boolean, value_json) ``` **Hinweis**: Das aktuelle Prisma-Schema im CRM-Service weicht teilweise ab (z.B. `app_crm` Schema-Prefix statt `crm_` Tabellen-Prefix, flaches Company/Contact-Modell statt Contact+Person+Company Split). Der Architekt hat das Ziel-Schema definiert — Abgleich und Migration sind Backend-Aufgaben. #### A.11 CRM Events (Kap 22.11) | Event | Ausgeloest von | Empfaenger/Zweck | |-------|---------------|-----------------| | `crm.contact.created` | CRM-Service | Zukuenftige Module (Marketing etc.) | | `crm.contact.updated` | CRM-Service | Zukuenftige Module | | `crm.deal.stage_changed` | CRM-Service | Reporting, Automatisierungen | | `crm.deal.won` | CRM-Service | Reporting, Modul-Integrationen | | `crm.deal.lost` | CRM-Service | Reporting | | `crm.activity.due_soon` | CRM-Service (Scheduler) | Benachrichtigungs-Service | | `core.user.deactivated` | Core-Service | CRM prueft Owner-Reassignment | **Backend-Aufgabe:** - [ ] Redis Pub/Sub Events bei Kontakt/Deal/Activity-Aenderungen publishen --- ### B) Office 365 Integration — CRM-relevante Teile (Kap 24) Die OAuth-Verbindung (Kap 24.1) und Azure App-Registrierung (Kap 24.8) sind **CORE-Aufgaben**. Der CRM-Service nutzt die bestehende MS-Verbindung des Users fuer folgende Features: #### B.1 E-Mail Tab im CRM-Kontakt (Kap 24.2) — Read-only Pro CRM-Kontakt wird ein "E-Mails"-Tab angezeigt mit allen Outlook-Mails von/an die Kontakt-E-Mail-Adresse. **Graph API Abfrage:** ``` GET /me/messages ?$filter=from/emailAddress/address eq '{kontakt_email}' or toRecipients/any(...) &$select=id,subject,from,toRecipients,receivedDateTime,bodyPreview,isRead &$orderby=receivedDateTime desc &$top=25 ``` | Feature | Detail | |---------|--------| | Anzeige | Absender, Betreff, Datum, Vorschautext (max. 255 Zeichen) | | Sortierung | Neueste zuerst, 25 pro Seite | | Caching | Redis 5 Min (Key: `user:{id}:mails:contact:{id}`) | | Kein Volltext | Nur bodyPreview, kein vollstaendiger E-Mail-Body | | Kein Senden | Read-only | **Backend-Aufgaben:** - [ ] `GET /crm/contacts/:id/emails` Endpoint (Proxy zu Graph API) - [ ] MS Access Token aus Redis / Refresh via Core - [ ] Redis-Caching (5 Min TTL) **Frontend-Aufgaben:** - [ ] "E-Mails" Tab in ContactDetailPage (nur wenn MS-Verbindung aktiv) - [ ] Ausgegraut wenn keine MS-Verbindung, Hinweis "Microsoft 365 verbinden" #### B.2 Kalender Tab im CRM-Kontakt (Kap 24.3) — Read-only Outlook-Termine bei denen der CRM-Kontakt als Teilnehmer eingetragen ist. **Graph API Abfrage:** ``` GET /me/calendarView ?startDateTime={heute}T00:00:00Z &endDateTime={heute+90Tage}T23:59:59Z &$filter=attendees/any(a: a/emailAddress/address eq '{kontakt_email}') &$select=id,subject,start,end,location,attendees,bodyPreview,onlineMeetingUrl ``` | Feature | Detail | |---------|--------| | Anzeige | Kommende 90 Tage + vergangene 90 Tage | | Felder | Titel, Datum/Uhrzeit, Ort, Online-Meeting-Link, Teilnehmer | | Caching | Redis 5 Min | | Kein Schreiben | Read-only | **Backend-Aufgaben:** - [ ] `GET /crm/contacts/:id/calendar` Endpoint - [ ] Redis-Caching **Frontend-Aufgaben:** - [ ] "Kalender" Tab in ContactDetailPage #### B.3 Aufgaben Sync — Bidirektional mit Microsoft To Do (Kap 24.4) CRM-Aufgaben koennen optional nach Microsoft To Do synchronisiert werden. **Richtung INSIGHT -> To Do:** - Trigger: Aufgabe erstellt/geaendert im CRM - Action: `POST /me/todo/lists/{defaultListId}/tasks` (oder PATCH wenn ms_task_id bekannt) - `ms_task_id` wird in `crm_activities` gespeichert **Richtung To Do -> INSIGHT:** - NestJS `@Cron` Polling alle 5 Min fuer User mit aktiver MS-Verbindung - `GET /me/todo/lists/{listId}/tasks?$filter=lastModifiedDateTime gt {letzter_sync}` - Status-Aenderungen (erledigt/offen) werden uebernommen - Redis-Lock pro User verhindert parallele Sync-Jobs - Nur INSIGHT-erstellte Aufgaben werden synchronisiert (die eine ms_task_id haben) **DB-Aenderungen an crm_activities:** ```sql + ms_task_id VARCHAR(255) -- To Do Aufgaben ID + ms_task_list_id VARCHAR(255) -- To Do Listen ID + ms_synced_at TIMESTAMPTZ -- Letzter erfolgreicher Sync ``` **Backend-Aufgaben:** - [ ] ms_task_id, ms_task_list_id, ms_synced_at Felder in Activity-Model - [ ] Graph API Integration: POST/PATCH Tasks - [ ] @Cron Scheduler (alle 5 Min) fuer To Do -> INSIGHT Sync - [ ] Redis-Lock pro User - [ ] Konfliktbehandlung: INSIGHT gewinnt (Last Write Wins mit Timestamp) #### B.4 Kontakte Export nach Outlook (Kap 24.5) — Manuell Button "Nach Outlook exportieren" im CRM-Kontakt (Person). **Ablauf:** 1. User klickt "Nach Outlook exportieren" 2. Pruefung: MS-Verbindung aktiv? -> Wenn nein: Hinweis 3. Existiert Kontakt in Outlook (ms_contact_id)? -> PATCH (Update) / POST (Neu) 4. ms_contact_id wird in crm_persons gespeichert **Feldzuordnung CRM -> Outlook:** | CRM Feld | Outlook Contacts (Graph API) | |----------|------------------------------| | Vorname + Nachname | givenName + surname | | Jobtitel | jobTitle | | Unternehmen | companyName | | Abteilung | department | | E-Mails | emailAddresses[] | | Telefone | businessPhones[] / mobilePhone | | Adresse | businessAddress | | LinkedIn-URL | businessHomePage | | Notizen | personalNotes | **DB-Aenderung an crm_persons:** ```sql + ms_contact_id VARCHAR(255) -- Outlook Kontakt ID ``` **Backend-Aufgaben:** - [ ] `POST /crm/contacts/:id/export-to-outlook` Endpoint - [ ] ms_contact_id Feld in Person-Model - [ ] Bei 404 (Kontakt in Outlook geloescht): neuen Kontakt erstellen, ms_contact_id aktualisieren **Frontend-Aufgaben:** - [ ] "Nach Outlook exportieren" Button in Kontakt-Header (nur fuer Personen) - [ ] Ausgegraut wenn keine MS-Verbindung --- ### C) Zusammenfassung: Priorisierte Aufgabenliste fuer CRM-Entwickler **Prio 1 — Kurzfristig (bestehende Features erweitern):** - [ ] Fehlende Kontakt-/Unternehmens-Felder im Prisma-Schema ergaenzen (LinkedIn, Geburtsdatum, Quelle, USt-IdNr etc.) - [ ] Lost-Grund (Enum + Freitext) bei Deals - [ ] Contact/Deal-Owner m:n Modell (crm_contact_owners, crm_deal_owners) - [ ] Redis Pub/Sub Events bei Entity-Aenderungen **Prio 2 — Mittelfristig (neue Features):** - [ ] Custom Fields (Definition + Wert-Speicherung + Admin-UI) - [ ] Firmendaten-Anreicherung (Unternehmensregister.de + North Data) - [ ] Kontakt-Import (CSV, Excel, vCard) mit Spalten-Mapper + Duplikat-Erkennung - [ ] Berechtigungsmodell (Eigene/Team/Alle Sichtbarkeit) - [ ] Kanban-Board fuer Deals (Drag & Drop) - [ ] Forecast-Ansicht **Prio 3 — Spaeter (abhaengig von Core-Vorarbeiten):** - [ ] Office 365: E-Mail Tab (benoetigt OAuth-Infrastruktur im Core) - [ ] Office 365: Kalender Tab - [ ] Office 365: Aufgaben Sync mit Microsoft To Do - [ ] Office 365: Kontakte Export nach Outlook - [ ] Visitenkarten-Scan (Anthropic Vision API) - [ ] CRM Reporting & Dashboards (5 Reports + CSV-Export) --- --- ## 2026-03-12 | Plattform-Admin: Briefing fuer CRM-Backend-Experten ### Kontext Der Architekt hat das Konzeptdokument v1.0 und das Claude Briefing aktualisiert. Es gibt umfangreiche neue Anforderungen fuer das CRM-Modul (Details: siehe Eintrag oben vom gleichen Datum). Dieses Briefing definiert die **Reihenfolge und Abhaengigkeiten** fuer die CRM-Backend-Entwicklung. ### Aktueller Stand Das CRM-Backend (`packages/crm-service/`) hat bereits folgende Module produktiv: | Modul | Status | Endpoints | |-------|--------|-----------| | Contacts (Person + Company) | Laeuft | CRUD + Suche + Paginierung | | Deals | Laeuft | CRUD + Pipeline-Zuordnung | | Pipelines + Stages | Laeuft | CRUD + Stage-Management | | Activities | Laeuft | CRUD (6 Typen) | | Industries | Laeuft | CRUD | | Companies (Standalone) | Laeuft | CRUD + Lexware-Sync | | Trade Events (Messe-Timer) | Laeuft | CRUD + Active-Filter | | CRM Settings | Laeuft | Modulverwaltung pro Tenant | Das Frontend fuer alle obigen Module ist ebenfalls deployed und funktional. ### Phasenplan: Was wann zu tun ist #### Phase 1 — SOFORT starten (keine Abhaengigkeiten) Diese Aufgaben erweitern bestehende Module und koennen sofort umgesetzt werden: **1.1 Fehlende Felder im Prisma-Schema ergaenzen** - Person: `linkedinUrl`, `birthday`, `source` (Enum), `status` (Enum) - Company: `vatId`, `taxId`, `tradeRegisterNumber`, `registerCourt`, `companySize` (Enum), `deliveryAddress`, `dataEnrichedAt`, `dataEnrichedSource` - Referenz: Abschnitt A.1 oben fuer vollstaendige Feldliste - **Achtung:** Bestehende API-Responses und DTOs muessen die neuen Felder enthalten - **Migration:** `prisma db push` auf dem Server nach Schema-Aenderung **1.2 Contact/Deal Owner (m:n Modell)** - Neue Tabellen: `crm_contact_owners` und `crm_deal_owners` (contact_id/deal_id, user_id, role: OWNER|MEMBER|WATCHER) - Bei Kontakt-Erstellung: erstellenden User automatisch als OWNER setzen - Owner-Info in Detail-Responses mitliefern (User-ID + Rolle) - Endpoints: `POST/DELETE /crm/contacts/:id/owners`, analog fuer Deals - Referenz: Abschnitt A.3 + A.4 oben **1.3 Lost-Grund bei Deals** - Neue Felder: `lostReason` (Enum: PRICE|TIMING|COMPETITOR|NO_NEED|OTHER) + `lostReasonText` (Freitext) - Bei Stage-Wechsel zu Lost: lostReason Pflichtfeld - Referenz: Abschnitt A.4 oben **1.4 Redis Pub/Sub Events** - Events publishen bei: Contact created/updated, Deal stage_changed/won/lost, Activity due_soon - Key-Prefix: `app:crm:events:*` - Event-Format: `{ type: 'crm.deal.won', tenantId, entityId, userId, timestamp, payload }` - Referenz: Abschnitt A.11 oben #### Phase 2 — DANACH (neue Features, keine CORE-Abhaengigkeit) Nach Abschluss von Phase 1 koennen diese Features **parallel** entwickelt werden: **2.1 Custom Fields System** - DB: `crm_custom_field_defs` + `crm_custom_field_values` (Details in A.6) - CRUD Endpoints fuer Field-Definitions: `GET/POST/PATCH/DELETE /crm/custom-fields` - Wert-Speicherung bei Entity Create/Update - Custom Fields in Entity-Detail-Responses mitliefern - 8 Feldtypen: Text, Textarea, Zahl, Datum, Dropdown, Mehrfachauswahl, Checkbox, URL **2.2 Firmendaten-Anreicherung (Data Enrichment)** - `POST /crm/companies/:id/enrich` Endpoint - Paralleler Abruf: Unternehmensregister.de (kostenlos) + North Data API (API-Key pro Tenant) - Ergebnis als Vorschlag zurueckgeben (Frontend zeigt Vergleich-Modal) - Admin-Einstellung fuer North Data API-Key - Referenz: Abschnitt A.2 oben **2.3 Kontakt-Import** - CSV-Import mit Spalten-Mapping (Backend parst, gibt Vorschau zurueck) - Excel-Import (.xlsx) — gleiche Logik - vCard-Import (.vcf + ZIP) - Duplikat-Erkennung via E-Mail - Referenz: Abschnitt A.7 oben **2.4 Berechtigungsmodell** - Sichtbarkeitsfilter in allen List-Queries: Eigene / Team / Alle - Konfigurierbar pro Rolle via Tenant-Admin - Referenz: Abschnitt A.8 oben **2.5 Forecast-Endpoint** - `GET /crm/deals/forecast` — Aggregierter Dealwert pro Stage gewichtet mit Wahrscheinlichkeit - Referenz: Abschnitt A.4 oben #### Phase 3 — SPAETER (abhaengig von CORE OAuth) > **BLOCKER:** Diese Features benoetigen die Microsoft 365 OAuth-Integration im Core-Service. > Der Core-Entwickler arbeitet **parallel** an: OAuth-Flow, `user_integrations`-Tabelle, > Azure App-Registrierung. Erst wenn das steht, kann Phase 3 beginnen. **3.1 E-Mail Tab** — `GET /crm/contacts/:id/emails` (Proxy zu MS Graph API, Redis 5 Min Cache) **3.2 Kalender Tab** — `GET /crm/contacts/:id/calendar` (Graph API, Redis Cache) **3.3 Aufgaben Sync** — Bidirektional mit Microsoft To Do (@Cron alle 5 Min, Redis-Lock pro User) **3.4 Kontakte Export** — `POST /crm/contacts/:id/export-to-outlook` (Graph API) Details: Abschnitte B.1–B.4 oben #### Phase 4 — NICE-TO-HAVE **4.1 Visitenkarten-Scan** — Anthropic Vision API, Rate Limit 50/Tag/Tenant **4.2 CRM Reporting** — 5 Dashboards + CSV-Export (Pipeline, Aktivitaeten, Kontaktwachstum, Win/Loss, Mitarbeiter-Performance) ### Wichtige technische Hinweise 1. **Schema-Abgleich noetig:** Das Ziel-Schema des Architekten (A.10) weicht vom aktuellen Prisma-Schema ab. Aktuell: flaches Contact/Company-Modell. Ziel: `crm_contacts` + `crm_persons` + `crm_companies` Split. **Entscheidung noetig:** Schrittweise migrieren oder Big-Bang-Umbau? 2. **User-Referenzen:** Contact/Deal-Owner referenzieren `user_id` aus `platform_core`. Keine User-Daten kopieren — bei Bedarf per REST-Call an Core-Service (`GET /api/users/:id`) oder JWT-Payload nutzen. 3. **Neue Rollen:** Das Berechtigungsmodell (A.8) definiert `team_lead` und `tenant_readonly` — diese Rollen existieren im Core noch NICHT. Abstimmung mit Core-Entwickler noetig. 4. **Bestehende Frontend-Hooks:** Das Frontend hat bereits React Query Hooks fuer alle bestehenden Endpoints. Neue Endpoints/Felder muessen in `packages/frontend/src/crm/types.ts`, `api.ts` und `hooks.ts` ergaenzt werden — das uebernimmt der Frontend-Entwickler nach Rueckmeldung. ### Kommunikationsweg Bitte fuer jede abgeschlossene Phase einen Eintrag hier in diese Datei schreiben: - Welche Endpoints sind neu/geaendert? - Welche Prisma-Migrationen wurden ausgefuehrt? - Gibt es Breaking Changes an bestehenden APIs? - Welche Frontend-Anpassungen sind noetig? Format: `## YYYY-MM-DD | CRM-Backend: [Betreff]` --- ## 2026-03-12 | CRM-Backend: Phase 1 — Schema-Expansion, Owner, Lost-Reason, Events ### Zusammenfassung Umsetzung der Phase 1 ("SOFORT") gemaess Architektur-Briefing. Alle Arbeitspakete ohne externe Abhaengigkeiten implementiert. ### 1. Prisma Schema + SQL Migration **Neue Enums (7):** `ContactSource`, `EntityStatus`, `CompanySize`, `OwnerRole`, `LostReason`, `EmailType`, `PhoneType` + `FOLLOWUP` zu ActivityType. **Neue Felder auf Contact:** `linkedinUrl`, `birthday`, `source` (ContactSource), `department`, `status` (EntityStatus). **Neue Felder auf Company:** `vatId`, `taxId`, `tradeRegisterNumber`, `registerCourt`, `companySize`, `deliveryStreet/Zip/City/Country`, `dataEnrichedAt`, `dataEnrichedSource`, `status` (EntityStatus). **Neue Felder auf Deal:** `lostReason` (LostReason), `lostReasonText`. **Neue Tabellen (5):** `contact_emails`, `contact_phones`, `contact_owners`, `company_owners`, `deal_owners`. **Migration:** `prisma/migrations/20260312_phase1_schema_expansion/migration.sql` — inkl. Daten-Migration (isActive→status, email/phone→contact_emails/phones, ownerId→company_owners). ### 2. Multi-Value E-Mails & Telefone (Breaking Change) Bisherige Einzel-Felder `email`, `phone`, `mobile` bleiben als **deprecated Legacy** erhalten. Neue Multi-Value Tabellen `contact_emails` und `contact_phones` dienen als primaere Datenquelle. Legacy-Felder werden automatisch aus Primary-Eintraegen synchronisiert. **Neue Endpoints/Verhalten:** - `POST /contacts` akzeptiert jetzt `emails: [{email, type, isPrimary}]` und `phones: [{phone, type, isPrimary}]` - `PATCH /contacts/:id` mit Replace-Strategie (delete all + recreate) - Analog fuer Companies - Lexware-Import erzeugt automatisch Multi-Value Eintraege - `status` Feld (ACTIVE/INACTIVE/BLOCKED) ersetzt `isActive` Boolean — beide werden synchron gehalten **DTOs:** - `CreateEmailDto`: `{email, type?: EmailType, isPrimary?: boolean}` - `CreatePhoneDto`: `{phone, type?: PhoneType, isPrimary?: boolean}` - `EntityStatus`: ACTIVE, INACTIVE, BLOCKED - `CompanySize`: SIZE_1_10, SIZE_11_50, SIZE_51_200, SIZE_201_500, SIZE_500_PLUS - `ContactSource`: TRADE_FAIR, REFERRAL, WEBSITE, COLD_CALL, IMPORT, BUSINESS_CARD, OTHER ### 3. Owner m:n Modell Jeder Contact, Company und Deal hat jetzt ein mehrstufiges Ownership-Modell mit Rollen. **Neue Endpoints:** ``` POST /crm/contacts/:id/owners Body: {userId, role?} DELETE /crm/contacts/:id/owners/:userId POST /crm/companies/:id/owners Body: {userId, role?} DELETE /crm/companies/:id/owners/:userId POST /crm/deals/:id/owners Body: {userId, role?} DELETE /crm/deals/:id/owners/:userId ``` **OwnerRole Enum:** OWNER, MEMBER, WATCHER. Bei `create()` wird der erstellende User automatisch als OWNER eingetragen. Upsert-Verhalten: Wenn Owner bereits existiert, wird nur die Rolle aktualisiert. ### 4. Lost-Reason bei Deals **Neue Felder:** `lostReason` (Enum: PRICE, TIMING, COMPETITOR, NO_NEED, OTHER), `lostReasonText` (Freitext). **Validierung:** - `PATCH /deals/:id` mit `status: 'LOST'` erfordert `lostReason` (im DTO oder bereits gesetzt) — sonst 400 BadRequest. - `PATCH /deals/:id` mit `status: 'WON'` loescht automatisch `lostReason` und `lostReasonText`. ### 5. Redis Pub/Sub Events **CrmEventPublisher** (global verfuegbar): Publiziert Events auf Redis Channels im Format `app:crm:events:{type}`. **Event-Typen:** - `crm.contact.created` — nach Contact-Erstellung - `crm.contact.updated` — nach Contact-Update - `crm.deal.created` — nach Deal-Erstellung - `crm.deal.stage_changed` — bei Stage-Aenderung (payload: previousStageId, newStageId) - `crm.deal.won` — bei Deal WON (payload: value, currency) - `crm.deal.lost` — bei Deal LOST (payload: lostReason) - `crm.activity.due_soon` — Activities faellig in 24h (Cron alle 15 Min) **Event-Payload:** ```json { "type": "crm.contact.created", "tenantId": "uuid", "entityId": "uuid", "userId": "uuid", "timestamp": "ISO-8601", "payload": {} } ``` ### Response-Aenderungen fuer Frontend Alle Entity-Responses (Contact, Company, Deal) enthalten jetzt zusaetzlich: - `emails: [{id, email, type, isPrimary, createdAt}]` - `phones: [{id, phone, type, isPrimary, createdAt}]` - `owners: [{id, tenantId, userId, role, createdAt}]` - `status: 'ACTIVE' | 'INACTIVE' | 'BLOCKED'` Contact zusaetzlich: `linkedinUrl`, `birthday`, `source`, `department`. Company zusaetzlich: `vatId`, `taxId`, `tradeRegisterNumber`, `registerCourt`, `companySize`, `deliveryStreet/Zip/City/Country`. Deal zusaetzlich: `lostReason`, `lostReasonText`. ### Geaenderte Dateien (18) + Neue Dateien (10) Siehe Plan-Datei fuer komplette Dateiliste. Wichtigste neue Dateien: - `src/common/dto/contact-info.dto.ts` — Shared DTOs + Enums - `src/common/dto/owner.dto.ts` — AddOwnerDto + OwnerRole - `src/owners/owners.service.ts` + `owners.module.ts` — Shared Owner CRUD - `src/events/crm-event-publisher.service.ts` — CRM Event Publisher - `src/events/crm-events.module.ts` — Global Events Module - `src/events/activity-due-soon.scheduler.ts` — Cron-Job ### TODO fuer Frontend 1. `types.ts` um neue Felder erweitern (emails[], phones[], owners[], status, linkedinUrl, birthday, source, department, lostReason, etc.) 2. Contact/Company Formulare um Multi-Value Email/Phone Eingabe erweitern 3. Owner-Management UI (Zuweisen/Entfernen von Owners) 4. Deal-Detail: Lost-Reason Feld bei Status LOST einblenden 5. EntityStatus Filter in Listen verwenden (statt isActive) --- ## 2026-03-12 | Frontend: Phase 1 Frontend-Integration abgeschlossen ### Zusammenfassung Alle Backend-Aenderungen aus Phase 1 sind vollstaendig ins Frontend integriert. Commit `48df3c3`, deployed auf dem Server. ### Was wurde umgesetzt | Bereich | Aenderung | |---------|-----------| | `types.ts` | 7 neue Enums + Label-Maps, erweiterte Interfaces (Contact, Company, Deal), neue Interfaces (ContactEmail, ContactPhone, EntityOwner) | | `api.ts` | 6 Owner-Endpoints (add/remove fuer Contact, Company, Deal) | | `hooks.ts` | 6 Owner-Mutation-Hooks mit Cache-Invalidierung | | `ContactFormModal` | Neue Felder: LinkedIn, Geburtstag, Quelle, Abteilung, Status | | `ContactDetailPage` | Anzeige: LinkedIn, Abteilung, Geburtstag, Quelle, Status | | `CompanyDetailPage` | Anzeige: Lieferadresse, USt-IdNr, Steuernummer, Handelsregister, Groesse, Anreicherungsdatum | | `DealFormModal` | Lost-Reason Dropdown + Freitext (Pflicht bei Status LOST) | | `DealDetailPage` | Lost-Reason Anzeige mit Label + optionalem Freitext | | `CompaniesPage` | EntityStatus statt isActive (ACTIVE=gruen, BLOCKED=rot, INACTIVE=orange) | | `ActivityFeed` + `ActivityFormModal` + `ContactDetailPage` | FOLLOWUP als ActivityType ergaenzt | ### Noch nicht umgesetzt (spaetere Iteration) - **Multi-Value E-Mail/Telefon UI**: Backend synct automatisch mit Legacy-Feldern (`email`, `phone`, `mobile`). Dediziertes Multi-Value-UI kommt spaeter. - **Owner-Management UI**: Hooks/API sind fertig, dedizierte UI-Komponente (Zuweisen/Entfernen) kommt spaeter. - **Status-Filter Dropdown**: Status-Dots in Listen aktualisiert, aber noch kein Filter-Dropdown in der Suchleiste. --- ## 2026-03-12 | Plattform-Admin: Briefing Phase 2 fuer CRM-Backend-Experten ### Kontext Phase 1 ist komplett abgeschlossen (Backend + Frontend deployed). Die naechste Prioritaet fuer das CRM-Backend ist **Phase 2** — neue Features ohne CORE-Abhaengigkeiten. ### Aktueller Stand nach Phase 1 | Modul | Status | |-------|--------| | Contacts (Person + Company) | Laeuft — mit erweiterten Feldern (LinkedIn, Geburtstag, Quelle, Status, Multi-Value E-Mail/Telefon) | | Companies (Standalone) | Laeuft — mit USt-IdNr, Handelsregister, Groesse, Lieferadresse, Lexware-Sync | | Deals | Laeuft — mit Lost-Reason (Enum + Freitext), Owner-Modell | | Pipelines + Stages | Laeuft | | Activities | Laeuft — 6 Typen inkl. FOLLOWUP, Scheduler fuer faellige Aktivitaeten | | Owner-Modell | Laeuft — m:n fuer Contacts, Companies, Deals (OWNER/MEMBER/WATCHER) | | Redis Pub/Sub Events | Laeuft — 7 Event-Typen | | Industries | Laeuft | | Trade Events (Messe-Timer) | Laeuft | | CRM Settings | Laeuft | ### Phase 2 Aufgaben — Priorisierte Reihenfolge --- #### 2.1 Custom Fields System (HOECHSTE PRIO) **Warum zuerst:** Groesster Mehrwert — Mandanten koennen Felder selbst konfigurieren, ohne Schema-Aenderungen oder Deployments. **Ziel-Schema (2 Tabellen):** ``` crm_custom_field_defs id UUID PK tenant_id UUID NOT NULL -> FK tenants entity_type ENUM('PERSON', 'COMPANY', 'DEAL') name VARCHAR -- interner Name (slug, auto-generiert) label VARCHAR -- Anzeigename field_type ENUM('TEXT', 'TEXTAREA', 'NUMBER', 'DATE', 'DROPDOWN', 'MULTI_SELECT', 'CHECKBOX', 'URL') options JSONB -- nur fuer DROPDOWN/MULTI_SELECT: [{value: "A", label: "Segment A"}, ...] is_required BOOLEAN DEFAULT false position INT DEFAULT 0 -- Sortierung created_at TIMESTAMP updated_at TIMESTAMP UNIQUE(tenant_id, entity_type, name) crm_custom_field_values id UUID PK tenant_id UUID NOT NULL -> FK tenants field_def_id UUID NOT NULL -> FK crm_custom_field_defs entity_id UUID NOT NULL -- Contact/Company/Deal ID value_text TEXT value_number DECIMAL value_date TIMESTAMP value_boolean BOOLEAN value_json JSONB -- fuer MULTI_SELECT created_at TIMESTAMP updated_at TIMESTAMP UNIQUE(field_def_id, entity_id) ``` **Endpoints (6):** | Methode | Pfad | Beschreibung | |---------|------|-------------| | `GET` | `/crm/custom-fields?entityType=PERSON` | Alle Feld-Definitionen (gefiltert nach Entity-Typ) | | `POST` | `/crm/custom-fields` | Neue Feld-Definition anlegen | | `PATCH` | `/crm/custom-fields/:id` | Definition bearbeiten (Label, Position, Required, Options) | | `DELETE` | `/crm/custom-fields/:id` | Definition loeschen (CASCADE auf Values!) | | `PUT` | `/crm/custom-fields/:entityId/values` | Alle Custom-Field-Werte fuer eine Entity setzen/aktualisieren (Bulk-Upsert) | | `GET` | `/crm/custom-fields/:entityId/values` | Alle Custom-Field-Werte fuer eine Entity lesen | **Wichtige Regeln:** - `name` wird aus `label` automatisch generiert (slugify: "Kundennummer" -> "kundennummer"). Muss pro Tenant+EntityType unique sein. - Bei `DELETE` einer Definition: Alle zugehoerigen Values loeschen (Cascade). - Custom Fields in Entity-Detail-Responses mitliefern (neues Feld `customFields` als Array). - Position bestimmt Reihenfolge in Formularen — bei Drag&Drop im Admin wird nur `position` per PATCH aktualisiert. - Validierung: Bei `is_required: true` pruefen, ob Wert gesetzt ist (beim Entity-Speichern). - Bei DROPDOWN/MULTI_SELECT: `options` muss ein JSON-Array mit `{value, label}` Objekten sein. **DTOs:** ```typescript // CreateCustomFieldDto { entityType: 'PERSON' | 'COMPANY' | 'DEAL' // @IsEnum label: string // @IsString, @MinLength(1) fieldType: 'TEXT' | 'TEXTAREA' | 'NUMBER' | 'DATE' | 'DROPDOWN' | 'MULTI_SELECT' | 'CHECKBOX' | 'URL' options?: { value: string; label: string }[] // Pflicht bei DROPDOWN/MULTI_SELECT isRequired?: boolean // Default: false position?: number // Default: 0 } // UpdateCustomFieldDto — PartialType(CreateCustomFieldDto) OHNE entityType und fieldType // (Typ-Aenderung wuerde bestehende Values invalidieren) // SetCustomFieldValuesDto { values: { fieldDefId: string // @IsUUID value: string | number | boolean | string[] | null }[] } ``` **Response-Format in Entity-Details:** ```json { "id": "...", "firstName": "Max", "customFields": [ { "fieldDefId": "uuid", "label": "Kundennummer", "fieldType": "TEXT", "value": "KD-12345" }, { "fieldDefId": "uuid", "label": "DSGVO-Einwilligung", "fieldType": "CHECKBOX", "value": true } ] } ``` --- #### 2.2 Kontakt-Import (CSV, Excel, vCard) **Warum wichtig:** Kunden wollen bestehende Daten migrieren. Ohne Import kein produktiver Einsatz. **Ablauf (3 Schritte):** 1. **Upload:** User laedt Datei hoch -> Backend parst und gibt Spalten + erste 10 Zeilen als Vorschau zurueck 2. **Mapping:** User ordnet Datei-Spalten den CRM-Feldern zu -> Backend fuehrt Import aus 3. **Ergebnis:** Backend gibt Zusammenfassung zurueck (erstellt, aktualisiert, uebersprungen, Fehler) **Endpoints (3):** | Methode | Pfad | Beschreibung | |---------|------|-------------| | `POST` | `/crm/import/preview` | Datei hochladen, Vorschau generieren. Body: `multipart/form-data` mit `file` + `type` (csv/xlsx/vcf) | | `POST` | `/crm/import/execute` | Import ausfuehren mit Spalten-Mapping | | `GET` | `/crm/import/history` | Letzte Imports mit Status (optional, nice-to-have) | **Preview-Response:** ```json { "columns": ["Name", "E-Mail", "Telefon", "Firma"], "rows": [ ["Max Mustermann", "max@firma.de", "+49 123", "Firma GmbH"], ["..."] ], "totalRows": 250, "format": "csv" } ``` **Execute-Request:** ```json { "importId": "uuid-aus-preview", "entityType": "PERSON", "mapping": { "Name": "lastName", "E-Mail": "email", "Telefon": "phone", "Firma": "companyName" }, "duplicateStrategy": "SKIP" | "UPDATE" | "MARK", "options": { "skipFirstRow": true } } ``` **Execute-Response:** ```json { "created": 180, "updated": 45, "skipped": 20, "errors": 5, "errorDetails": [ { "row": 42, "field": "email", "message": "Ungueltige E-Mail-Adresse" } ] } ``` **Unterstuetzte Formate:** - **CSV:** Standard-Parsing mit auto-detect Delimiter (`,` / `;` / `\t`). Encoding: UTF-8 mit BOM-Erkennung. - **XLSX:** Erstes Sheet lesen. Bibliothek-Empfehlung: `xlsx` oder `exceljs` (was bereits in Node-Abhaengigkeiten ist). - **vCard (.vcf):** Einzeln oder als ZIP mit mehreren .vcf Dateien. Mapping vorgegeben (vCard-Felder -> CRM-Felder). **Duplikat-Erkennung:** Primaer ueber `email`-Feld. Wenn E-Mail bereits existiert (im gleichen Tenant): - `SKIP`: Zeile ueberspringen - `UPDATE`: Bestehenden Kontakt aktualisieren (nur nicht-leere Felder ueberschreiben) - `MARK`: Importieren und als potentielles Duplikat markieren (spaeter manuell aufloesen) **Wichtig:** - Maximal 5.000 Zeilen pro Import (konfigurierbar pro Tenant via CRM Settings) - Temporaere Datei nach Import loeschen (DSGVO) - Import laeuft synchron bei <500 Zeilen, asynchron mit Status-Polling bei >500 - vCard-Import: Mapping ist fix (kein User-Mapping noetig), aber Vorschau trotzdem zeigen --- #### 2.3 Forecast-Endpoint **Warum:** Vertriebsleitung braucht Uebersicht ueber erwarteten Umsatz. **Endpoint:** ``` GET /crm/deals/forecast?pipelineId=uuid&period=quarter|month|year ``` **Response:** ```json { "pipeline": "Vertrieb", "period": "2026-Q1", "stages": [ { "stageId": "uuid", "stageName": "Qualifiziert", "probability": 0.3, "dealCount": 12, "totalValue": 150000, "weightedValue": 45000 } ], "totals": { "dealCount": 45, "totalValue": 520000, "weightedValue": 198000 } } ``` **Logik:** - `weightedValue = totalValue * probability` - `probability` kommt aus der Stage-Definition (neues Feld `probability: DECIMAL DEFAULT 0` in `crm_pipeline_stages` ergaenzen!) - Nur Deals mit Status `OPEN` zaehlen - `period`-Filter: basiert auf `expectedCloseDate` - Wenn `pipelineId` fehlt: alle Pipelines zusammenfassen **Schema-Aenderung:** Neues Feld in `crm_pipeline_stages`: ``` probability DECIMAL(3,2) DEFAULT 0 -- 0.00 bis 1.00 ``` Default-Werte bei bestehenden Stages auf 0 setzen. Frontend-Entwickler ergaenzt Wahrscheinlichkeits-Eingabe im Pipeline-Admin. --- #### 2.4 Firmendaten-Anreicherung (Data Enrichment) **Warum:** Automatisiertes Befuellen von Firmendaten spart manuelle Recherche. **Endpoint:** ``` POST /crm/companies/:id/enrich ``` **Response (Vorschlagsdaten, nicht automatisch uebernommen):** ```json { "source": "north_data", "suggestions": { "street": { "current": "", "suggested": "Industriestr. 5", "source": "north_data" }, "city": { "current": "Muenchen", "suggested": "Muenchen", "source": "north_data" }, "vatId": { "current": "", "suggested": "DE123456789", "source": "unternehmensregister" }, "tradeRegisterNumber": { "current": "", "suggested": "HRB 12345", "source": "unternehmensregister" }, "registerCourt": { "current": "", "suggested": "AG Muenchen", "source": "unternehmensregister" }, "industry": { "current": null, "suggested": "Software & IT", "source": "north_data" }, "companySize": { "current": null, "suggested": "SIZE_51_200", "source": "north_data" }, "websiteUrl": { "current": "", "suggested": "https://firma.de", "source": "north_data" } }, "enrichedAt": "2026-03-12T10:30:00Z" } ``` **Implementierung:** 1. Unternehmensregister.de abfragen (kostenlos, Scraping/API — Firmenname als Suchbegriff) 2. North Data API abfragen (API-Key aus Tenant-Settings, `GET https://www.northdata.de/api/v1/company?name=...`) 3. Ergebnisse zusammenfuehren (North Data hat Vorrang bei Konflikten) 4. Timeout pro Quelle: 10 Sekunden, bei Fehler einzelner Quelle: verfuegbare Daten zurueckgeben 5. `dataEnrichedAt` + `dataEnrichedSource` in Company aktualisieren (NACH User-Bestaetigung, d.h. beim naechsten PATCH) **Admin-Einstellung noetig:** - North Data API-Key pro Tenant speichern (`crm_settings` Tabelle oder eigene `crm_integrations` Tabelle) - Endpoint: `GET/PUT /crm/settings/integrations/north-data` mit `{ apiKey: string }` **Hinweis:** Dieser Endpoint kann initial nur mit Unternehmensregister.de umgesetzt werden (kostenlos). North Data wird aktiviert, sobald der Tenant einen API-Key hinterlegt hat. --- #### 2.5 Berechtigungsmodell (NIEDRIGSTE PRIO in Phase 2) **Warum niedrigere Prio:** Aktuell gibt es wenige User pro Tenant. Wird wichtiger bei Wachstum. **Konzept:** Sichtbarkeitsfilter in allen List-Queries basierend auf Owner-Zugehoerigkeit. | Stufe | Beschreibung | |-------|-------------| | `OWN` | User sieht nur Entities, bei denen er Owner/Member ist | | `TEAM` | User sieht alle Entities seiner Abteilung (benoetigt `department`-Feld am User — Abstimmung mit Core noetig!) | | `ALL` | User sieht alle Entities des Tenants (aktuelles Verhalten) | **Schema-Aenderung:** Neue Tabelle oder Erweiterung von `crm_settings`: ``` crm_visibility_settings id UUID PK tenant_id UUID NOT NULL role VARCHAR -- 'tenant_admin', 'team_lead', 'tenant_member', 'tenant_readonly' visibility ENUM('OWN', 'TEAM', 'ALL') can_create BOOLEAN DEFAULT true can_edit ENUM('OWN', 'TEAM', 'ALL') can_delete ENUM('OWN', 'TEAM', 'ALL') UNIQUE(tenant_id, role) ``` **Implementierung:** - In allen `findAll`/`findOne` Service-Methoden: Sichtbarkeitsfilter basierend auf User-Rolle + Tenant-Settings - `TenantGuard` erweitern oder eigenen `VisibilityGuard` erstellen - Default-Werte: `tenant_admin` -> ALL/ALL/ALL, `tenant_member` -> ALL (bisheriges Verhalten beibehalten!) - Endpoint: `GET/PUT /crm/settings/visibility` fuer Admin-Konfiguration **ACHTUNG — Abhaengigkeit:** Die Rolle `team_lead` und `tenant_readonly` existieren im Core noch NICHT. Fuer Phase 2 reicht es, das Berechtigungsmodell mit den bestehenden Rollen (`tenant_admin`, `tenant_member`) zu implementieren. Erweiterung auf neue Rollen erfolgt nach Abstimmung mit dem Core-Entwickler. --- ### Empfohlene Reihenfolge ``` 2.1 Custom Fields ━━━━━━━━━━━━━━━━━━━━━ (1-2 Tage) | 2.2 Kontakt-Import ━━━━━━━━━━━━━━━━━━━━━ | parallel moeglich | 2.3 Forecast ━━━━━━━━━━━━━ | | 2.4 Data Enrichment ━━━━━━━━━━━━━━━ | | 2.5 Berechtigungen ━━━━━━━━━━━━━━━━━━ | (erst wenn 2.1-2.4 stehen) ``` **2.1 und 2.2 koennen parallel entwickelt werden** — sie haben keine Abhaengigkeiten zueinander. 2.3 (Forecast) benoetigt die Schema-Aenderung an `crm_pipeline_stages` (Probability-Feld). 2.4 (Enrichment) ist eigenstaendig. 2.5 (Berechtigungen) sollte als letztes kommen, da es alle List-Queries aendert. ### Technische Hinweise 1. **Prisma-Schema:** Alle neuen Tabellen in `packages/crm-service/prisma/crm.schema.prisma`. Migration mit `prisma db push` oder eigener SQL-Migration. 2. **Modul-Struktur:** Fuer jedes Feature ein eigenes NestJS-Modul: - `src/custom-fields/` — CustomFieldsModule (Controller, Service, DTOs) - `src/import/` — ImportModule (Controller, Service, DTOs) - `src/forecast/` — ForecastModule oder als Teil von DealsModule (nur 1 Endpoint) - `src/enrichment/` — EnrichmentModule (Controller, Service) 3. **File Upload (Import):** NestJS `@UseInterceptors(FileInterceptor('file'))` mit `@UploadedFile()`. Maximal 10 MB. Temporaere Dateien in `/tmp/` mit UUID-Prefix, nach Verarbeitung loeschen. 4. **Testing:** Bei Import besonders wichtig: Edge Cases (leere Zeilen, Sonderzeichen, fehlende Pflichtfelder, doppelte E-Mails). 5. **Bestehende Frontend-Hooks:** Das Frontend hat Query-Key-Factories und Hook-Patterns etabliert. Fuer jedes neue Modul erstelle ich (Frontend) die entsprechenden Hooks, sobald die Endpoints stehen. **Bitte pro abgeschlossenem Feature einen Eintrag hier schreiben.** ### Kommunikationsweg Bitte fuer jedes abgeschlossene Feature einen Eintrag in diese Datei schreiben: ``` ## YYYY-MM-DD | CRM-Backend: Phase 2.X — [Feature-Name] ### Neue Endpoints (Methode, Pfad, kurze Beschreibung) ### Schema-Aenderungen (neue Tabellen/Felder) ### Response-Aenderungen (was aendert sich an bestehenden Responses?) ### TODO fuer Frontend ``` **Reihenfolge der Rueckmeldungen:** Am besten jedes Feature einzeln melden, dann kann ich (Frontend) parallel integrieren. Nicht alles auf einmal. --- *Bitte neue Eintraege unten anfuegen. Format: `## YYYY-MM-DD | Absender: Betreff`* --- ## 2026-03-12 | CRM-Backend: Phase 2.1 — Custom Fields System ### Neue Endpoints | Methode | Pfad | Beschreibung | |---------|------|-------------| | `POST` | `/api/v1/crm/custom-fields` | Feld-Definition erstellen | | `GET` | `/api/v1/crm/custom-fields?entityType=PERSON` | Alle Feld-Definitionen auflisten (optional nach Entity-Typ gefiltert) | | `GET` | `/api/v1/crm/custom-fields/:id` | Einzelne Feld-Definition abrufen | | `PATCH` | `/api/v1/crm/custom-fields/:id` | Definition aktualisieren (Label, Options, Position, Required) | | `DELETE` | `/api/v1/crm/custom-fields/:id` | Definition loeschen (CASCADE auf alle gespeicherten Werte!) | | `PUT` | `/api/v1/crm/custom-fields/:entityId/values` | Custom-Field-Werte fuer eine Entity setzen (Bulk-Upsert) | | `GET` | `/api/v1/crm/custom-fields/:entityId/values` | Custom-Field-Werte fuer eine Entity lesen | ### Schema-Aenderungen **Neue Enums:** - `CustomFieldEntityType`: PERSON, COMPANY, DEAL - `CustomFieldType`: TEXT, TEXTAREA, NUMBER, DATE, DROPDOWN, MULTI_SELECT, CHECKBOX, URL **Neue Tabellen:** | Tabelle | Beschreibung | |---------|-------------| | `crm_custom_field_defs` | Feld-Definitionen pro Tenant + Entity-Typ. Unique: `[tenant_id, entity_type, name]`. Felder: entityType, name (Auto-Slug), label, fieldType (immutable), options (JSONB fuer DROPDOWN/MULTI_SELECT), isRequired, position | | `crm_custom_field_values` | Gespeicherte Werte. Unique: `[field_def_id, entity_id]`. Spalten pro Typ: valueText, valueNumber, valueDate, valueBoolean, valueJson. FK auf Definitionen mit CASCADE Delete | **SQL Migration:** `prisma/migrations/20260312_phase2_custom_fields/migration.sql` ### Response-Aenderungen **Contact-Detail, Company-Detail, Deal-Detail** enthalten jetzt ein neues Feld `customFields`: ```json { "id": "...", "firstName": "Max", "customFields": [ { "fieldDefId": "uuid", "name": "kundennummer", "label": "Kundennummer", "fieldType": "TEXT", "value": "KD-12345" }, { "fieldDefId": "uuid", "name": "segment", "label": "Segment", "fieldType": "DROPDOWN", "value": "A" }, { "fieldDefId": "uuid", "name": "newsletter", "label": "Newsletter", "fieldType": "CHECKBOX", "value": null } ] } ``` Das Array enthaelt ALLE definierten Custom Fields fuer den jeweiligen Entity-Typ (auch ohne Wert = `value: null`), sortiert nach `position`. Das Frontend bekommt so immer die vollstaendige Feldliste fuer Formular-Rendering. ### Value-Mapping nach Feldtyp | FieldType | JS-Typ | Beispiel | |-----------|--------|---------| | TEXT, TEXTAREA, URL | string | `"KD-12345"` | | NUMBER | number | `42.5` | | DATE | string (ISO) | `"2026-01-15T00:00:00.000Z"` | | CHECKBOX | boolean | `true` | | DROPDOWN | string (single select) | `"premium"` | | MULTI_SELECT | string[] | `["tag-a", "tag-b"]` | ### Wichtige Regeln - **name** (interner Slug) wird automatisch aus dem Label generiert (Umlaute: ae/oe/ue/ss). Unique pro Tenant + Entity-Typ. - **entityType** und **fieldType** sind nach Erstellung **nicht mehr aenderbar** (wuerde bestehende Werte invalidieren). - **DROPDOWN/MULTI_SELECT** erfordern `options`: `[{value: "a", label: "Segment A"}, ...]`. Bei Wert-Speicherung wird gegen die Options validiert. - **DELETE** einer Definition loescht automatisch alle gespeicherten Werte (CASCADE). - **Orphan-Cleanup**: Beim Loeschen eines Contacts/Company/Deal werden zugehoerige Custom-Field-Werte automatisch entfernt. - **isRequired** wird aktuell nur client-seitig ausgewertet (das Flag wird mitgeliefert). Server-seitige Pflichtfeld-Pruefung kann spaeter ergaenzt werden. ### ~~TODO fuer Frontend~~ — ERLEDIGT (2026-03-12) Alle 4 Punkte wurden im Frontend umgesetzt und deployed (Commit `aaedf68`). --- ## 2026-03-12 | Frontend: Phase 2.1 Custom Fields — Frontend-Integration ### Was wurde umgesetzt Die komplette Frontend-Integration fuer Custom Fields ist implementiert und auf dem Server deployed. #### Neue Dateien (2 Dateien) ``` packages/frontend/src/crm/ CustomFieldsDisplay.tsx -- Read-only Anzeige von Custom Fields in Detail-Pages CustomFieldsForm.tsx -- Editierbare Custom Fields in Entity-Formularen ``` #### Geaenderte Dateien (10 Dateien) | Datei | Aenderung | |-------|-----------| | `types.ts` | Neue Typen: `CustomFieldEntityType`, `CustomFieldType`, `CustomFieldDef`, `CustomFieldValue`, `CustomFieldOption`, Payload-Interfaces. `customFields?: CustomFieldValue[]` zu Contact/Company/Deal hinzugefuegt. | | `api.ts` | `customFieldsApi`-Objekt mit 7 Methoden: listDefs, getDef, createDef, updateDef, deleteDef, getValues, setValues | | `hooks.ts` | 6 neue Hooks: `useCustomFieldDefs`, `useCreateCustomFieldDef`, `useUpdateCustomFieldDef`, `useDeleteCustomFieldDef`, `useCustomFieldValues`, `useSetCustomFieldValues`. Query-Key-Factory `crmKeys.customFields`. | | `settings/CrmSettingsPage.tsx` | Neuer Tab "Eigene Felder" mit `CustomFieldsConfig`-Komponente (~490 Zeilen): Entity-Typ-Filter, Add/Edit-Formular, Options-Editor fuer DROPDOWN/MULTI_SELECT, Sortierung, Loeschen mit Warnung. | | `contacts/ContactDetailPage.tsx` | `CustomFieldsDisplay` nach Notizen-Sektion eingebunden | | `companies/CompanyDetailPage.tsx` | `CustomFieldsDisplay` nach Notizen-Sektion eingebunden | | `deals/DealDetailPage.tsx` | `CustomFieldsDisplay` nach Notizen-Sektion eingebunden | | `contacts/ContactFormModal.tsx` | `CustomFieldsForm` + `useSetCustomFieldValues` integriert, `saveCustomFields` in onSuccess-Handlers | | `companies/CompanyFormModal.tsx` | Gleiche Integration wie ContactFormModal | | `deals/DealFormModal.tsx` | Gleiche Integration wie ContactFormModal | ### Architektur-Entscheidungen 1. **Zwei getrennte Komponenten**: `CustomFieldsDisplay` (read-only, Detail-Pages) und `CustomFieldsForm` (editierbar, Formulare) — saubere Trennung von Anzeige und Bearbeitung. 2. **Ref-basierter State in Formularen**: `customFieldValuesRef` statt useState, um unnoetige Re-Renders zu vermeiden. Werte werden erst beim Submit ausgelesen. 3. **Post-Save-Pattern**: Custom Fields werden NACH dem Entity-Save gespeichert (`PUT /custom-fields/:entityId/values`), da bei Create die Entity-ID erst aus der Response kommt. 4. **DROPDOWN Fallback**: `CustomFieldValue` liefert keine Options-Liste (nur Definitionen haben sie). DROPDOWN rendert daher als Text-Input. Verbesserung: Options aus Definitionen nachladen. ### Unterstuetzte Feldtypen (alle 8) | Typ | Display (Detail) | Form (Formular) | |-----|-------------------|------------------| | TEXT | String | `` | | TEXTAREA | Pre-wrap String | `