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

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

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

134 KiB
Raw Blame History

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):

{
  "success": true,
  "data": [...],
  "pagination": { "page": 1, "pageSize": 25, "total": 42, "totalPages": 2 },
  "meta": { "timestamp": "..." }
}

Einzelobjekt:

{
  "success": true,
  "data": { ... },
  "meta": { "timestamp": "..." }
}

Fehler:

{
  "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

  • Traefik HTTPS-Router: crm-secure Router angelegt mit entrypoints=websecure, tls=true, Priority 100. Deployed in Commit c9e2c4a.

  • Pipeline-Stages bearbeiten: Neuer Endpoint PATCH /crm/pipelines/:id/stages/:stageId hinzugefuegt. Akzeptiert:

    {
      "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:

{
  "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

  • Pipeline-Stages bearbeiten — Frontend nutzt den neuen PATCH-Endpoint
  • 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)

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):

{
  "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:

{
  "company": { "id": "...", "name": "Xinion GmbH", "industry": "Enterprise Software" }
}

Contact-Detail liefert:

{
  "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:

{
  "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

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:

{
  "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[]:

{
  "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:

{
  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.

{
  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:

{
  "relatedCompanyId": "uuid",
  "relationshipTypeId": "uuid",
  "notes": "optional"
}

GET-Response (einzelne Beziehung):

{
  "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:

{
  "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 regeneriertprisma 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

{
  "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

  • Migration auf Server anwenden — Erledigt (beide Migrationen)
  • Container neu bauen und deployen — Frontend + CRM Backend deployed
  • 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:

// 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:

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:

-- 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:

+ 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:

+ 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 TabGET /crm/contacts/:id/emails (Proxy zu MS Graph API, Redis 5 Min Cache) 3.2 Kalender TabGET /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 ExportPOST /crm/contacts/:id/export-to-outlook (Graph API)

Details: Abschnitte B.1B.4 oben

Phase 4 — NICE-TO-HAVE

4.1 Visitenkarten-Scan — Anthropic Vision API, Rate Limit 50/Tag/Tenant 4.2 CRM Reporting — 5 Dashboards + CSV-Export (Pipeline, Aktivitaeten, Kontaktwachstum, Win/Loss, Mitarbeiter-Performance)

Wichtige technische Hinweise

  1. Schema-Abgleich noetig: Das Ziel-Schema des Architekten (A.10) weicht vom aktuellen Prisma-Schema ab. Aktuell: flaches Contact/Company-Modell. Ziel: crm_contacts + crm_persons + crm_companies Split. Entscheidung noetig: Schrittweise migrieren oder Big-Bang-Umbau?

  2. User-Referenzen: Contact/Deal-Owner referenzieren user_id aus platform_core. Keine User-Daten kopieren — bei Bedarf per REST-Call an Core-Service (GET /api/users/:id) oder JWT-Payload nutzen.

  3. Neue Rollen: Das Berechtigungsmodell (A.8) definiert team_lead und tenant_readonly — diese Rollen existieren im Core noch NICHT. Abstimmung mit Core-Entwickler noetig.

  4. Bestehende Frontend-Hooks: Das Frontend hat bereits React Query Hooks fuer alle bestehenden Endpoints. Neue Endpoints/Felder muessen in packages/frontend/src/crm/types.ts, api.ts und hooks.ts ergaenzt werden — das uebernimmt der Frontend-Entwickler nach Rueckmeldung.

Kommunikationsweg

Bitte fuer jede abgeschlossene Phase einen Eintrag hier in diese Datei schreiben:

  • Welche Endpoints sind neu/geaendert?
  • Welche Prisma-Migrationen wurden ausgefuehrt?
  • Gibt es Breaking Changes an bestehenden APIs?
  • Welche Frontend-Anpassungen sind noetig?

Format: ## YYYY-MM-DD | CRM-Backend: [Betreff]


2026-03-12 | CRM-Backend: Phase 1 — Schema-Expansion, Owner, Lost-Reason, Events

Zusammenfassung

Umsetzung der Phase 1 ("SOFORT") gemaess Architektur-Briefing. Alle Arbeitspakete ohne externe Abhaengigkeiten implementiert.

1. Prisma Schema + SQL Migration

Neue Enums (7): ContactSource, EntityStatus, CompanySize, OwnerRole, LostReason, EmailType, PhoneType + FOLLOWUP zu ActivityType.

Neue Felder auf Contact: linkedinUrl, birthday, source (ContactSource), department, status (EntityStatus).

Neue Felder auf Company: vatId, taxId, tradeRegisterNumber, registerCourt, companySize, deliveryStreet/Zip/City/Country, dataEnrichedAt, dataEnrichedSource, status (EntityStatus).

Neue Felder auf Deal: lostReason (LostReason), lostReasonText.

Neue Tabellen (5): contact_emails, contact_phones, contact_owners, company_owners, deal_owners.

Migration: prisma/migrations/20260312_phase1_schema_expansion/migration.sql — inkl. Daten-Migration (isActive→status, email/phone→contact_emails/phones, ownerId→company_owners).

2. Multi-Value E-Mails & Telefone (Breaking Change)

Bisherige Einzel-Felder email, phone, mobile bleiben als deprecated Legacy erhalten. Neue Multi-Value Tabellen contact_emails und contact_phones dienen als primaere Datenquelle. Legacy-Felder werden automatisch aus Primary-Eintraegen synchronisiert.

Neue Endpoints/Verhalten:

  • POST /contacts akzeptiert jetzt emails: [{email, type, isPrimary}] und phones: [{phone, type, isPrimary}]
  • PATCH /contacts/:id mit Replace-Strategie (delete all + recreate)
  • Analog fuer Companies
  • Lexware-Import erzeugt automatisch Multi-Value Eintraege
  • status Feld (ACTIVE/INACTIVE/BLOCKED) ersetzt isActive Boolean — beide werden synchron gehalten

DTOs:

  • CreateEmailDto: {email, type?: EmailType, isPrimary?: boolean}
  • CreatePhoneDto: {phone, type?: PhoneType, isPrimary?: boolean}
  • EntityStatus: ACTIVE, INACTIVE, BLOCKED
  • CompanySize: SIZE_1_10, SIZE_11_50, SIZE_51_200, SIZE_201_500, SIZE_500_PLUS
  • ContactSource: TRADE_FAIR, REFERRAL, WEBSITE, COLD_CALL, IMPORT, BUSINESS_CARD, OTHER

3. Owner m:n Modell

Jeder Contact, Company und Deal hat jetzt ein mehrstufiges Ownership-Modell mit Rollen.

Neue Endpoints:

POST   /crm/contacts/:id/owners          Body: {userId, role?}
DELETE /crm/contacts/:id/owners/:userId

POST   /crm/companies/:id/owners         Body: {userId, role?}
DELETE /crm/companies/:id/owners/:userId

POST   /crm/deals/:id/owners             Body: {userId, role?}
DELETE /crm/deals/:id/owners/:userId

OwnerRole Enum: OWNER, MEMBER, WATCHER.

Bei create() wird der erstellende User automatisch als OWNER eingetragen. Upsert-Verhalten: Wenn Owner bereits existiert, wird nur die Rolle aktualisiert.

4. Lost-Reason bei Deals

Neue Felder: lostReason (Enum: PRICE, TIMING, COMPETITOR, NO_NEED, OTHER), lostReasonText (Freitext).

Validierung:

  • PATCH /deals/:id mit status: 'LOST' erfordert lostReason (im DTO oder bereits gesetzt) — sonst 400 BadRequest.
  • PATCH /deals/:id mit status: 'WON' loescht automatisch lostReason und lostReasonText.

5. Redis Pub/Sub Events

CrmEventPublisher (global verfuegbar): Publiziert Events auf Redis Channels im Format app:crm:events:{type}.

Event-Typen:

  • crm.contact.created — nach Contact-Erstellung
  • crm.contact.updated — nach Contact-Update
  • crm.deal.created — nach Deal-Erstellung
  • crm.deal.stage_changed — bei Stage-Aenderung (payload: previousStageId, newStageId)
  • crm.deal.won — bei Deal WON (payload: value, currency)
  • crm.deal.lost — bei Deal LOST (payload: lostReason)
  • crm.activity.due_soon — Activities faellig in 24h (Cron alle 15 Min)

Event-Payload:

{
  "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:

// 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:

{
  "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:

{
  "columns": ["Name", "E-Mail", "Telefon", "Firma"],
  "rows": [
    ["Max Mustermann", "max@firma.de", "+49 123", "Firma GmbH"],
    ["..."]
  ],
  "totalRows": 250,
  "format": "csv"
}

Execute-Request:

{
  "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:

{
  "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:

{
  "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):

{
  "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:

{
  "id": "...",
  "firstName": "Max",
  "customFields": [
    {
      "fieldDefId": "uuid",
      "name": "kundennummer",
      "label": "Kundennummer",
      "fieldType": "TEXT",
      "value": "KD-12345"
    },
    {
      "fieldDefId": "uuid",
      "name": "segment",
      "label": "Segment",
      "fieldType": "DROPDOWN",
      "value": "A"
    },
    {
      "fieldDefId": "uuid",
      "name": "newsletter",
      "label": "Newsletter",
      "fieldType": "CHECKBOX",
      "value": null
    }
  ]
}

Das Array enthaelt ALLE definierten Custom Fields fuer den jeweiligen Entity-Typ (auch ohne Wert = value: null), sortiert nach position. Das Frontend bekommt so immer die vollstaendige Feldliste fuer Formular-Rendering.

Value-Mapping nach Feldtyp

FieldType JS-Typ Beispiel
TEXT, TEXTAREA, URL string "KD-12345"
NUMBER number 42.5
DATE string (ISO) "2026-01-15T00:00:00.000Z"
CHECKBOX boolean true
DROPDOWN string (single select) "premium"
MULTI_SELECT string[] ["tag-a", "tag-b"]

Wichtige Regeln

  • name (interner Slug) wird automatisch aus dem Label generiert (Umlaute: ae/oe/ue/ss). Unique pro Tenant + Entity-Typ.
  • entityType und fieldType sind nach Erstellung nicht mehr aenderbar (wuerde bestehende Werte invalidieren).
  • DROPDOWN/MULTI_SELECT erfordern options: [{value: "a", label: "Segment A"}, ...]. Bei Wert-Speicherung wird gegen die Options validiert.
  • DELETE einer Definition loescht automatisch alle gespeicherten Werte (CASCADE).
  • Orphan-Cleanup: Beim Loeschen eines Contacts/Company/Deal werden zugehoerige Custom-Field-Werte automatisch entfernt.
  • isRequired wird aktuell nur client-seitig ausgewertet (das Flag wird mitgeliefert). Server-seitige Pflichtfeld-Pruefung kann spaeter ergaenzt werden.

TODO fuer Frontend — ERLEDIGT (2026-03-12)

Alle 4 Punkte wurden im Frontend umgesetzt und deployed (Commit aaedf68).


2026-03-12 | Frontend: Phase 2.1 Custom Fields — Frontend-Integration

Was wurde umgesetzt

Die komplette Frontend-Integration fuer Custom Fields ist implementiert und auf dem Server deployed.

Neue Dateien (2 Dateien)

packages/frontend/src/crm/
  CustomFieldsDisplay.tsx    -- Read-only Anzeige von Custom Fields in Detail-Pages
  CustomFieldsForm.tsx       -- Editierbare Custom Fields in Entity-Formularen

Geaenderte Dateien (10 Dateien)

Datei Aenderung
types.ts Neue Typen: CustomFieldEntityType, CustomFieldType, CustomFieldDef, CustomFieldValue, CustomFieldOption, Payload-Interfaces. customFields?: CustomFieldValue[] zu Contact/Company/Deal hinzugefuegt.
api.ts customFieldsApi-Objekt mit 7 Methoden: listDefs, getDef, createDef, updateDef, deleteDef, getValues, setValues
hooks.ts 6 neue Hooks: useCustomFieldDefs, useCreateCustomFieldDef, useUpdateCustomFieldDef, useDeleteCustomFieldDef, useCustomFieldValues, useSetCustomFieldValues. Query-Key-Factory crmKeys.customFields.
settings/CrmSettingsPage.tsx Neuer Tab "Eigene Felder" mit CustomFieldsConfig-Komponente (~490 Zeilen): Entity-Typ-Filter, Add/Edit-Formular, Options-Editor fuer DROPDOWN/MULTI_SELECT, Sortierung, Loeschen mit Warnung.
contacts/ContactDetailPage.tsx CustomFieldsDisplay nach Notizen-Sektion eingebunden
companies/CompanyDetailPage.tsx CustomFieldsDisplay nach Notizen-Sektion eingebunden
deals/DealDetailPage.tsx CustomFieldsDisplay nach Notizen-Sektion eingebunden
contacts/ContactFormModal.tsx CustomFieldsForm + useSetCustomFieldValues integriert, saveCustomFields in onSuccess-Handlers
companies/CompanyFormModal.tsx Gleiche Integration wie ContactFormModal
deals/DealFormModal.tsx Gleiche Integration wie ContactFormModal

Architektur-Entscheidungen

  1. Zwei getrennte Komponenten: CustomFieldsDisplay (read-only, Detail-Pages) und CustomFieldsForm (editierbar, Formulare) — saubere Trennung von Anzeige und Bearbeitung.
  2. Ref-basierter State in Formularen: customFieldValuesRef statt useState, um unnoetige Re-Renders zu vermeiden. Werte werden erst beim Submit ausgelesen.
  3. Post-Save-Pattern: Custom Fields werden NACH dem Entity-Save gespeichert (PUT /custom-fields/:entityId/values), da bei Create die Entity-ID erst aus der Response kommt.
  4. DROPDOWN Fallback: CustomFieldValue liefert keine Options-Liste (nur Definitionen haben sie). DROPDOWN rendert daher als Text-Input. Verbesserung: Options aus Definitionen nachladen.

Unterstuetzte Feldtypen (alle 8)

Typ Display (Detail) Form (Formular)
TEXT String <input type="text">
TEXTAREA Pre-wrap String <textarea>
NUMBER Locale-formatiert (de-DE) <input type="number">
DATE Formatiert (dd.MM.yyyy) <input type="date">
CHECKBOX "Ja" / "Nein" <input type="checkbox">
URL Klickbarer Link <input type="url">
DROPDOWN String <input type="text"> (Fallback)
MULTI_SELECT Farbige Badges Tag-Input mit Enter-to-Add

Admin-Bereich (CRM Einstellungen > Eigene Felder)

  • Entity-Typ-Filter: Buttons fuer Kontakte/Unternehmen/Vorgaenge
  • Feld hinzufuegen: Label, Feldtyp (Dropdown), Pflichtfeld-Toggle
  • Options-Editor: Fuer DROPDOWN und MULTI_SELECT — dynamische Wert/Label-Paare
  • Inline-Bearbeitung: Label, Pflichtfeld, Options aendern
  • Sortierung: Hoch/Runter-Buttons fuer Position
  • Loeschen: Mit Warnung ueber Cascading (alle gespeicherten Werte werden geloescht)

Deployment-Notiz

Das Docker-Volume /app/node_modules im CRM-Container cached den alten Prisma-Client. Bei Schema-Aenderungen muss der Container mit -V Flag neu erstellt werden:

docker compose -f docker-compose.yml -f docker-compose.crm.yml up -d crm --force-recreate -V

Offene Punkte / Verbesserungen

  • DROPDOWN/MULTI_SELECT im Formular: Options aus Feld-Definitionen laden statt Freitext-Fallback
  • isRequired client-seitig validieren (Flag wird geliefert, Validierung noch nicht implementiert)
  • Drag & Drop fuer Position-Sortierung (aktuell: Hoch/Runter-Buttons)

2026-03-12 | Architekt: Backend-Briefing Phase 2.2, 2.3, 2.4

Parallelisierungsplan

Backend und Frontend arbeiten parallel an allen drei Phasen. Die API-Contracts sind unten festgelegt — Frontend baut UI gegen diese Contracts, Backend implementiert die Endpoints. Integration-Test wenn beide fertig sind.

Phase 2.3 (Forecast):     Backend ━━━━━  Frontend ━━━━━  (parallel)
Phase 2.2 (Import):       Backend ━━━━━━━━━  Frontend ━━━━━━━━━  (parallel)
Phase 2.4 (Enrichment):   Backend ━━━━━━━  Frontend ━━━━━  (parallel)

Reihenfolge: 2.3 → 2.2 → 2.4 (nach aufsteigendem Aufwand).

Alle drei Phasen sind unabhaengig voneinander — keine Abhaengigkeiten zwischen den Phasen.


Phase 2.3: Forecasting (Prioritaet 1 — kleinstes Delta)

Schema-Aenderung

Neues Feld in PipelineStage:

model PipelineStage {
  // ... bestehende Felder ...
  probability Decimal @default(0) @db.Decimal(3,2)  // NEU: 0.00 bis 1.00
}

Migration: 20260312_phase23_forecast

ALTER TABLE app_crm.pipeline_stages
  ADD COLUMN probability DECIMAL(3,2) NOT NULL DEFAULT 0;

DTO-Aenderungen:

  • CreatePipelineStageDto: Neues optionales Feld probability?: number (0-1, @Min(0) @Max(1))
  • UpdatePipelineStageDto: Neues optionales Feld probability?: number
  • Bestehende Stage-Responses muessen probability enthalten

Neuer Endpoint: Forecast

GET /crm/deals/forecast?pipelineId=uuid&period=quarter|month|year

Platzierung: Im DealsController als eigene Route VOR der :id-Route, damit kein Routing-Konflikt entsteht. Alternativ: eigener ForecastController (bevorzugt fuer saubere Trennung).

Query-Parameter:

Param Typ Default Beschreibung
pipelineId UUID (optional) alle Nur Deals dieser Pipeline
period quarter / month / year quarter Zeitraum-Gruppierung

Logik:

  1. Alle Deals mit status = 'OPEN' laden (im Tenant)
  2. Optional nach pipelineId filtern
  3. Period-Filter auf expectedCloseDate:
    • quarter: Aktuelles Quartal (z.B. Q1 2026 = Jan-Mrz)
    • month: Aktueller Monat
    • year: Aktuelles Jahr
  4. Nach Stage gruppieren
  5. Pro Stage: dealCount, totalValue (Summe value), weightedValue (totalValue * stage.probability)
  6. Totals ueber alle Stages berechnen

Response-Contract:

{
  "success": true,
  "data": {
    "pipeline": "Vertrieb",
    "pipelineId": "uuid-oder-null",
    "period": "2026-Q1",
    "periodStart": "2026-01-01T00:00:00Z",
    "periodEnd": "2026-03-31T23:59:59Z",
    "stages": [
      {
        "stageId": "uuid",
        "stageName": "Qualifiziert",
        "probability": 0.3,
        "dealCount": 12,
        "totalValue": 150000.00,
        "weightedValue": 45000.00
      },
      {
        "stageId": "uuid",
        "stageName": "Angebot",
        "probability": 0.6,
        "dealCount": 8,
        "totalValue": 200000.00,
        "weightedValue": 120000.00
      }
    ],
    "totals": {
      "dealCount": 45,
      "totalValue": 520000.00,
      "weightedValue": 198000.00
    }
  },
  "meta": { "timestamp": "2026-03-12T18:00:00Z" }
}

Wenn pipelineId nicht angegeben: pipeline = "Alle Pipelines", pipelineId = null, Deals aus allen Pipelines zusammengefasst.

TODO Backend (Phase 2.3)

  • Prisma-Schema: probability Feld in PipelineStage
  • Migration erstellen und anwenden
  • CreatePipelineStageDto + UpdatePipelineStageDto erweitern
  • Pipeline-Stage-Responses: probability mitliefern (findAll, findOne)
  • Forecast-Endpoint implementieren (eigener Controller oder in DealsController)
  • Response-Contract exakt wie oben

Phase 2.2: CSV/Excel Import (Prioritaet 2 — hoechster User-Value)

Neue Dependencies

npm install csv-parser xlsx
npm install -D @types/multer

(multer ist bereits in @nestjs/platform-express enthalten)

Neues Modul

src/import/
  import.module.ts
  import.controller.ts
  import.service.ts
  dto/
    import-preview.dto.ts
    import-execute.dto.ts

Registrierung in app.module.ts: imports: [..., ImportModule]

Endpoint 1: Preview

POST /crm/import/preview
Content-Type: multipart/form-data

Form Fields:

Field Typ Beschreibung
file File CSV, XLSX oder vCard Datei
entityType String PERSON, COMPANY oder DEAL

NestJS-Pattern:

@Post('preview')
@UseInterceptors(FileInterceptor('file', {
  limits: { fileSize: 10 * 1024 * 1024 }, // 10 MB
  fileFilter: (req, file, cb) => {
    const allowed = ['.csv', '.xlsx', '.xls', '.vcf'];
    const ext = extname(file.originalname).toLowerCase();
    cb(null, allowed.includes(ext));
  },
}))
async preview(
  @CurrentUser() user: JwtPayload,
  @UploadedFile() file: Express.Multer.File,
  @Body('entityType') entityType: string,
)

Verarbeitungs-Logik:

  1. Format erkennen (Extension oder MIME-Type)
  2. CSV: Auto-detect Delimiter (, ; \t). UTF-8 mit BOM-Erkennung. Erste Zeile = Header.
  3. XLSX: Erstes Sheet lesen. Erste Zeile = Header.
  4. vCard (.vcf): Felder extrahieren (FN, N, EMAIL, TEL, ORG, ADR, URL, NOTE)
  5. Datei mit UUID-Prefix in /tmp/ speichern fuer spaetere Verarbeitung
  6. Erste 10 Datenzeilen + Spaltennamen + Gesamtzeilenzahl zurueckgeben

Response-Contract:

{
  "success": true,
  "data": {
    "importId": "uuid",
    "format": "csv",
    "columns": ["Name", "E-Mail", "Telefon", "Firma", "PLZ", "Stadt"],
    "rows": [
      ["Max Mustermann", "max@firma.de", "+49 123 456", "Firma GmbH", "80331", "Muenchen"],
      ["Anna Schmidt", "anna@example.com", "+49 987 654", "Schmidt AG", "10115", "Berlin"]
    ],
    "totalRows": 250,
    "availableTargetFields": {
      "PERSON": [
        { "field": "firstName", "label": "Vorname" },
        { "field": "lastName", "label": "Nachname" },
        { "field": "email", "label": "E-Mail" },
        { "field": "phone", "label": "Telefon" },
        { "field": "mobile", "label": "Mobil" },
        { "field": "companyName", "label": "Firmenname" },
        { "field": "position", "label": "Position" },
        { "field": "department", "label": "Abteilung" },
        { "field": "street", "label": "Strasse" },
        { "field": "zip", "label": "PLZ" },
        { "field": "city", "label": "Stadt" },
        { "field": "country", "label": "Land" },
        { "field": "website", "label": "Website" },
        { "field": "linkedinUrl", "label": "LinkedIn URL" },
        { "field": "notes", "label": "Notizen" },
        { "field": "tags", "label": "Tags (kommasepariert)" }
      ],
      "COMPANY": [
        { "field": "name", "label": "Firmenname" },
        { "field": "email", "label": "E-Mail" },
        { "field": "phone", "label": "Telefon" },
        { "field": "website", "label": "Website" },
        { "field": "street", "label": "Strasse" },
        { "field": "zip", "label": "PLZ" },
        { "field": "city", "label": "Stadt" },
        { "field": "country", "label": "Land" },
        { "field": "vatId", "label": "USt-IdNr." },
        { "field": "tradeRegisterNumber", "label": "Handelsregisternr." },
        { "field": "registerCourt", "label": "Registergericht" },
        { "field": "notes", "label": "Notizen" },
        { "field": "tags", "label": "Tags (kommasepariert)" }
      ]
    }
  }
}

Hinweis: availableTargetFields liefert die moeglichen Zielfelder fuer den Entity-Typ, damit das Frontend das Mapping-Dropdown befuellen kann.

Endpoint 2: Execute

POST /crm/import/execute
Content-Type: application/json

Request-Body:

{
  "importId": "uuid-aus-preview",
  "entityType": "PERSON",
  "mapping": {
    "Name": "lastName",
    "E-Mail": "email",
    "Telefon": "phone",
    "Firma": "companyName",
    "PLZ": "zip",
    "Stadt": "city"
  },
  "duplicateStrategy": "SKIP",
  "options": {
    "skipFirstRow": true
  }
}

DTO-Validierung:

  • importId: @IsUUID()
  • entityType: @IsEnum(['PERSON', 'COMPANY', 'DEAL'])
  • mapping: Record<string, string> — Key = Datei-Spalte, Value = CRM-Feld
  • duplicateStrategy: @IsEnum(['SKIP', 'UPDATE', 'MARK'])
  • options.skipFirstRow: @IsBoolean() @IsOptional()

Verarbeitungs-Logik:

  1. Temp-Datei ueber importId laden
  2. Alle Zeilen parsen
  3. Pro Zeile: Felder gemaess Mapping zuordnen
  4. Duplikat-Erkennung ueber email Feld (case-insensitive):
    • SKIP: Zeile ueberspringen, Zaehler erhoehen
    • UPDATE: Bestehenden Datensatz aktualisieren (nur nicht-leere Felder ueberschreiben)
    • MARK: Importieren und Tag IMPORT_DUPLICATE setzen
  5. Validierung pro Zeile (E-Mail-Format, Pflichtfelder je nach entityType)
  6. Batch-Insert via Prisma (nicht einzeln!)
  7. DSGVO: Temp-Datei nach Verarbeitung LOESCHEN
  8. Import-Statistik zurueckgeben

Response-Contract:

{
  "success": true,
  "data": {
    "created": 180,
    "updated": 45,
    "skipped": 20,
    "errors": 5,
    "totalProcessed": 250,
    "errorDetails": [
      { "row": 42, "field": "email", "value": "ungueltig", "message": "Ungueltige E-Mail-Adresse" },
      { "row": 87, "field": "lastName", "value": "", "message": "Nachname ist ein Pflichtfeld" }
    ]
  }
}

Constraints:

  • Max 5.000 Zeilen pro Import — bei mehr: 400 Bad Request mit Fehlermeldung
  • Max 10 MB Dateigroesse (im FileInterceptor konfiguriert)
  • Synchron bei <500 Zeilen
  • Bei >500 Zeilen: Synchron verarbeiten ist OK fuer Alpha (async spaeter)

Endpoint 3: Import History (Nice-to-Have)

GET /crm/import/history

Optional — nur wenn Zeit uebrig. Zeigt letzte 20 Imports fuer den Tenant.

TODO Backend (Phase 2.2)

  • Dependencies installieren: csv-parser, xlsx
  • ImportModule + Controller + Service + DTOs erstellen
  • In app.module.ts registrieren
  • Preview-Endpoint: CSV/XLSX/vCard Parsing, Temp-Datei-Management
  • Execute-Endpoint: Mapping, Duplikat-Erkennung, Batch-Insert
  • Temp-Dateien nach Verarbeitung loeschen (DSGVO)
  • Response-Contracts exakt wie oben

Phase 2.4: Datenanreicherung (Prioritaet 3)

Neues Modul

src/enrichment/
  enrichment.module.ts
  enrichment.controller.ts
  enrichment.service.ts

Registrierung in app.module.ts: imports: [..., EnrichmentModule]

Endpoint 1: Company Enrichment

POST /crm/companies/:id/enrich

Logik:

  1. Company aus DB laden (Name, aktuelle Felder)
  2. Unternehmensregister.de abfragen (kostenlos, immer verfuegbar):
    • Suche nach Firmenname
    • Felder: Handelsregisternr., Registergericht, Rechtsform, Sitz
    • Timeout: 10 Sekunden
  3. North Data API abfragen (nur wenn API-Key konfiguriert):
    • GET https://www.northdata.de/api/v1/company?name={name}&country=DE
    • Header: X-Api-Key: {apiKey}
    • Felder: Adresse, Branche, Umsatz, Mitarbeiterzahl, Website
    • Timeout: 10 Sekunden
  4. Ergebnisse zusammenfuehren (North Data hat Vorrang bei Konflikten)
  5. NUR Vorschlaege zurueckgeben — NICHT automatisch in DB schreiben!
  6. Bei Fehler einer Quelle: verfuegbare Daten der anderen Quelle zurueckgeben

Response-Contract:

{
  "success": true,
  "data": {
    "companyId": "uuid",
    "sources": ["unternehmensregister", "north_data"],
    "suggestions": {
      "street": {
        "current": "",
        "suggested": "Industriestr. 5",
        "source": "north_data"
      },
      "city": {
        "current": "Muenchen",
        "suggested": "Muenchen",
        "source": "north_data",
        "match": true
      },
      "zip": {
        "current": "",
        "suggested": "80339",
        "source": "north_data"
      },
      "vatId": {
        "current": "",
        "suggested": "DE123456789",
        "source": "unternehmensregister"
      },
      "tradeRegisterNumber": {
        "current": "",
        "suggested": "HRB 12345",
        "source": "unternehmensregister"
      },
      "registerCourt": {
        "current": "",
        "suggested": "AG Muenchen",
        "source": "unternehmensregister"
      },
      "industry": {
        "current": null,
        "suggested": "Software & IT",
        "source": "north_data"
      },
      "companySize": {
        "current": null,
        "suggested": "SIZE_51_200",
        "source": "north_data"
      },
      "website": {
        "current": "",
        "suggested": "https://firma.de",
        "source": "north_data"
      }
    },
    "enrichedAt": "2026-03-12T18:30:00Z",
    "warnings": []
  }
}

Hinweis: match: true zeigt an, dass der aktuelle Wert mit dem Vorschlag uebereinstimmt (kein Update noetig). Nur Felder mit suggested !== null werden in der Response mitgegeben.

Endpoint 2: North Data API-Key Config

GET /crm/settings/integrations/north-data

Response:

{
  "success": true,
  "data": {
    "configured": true,
    "apiKey": "****e4f8"
  }
}
PUT /crm/settings/integrations/north-data

Request:

{ "apiKey": "nd_abc123..." }

Speicherung: In Redis unter Key crm:{tenantId}:integrations:north_data oder in einer neuen crm_integrations-Tabelle.

Wichtig: dataEnrichedAt / dataEnrichedSource

Diese Felder existieren bereits im Company-Schema (Phase 1):

dataEnrichedAt     DateTime? @map("data_enriched_at")
dataEnrichedSource String?   @map("data_enriched_source") @db.VarChar(200)

Sie werden NICHT vom Enrich-Endpoint aktualisiert. Stattdessen setzt das Frontend sie beim naechsten PATCH /crm/companies/:id wenn der User Vorschlaege uebernimmt:

{
  "street": "Industriestr. 5",
  "dataEnrichedAt": "2026-03-12T18:30:00Z",
  "dataEnrichedSource": "north_data,unternehmensregister"
}

TODO Backend (Phase 2.4)

  • EnrichmentModule + Controller + Service erstellen
  • In app.module.ts registrieren
  • Unternehmensregister.de Abfrage implementieren (Scraping oder API)
  • North Data API Integration (mit API-Key aus Settings)
  • Settings-Endpoints fuer API-Key (GET/PUT)
  • Response-Contract exakt wie oben
  • Timeouts + Fehlerbehandlung pro Quelle

Hinweise fuer den Backend-Entwickler

  1. Reihenfolge: Phase 2.3 zuerst (kleinstes Delta), dann 2.2, dann 2.4
  2. Patterns: Alle bestehenden Patterns beibehalten (TenantGuard, singleResponse/paginatedResponse, class-validator DTOs, Swagger-Dekoratoren)
  3. Docker-Volume: Bei Schema-Aenderungen Container mit -V neu erstellen:
    docker compose -f docker-compose.yml -f docker-compose.crm.yml up -d crm --force-recreate -V
    
  4. Tests: Import-Edge-Cases besonders gruendlich testen (leere Zeilen, Sonderzeichen, fehlende Pflichtfelder, doppelte E-Mails)
  5. DSGVO: Import-Temp-Dateien MUESSEN nach Verarbeitung geloescht werden
  6. Completion-Report: Nach jeder Phase einen Eintrag in dieser Datei ergaenzen (wie bei Phase 2.1)

Phase 2.3 Backend — Forecast (DONE)

Implementiert am: 2026-03-12

Schema-Aenderung

  • PipelineStage.probability — Decimal(3,2), Default 0, Bereich 0.001.00
  • Migration: prisma/migrations/20260312_phase23_forecast/migration.sql

Neue Endpoints

Methode Pfad Beschreibung
GET /api/v1/crm/deals/forecast?pipelineId=&period= Umsatz-Forecast (gewichtete Pipeline)

Forecast Response

{
  "success": true,
  "data": {
    "period": "quarter",
    "periodStart": "2026-01-01T00:00:00.000Z",
    "periodEnd": "2026-03-31T23:59:59.999Z",
    "currency": "EUR",
    "stages": [
      {
        "stageId": "uuid",
        "stageName": "Qualifiziert",
        "stageColor": "#3B82F6",
        "probability": 0.25,
        "sortOrder": 1,
        "pipelineId": "uuid",
        "pipelineName": "Sales",
        "dealCount": 5,
        "totalValue": 50000,
        "weightedValue": 12500
      }
    ],
    "totals": {
      "dealCount": 15,
      "totalValue": 150000,
      "weightedValue": 67500
    }
  }
}

Query-Parameter

  • pipelineId (optional, UUID) — Filter nach Pipeline
  • period (optional, enum: month|quarter|year, default: quarter)

DTO-Updates

  • CreatePipelineStageDto.probability — Optional, @IsNumber, @Min(0), @Max(1)
  • UpdateStageDto.probability — Optional, gleiche Validierung

Geaenderte Dateien

  • prisma/crm.schema.prisma — probability auf PipelineStage
  • src/pipelines/dto/create-pipeline.dto.ts — probability-Feld
  • src/pipelines/dto/update-stage.dto.ts — probability-Feld
  • src/pipelines/pipelines.service.ts — probability in create/addStage/updateStage
  • src/pipelines/pipelines.controller.ts — probability durchreichen
  • src/deals/dto/forecast-query.dto.ts — NEU: ForecastQueryDto + ForecastPeriod Enum
  • src/deals/deals.service.ts — NEU: forecast() + getPeriodBounds()
  • src/deals/deals.controller.ts — NEU: GET /deals/forecast (vor :id Route)

TODO Frontend

  • Forecast-Seite (/crm/forecast) — Pipeline-Filter, Zeitraum-Toggle (Monat/Quartal/Jahr), Tabelle mit gewichteten Werten + Summary-Cards
  • probability-Feld in Pipeline-Stage-Editor (0100% Input in PipelinesPage)

Phase 2.2 Backend — CSV/Excel Import (DONE)

Implementiert am: 2026-03-12

Neue Dependencies

  • csv-parser: ^3.0.0 — CSV-Parser mit Stream-Support
  • xlsx: ^0.18.5 — Excel-Parser (XLSX/XLS)
  • @types/multer: ^1.4.12 (devDep)

Neue Endpoints

Methode Pfad Beschreibung
POST /api/v1/crm/import/preview Datei-Vorschau (Multipart, CSV/XLSX)
POST /api/v1/crm/import/execute Import ausfuehren

Preview (Multipart POST)

  • File-Upload: file Feld, max 10MB, MIME: text/csv, application/vnd.ms-excel, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
  • Query-Params: entityType (contact|company), delimiter (optional)
  • Max Rows: 5000
  • Preview Rows: Erste 10
{
  "success": true,
  "data": {
    "importId": "uuid",
    "format": "csv",
    "columns": ["Vorname", "Nachname", "E-Mail", "Telefon"],
    "rows": [{"Vorname": "Max", "Nachname": "Muster", ...}],
    "totalRows": 250,
    "availableTargetFields": ["firstName", "lastName", "email", ...]
  }
}

Execute (JSON POST)

{
  "importId": "uuid-from-preview",
  "entityType": "contact",
  "mapping": [
    {"sourceColumn": "Vorname", "targetField": "firstName"},
    {"sourceColumn": "E-Mail", "targetField": "email"}
  ],
  "duplicateStrategy": "SKIP"
}

Duplikat-Strategien (E-Mail-Abgleich, case-insensitive):

  • SKIP — Zeile ueberspringen
  • UPDATE — Bestehenden Datensatz aktualisieren
  • MARK — Neuen Datensatz mit Tag "DUPLIKAT" erstellen

Response:

{
  "success": true,
  "data": {
    "created": 230,
    "updated": 0,
    "skipped": 15,
    "errors": 5,
    "totalProcessed": 250,
    "errorDetails": [{"row": 42, "field": "", "value": "", "message": "..."}]
  }
}

Target Fields

  • Contact: firstName, lastName, email, phone, mobile, companyName, position, department, website, street, zip, city, state, country, notes, tags, source, linkedinUrl
  • Company: name, email, phone, website, industry, street, zip, city, state, country, vatId, taxId, tradeRegisterNumber, registerCourt, notes, tags

GDPR

  • Temp-Dateien werden unter /tmp/crm-import-{uuid}{ext} gespeichert
  • Nach Execute werden Temp-Dateien IMMER geloescht (finally-Block)
  • Owner wird automatisch auf den importierenden User gesetzt

Neue Dateien

  • src/import/import.module.ts
  • src/import/import.controller.ts
  • src/import/import.service.ts
  • src/import/dto/import-preview.dto.ts
  • src/import/dto/import-execute.dto.ts

TODO Frontend

  • Import-Wizard (3 Schritte: Upload → Spalten-Mapping → Ergebnis) in CRM-Einstellungen → Tab "Import"
  • Import-Ergebnis-Anzeige (created/updated/skipped/errors mit Fehlerdetails)
  • entityType-Konvertierung: PERSON→contact, COMPANY→company
  • mapping-Konvertierung: Record→Array [{sourceColumn, targetField}]

Phase 2.4 Backend — Datenanreicherung / Enrichment (DONE)

Implementiert am: 2026-03-12

Neue Endpoints

Methode Pfad Beschreibung
POST /api/v1/crm/companies/:id/enrich Unternehmensdaten anreichern (Suggestion-Only!)
GET /api/v1/crm/settings/integrations/north-data North Data Einstellungen abrufen
PUT /api/v1/crm/settings/integrations/north-data North Data Einstellungen aktualisieren

Datenquellen

  1. Unternehmensregister.de (kostenfrei) — Handelsregister-Daten: registerNumber, registerCourt, Adresse
  2. North Data API (kostenpflichtig, optional) — Erweiterte Daten: name, vatId, website, phone, Adresse, industry

Beide Quellen werden parallel abgefragt (Promise.allSettled). Graceful Degradation: Wenn eine Quelle fehlschlaegt, werden Ergebnisse der anderen zurueckgegeben.

Enrich Response (Suggestion-Only, KEIN Auto-Write)

{
  "success": true,
  "data": {
    "companyId": "uuid",
    "companyName": "Muster GmbH",
    "sources": ["unternehmensregister.de", "northdata.de"],
    "suggestions": {
      "tradeRegisterNumber": {
        "current": null,
        "suggested": "HRB 12345",
        "source": "unternehmensregister.de"
      },
      "vatId": {
        "current": null,
        "suggested": "DE123456789",
        "source": "northdata.de"
      }
    },
    "enrichedAt": "2026-03-12T14:30:00.000Z",
    "warnings": []
  }
}

North Data Settings

  • Gespeichert in Redis: crm:{tenantId}:integrations:north_data
  • Fallback: Env-Variable NORTH_DATA_API_KEY
  • API-Key wird in GET-Response maskiert: ****xxxx

Env-Variablen (optional)

Health-Check

  • /health Response enthaelt neuen Service: enrichment: 'up' | 'down' | 'unconfigured'
  • Version auf 0.3.0 erhoeht

Neue Dateien

  • src/enrichment/enrichment.module.ts
  • src/enrichment/enrichment.controller.ts
  • src/enrichment/enrichment.service.ts
  • src/enrichment/dto/enrich-response.dto.ts
  • src/enrichment/dto/enrichment-settings.dto.ts

Geaenderte Dateien

  • src/app.module.ts — EnrichmentModule registriert
  • src/config/env.validation.ts — NORTH_DATA_API_KEY + _URL
  • src/health/health.module.ts — EnrichmentModule importiert
  • src/health/health.controller.ts — enrichment Health-Check

TODO Frontend

  • "Anreichern"-Button auf Company-Detail — POST /enrich, Suggestions-Modal (Tabelle mit Feld/Aktuell/Vorschlag/Quelle), "Uebernehmen" per PATCH
  • suggestions-Konvertierung: Record<field,{current,suggested,source}> → Array fuer UI
  • North Data API-Key in CRM-Einstellungen → Tab "Integrationen"

2026-03-12 | Frontend: Phase 2 abgeschlossen — Naechste Aufgaben fuer CRM-Backend

Phase 2 Status: KOMPLETT

Alle vier Phase-2-Features sind backend- und frontendseitig implementiert und auf insight-dev-01 deployed.

Phase Backend Frontend Deployed
2.1 Custom Fields DONE DONE Ja
2.2 CSV/Excel Import DONE DONE Ja
2.3 Forecasting DONE DONE Ja
2.4 Datenanreicherung DONE DONE Ja

Phase 3 (Microsoft 365 OAuth) ist BLOCKIERT bis Core-Service OAuth liefert.


Naechste Aufgaben fuer den CRM-Backend-Entwickler

Prioritaet 1: Vertraege-API (Contract CRUD)

Das Datenmodell (Contract) ist bereits im Prisma-Schema vorhanden, aber es fehlen die Endpoints. Das Frontend hat eine ContractsCard.tsx die aktuell nur ein Platzhalter-UI anzeigt.

Benoetigt:

GET    /api/v1/crm/companies/:id/contracts        — Liste der Vertraege einer Firma
POST   /api/v1/crm/companies/:id/contracts        — Vertrag anlegen
PATCH  /api/v1/crm/companies/:id/contracts/:cid   — Vertrag bearbeiten
DELETE /api/v1/crm/companies/:id/contracts/:cid   — Vertrag loeschen

Contract-Felder: title, status (DRAFT|ACTIVE|EXPIRED|CANCELLED), startDate, endDate, value (Decimal), currency (Default EUR), notes

Response-Format: Standard { success, data, meta } wie alle anderen Endpoints.

Frontend-Hinweis: Die ContractsCard.tsx und die Typen (Contract, ContractStatus) sind bereits im Frontend definiert (packages/frontend/src/crm/types.ts). Nach Implementierung der Endpoints muss der Frontend-Entwickler nur noch die Hooks + API-Calls hinzufuegen.


Prioritaet 2: Kanban-Board Support (Deal-Stage-Move)

Das Backend ist bereits bereit (PATCH /crm/deals/:id mit { stageId }). Der Frontend-Entwickler kann das Kanban-Board ohne Backend-Aenderungen implementieren.

Kein Backend-Handlungsbedarf.


Prioritaet 3: Activity Feed — E-Mail & Aufgaben Tabs

Die Tabs "E-Mail" und "Aufgaben" auf der Company-Detail-Seite sind aktuell disabled (Platzhalter). Fuer die E-Mail-Integration wird Phase 3 (MS 365 OAuth) benoetigt.

Fuer Aufgaben (Tasks) ohne MS 365:

  • Task-Typ existiert bereits in ActivityType (TASK, FOLLOWUP)
  • Das Frontend kann Aufgaben bereits anlegen/anzeigen
  • Optional: scheduledAt + completedAt Filter-Endpoints (GET /crm/activities?type=TASK&completedAt=null)

TODO Backend (naechste Session)

  • Vertraege-API: GET/POST/PATCH/DELETE /crm/companies/:id/contractsDONE
  • Prisma-Migration: Contract-Tabelle bereits im Schema vorhanden, keine Migration noetig
  • Activity-Filter ?type=TASK — Bereits implementiert in QueryActivitiesDto + ActivitiesService.findAll()
  • Dokumentation aktualisiert

2026-03-12 | Backend: Contract CRUD — Fertiggestellt

Neue Dateien:

packages/crm-service/src/contracts/
  contracts.module.ts             — Feature Module
  contracts.service.ts            — CRUD Service (scoped by tenantId + companyId)
  contracts.controller.ts         — REST Controller (nested: /companies/:companyId/contracts)
  dto/
    create-contract.dto.ts        — CreateContractDto + ContractStatus Enum
    update-contract.dto.ts        — UpdateContractDto (PartialType)
    query-contracts.dto.ts        — QueryContractsDto (Pagination + Status-Filter + Search)

Geaenderte Dateien:

  • src/app.module.ts — ContractsModule registriert
  • Summarize.md — Contracts-Modul dokumentiert

Endpoints:

Methode Pfad Beschreibung
GET /api/v1/crm/companies/:id/contracts Vertraege auflisten (paginiert, filterbar nach status, search)
POST /api/v1/crm/companies/:id/contracts Vertrag anlegen
GET /api/v1/crm/companies/:id/contracts/:cid Vertrag-Details
PATCH /api/v1/crm/companies/:id/contracts/:cid Vertrag aktualisieren
DELETE /api/v1/crm/companies/:id/contracts/:cid Vertrag loeschen

Query-Parameter (GET Liste): ?status=ACTIVE&search=Wartung&sort=startDate&order=asc&page=1&pageSize=25

Response-Format: Standard { success, data, meta } / { success, data, pagination, meta }

Activity-Filter: Bereits implementiert — GET /api/v1/crm/activities?type=TASK&companyId=xxx funktioniert.

TODO Frontend: Hooks + API-Calls in ContractsCard.tsx implementieren.DONE (2026-03-12)


2026-03-12 | Frontend: Vertraege-Modul implementiert

Was wurde umgesetzt

Die ContractsCard.tsx ist vollstaendig implementiert. Das "Modul in Entwicklung"-Platzhalter wurde ersetzt durch eine vollwertige CRUD-Komponente.

Geaenderte / neue Dateien

Datei Aenderung
crm/companies/ContractsCard.tsx Komplette Neuentwicklung (Platzhalter → vollwertige CRUD-Card)
crm/types.ts CreateContractPayload, UpdateContractPayload, ContractsQueryParams ergaenzt
crm/api.ts contractsApi mit list, create, update, delete (nested unter /companies/:id/contracts)
crm/hooks.ts crmKeys.contracts + 4 Hooks: useContracts, useCreateContract, useUpdateContract, useDeleteContract
crm/companies/CompanyDetailPage.tsx contractCount-Prop entfernt (Card laedt nun selbst via API)

Funktionsumfang

  • Liste: Alle Vertraege einer Company mit Status-Badge (Entwurf/Aktiv/Abgelaufen/Storniert), Laufzeit, formatiertem Vertragswert
  • Erstellen: "+ Neu" Button oeffnet Modal mit Titel*, Status, Beginn/Ende, Wert/Waehrung, Notizen
  • Bearbeiten: Stift-Icon pro Zeile oeffnet vorbefuelltes Edit-Modal
  • Loeschen: X-Icon mit Bestaetigung per window.confirm
  • Waehrungen: EUR, USD, CHF, GBP auswaehlbar
  • Fehlerbehandlung: API-Fehler werden im Modal angezeigt

TypeScript-Check

npx tsc --noEmit — 0 Fehler


2026-03-12 | Plattform-Admin: Briefing — Vertragsdokumente (Datei-Upload)

Anforderung

An jedem Vertrag soll eine oder mehrere Dateien (primär PDFs) hochgeladen werden koennen — z.B. das unterschriebene Original-Dokument, Anlagen oder Nachtraege.

Neue DB-Tabelle: contract_files

CREATE TABLE app_crm.contract_files (
  id               UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id        UUID NOT NULL,
  contract_id      UUID NOT NULL REFERENCES app_crm.contracts(id) ON DELETE CASCADE,
  original_name    VARCHAR(500) NOT NULL,
  storage_path     VARCHAR(1000) NOT NULL,  -- relativer Pfad auf Disk
  mime_type        VARCHAR(200) NOT NULL,
  size             INTEGER NOT NULL,         -- Bytes
  uploaded_by      UUID NOT NULL,
  created_at       TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX ON app_crm.contract_files(contract_id);

Prisma-Modell (in crm.schema.prisma erganzen):

model ContractFile {
  id           String   @id @default(uuid())
  tenantId     String   @map("tenant_id") @db.Uuid
  contractId   String   @map("contract_id") @db.Uuid
  originalName String   @map("original_name") @db.VarChar(500)
  storagePath  String   @map("storage_path") @db.VarChar(1000)
  mimeType     String   @map("mime_type") @db.VarChar(200)
  size         Int
  uploadedBy   String   @map("uploaded_by") @db.Uuid
  createdAt    DateTime @default(now()) @map("created_at")
  contract     Contract @relation(fields: [contractId], references: [id], onDelete: Cascade)

  @@map("contract_files")
  @@schema("app_crm")
}

Rueckwaertsrelation im Contract-Modell erganzen:

model Contract {
  // ... bestehende Felder ...
  files ContractFile[]
}

Datei-Speicherung

  • Ablageort: /app/uploads/contracts/{tenantId}/{contractId}/{uuid}-{originalName}
  • Docker-Volume: ./uploads:/app/uploads (im docker-compose.crm.yml erganzen)
  • Verzeichnis wird automatisch beim ersten Upload erstellt (fs.mkdirSync(..., { recursive: true }))
  • Dateiname auf Disk: {uuid}-{originalName} (UUID-Prefix verhindert Kollisionen und path-traversal)

Erlaubte Dateitypen und Groessen

Typ MIME Max
PDF application/pdf 25 MB
Word application/msword, .../wordprocessingml.document 25 MB
Excel application/vnd.ms-excel, .../spreadsheetml.sheet 25 MB
  • Multer-Validierung: fileFilter + limits: { fileSize: 26_214_400 } (25 MB)
  • Max. Dateien pro Vertrag: 10

Neue Endpoints (4)

Methode Pfad Beschreibung
POST /api/v1/crm/companies/:companyId/contracts/:contractId/files Datei hochladen (multipart/form-data, field: file)
GET /api/v1/crm/companies/:companyId/contracts/:contractId/files Datei-Liste abrufen
GET /api/v1/crm/companies/:companyId/contracts/:contractId/files/:fileId/download Datei herunterladen / anzeigen
DELETE /api/v1/crm/companies/:companyId/contracts/:contractId/files/:fileId Datei loeschen

POST /files — Upload

  • Multer FileInterceptor('file')
  • Validierung: Dateityp + Groesse
  • Speichern auf Disk
  • ContractFile-Record in DB anlegen
  • Sicherheit: tenantId aus JWT, nicht aus Request-Body!
  • Tenant-Pruefung: Contract muss zum Tenant gehoeren

Response:

{
  "success": true,
  "data": {
    "id": "uuid",
    "contractId": "uuid",
    "originalName": "Wartungsvertrag_2026.pdf",
    "mimeType": "application/pdf",
    "size": 245678,
    "uploadedBy": "uuid",
    "createdAt": "2026-03-12T..."
  }
}

GET /files — Liste

Response:

{
  "success": true,
  "data": [
    {
      "id": "uuid",
      "contractId": "uuid",
      "originalName": "Wartungsvertrag_2026.pdf",
      "mimeType": "application/pdf",
      "size": 245678,
      "uploadedBy": "uuid",
      "createdAt": "2026-03-12T..."
    }
  ]
}

GET /files/:fileId/download — Herunterladen / Inline anzeigen

  • Query-Parameter: ?inline=true (optional) — setzt Content-Disposition: inline statt attachment
  • Default: Content-Disposition: attachment; filename="Wartungsvertrag_2026.pdf"
  • Content-Type: aus DB (mimeType)
  • Implementierung: @Res() res: Response, res.sendFile(absolutePath) oder NestJS StreamableFile
  • Sicherheit: Pruefung dass fileId zum tenantId gehoert, KEIN direktes Auslesen von storagePath aus Request

DELETE /files/:fileId — Loeschen

  • DB-Record loeschen
  • Datei auf Disk loeschen (fs.unlink)
  • Falls Datei auf Disk nicht gefunden: trotzdem DB-Record loeschen (kein Fehler)

Sicherheits-Hinweise

  1. Path Traversal: originalName NIEMALS direkt als Pfad verwenden. Immer UUID-Prefix auf Disk.
  2. Tenant-Isolation: Jeder File-Zugriff muss tenantId aus JWT gegen contract.tenantId pruefen.
  3. Upload-Limit: Max 10 Dateien pro Vertrag — pruefe Anzahl vor Upload.
  4. DSGVO: Dateien werden beim Loeschen des Vertrags automatisch entfernt (Cascade in DB + Cron-Job oder onDelete-Hook fuer Disk-Cleanup).

Modulstruktur

  • Kein eigenes Modul noetig — in ContractsModule (src/contracts/) integrieren:
    • contracts.controller.ts — neue Routen erganzen
    • contracts.service.tsuploadFile, listFiles, downloadFile, deleteFile Methoden
    • multer bereits als @nestjs/platform-express Peer-Dependency vorhanden

Migration

prisma/migrations/20260312_contract_files/migration.sql

Nach Migration: docker compose ... up -d crm --force-recreate -V

Und im docker-compose.crm.yml:

volumes:
  - ./uploads:/app/uploads

TODO fuer Frontend

Nach Implementierung der 4 Endpoints:

  • ContractFile-Interface in types.ts
  • contractFilesApi in api.ts (upload, list, download als Blob, delete)
  • useContractFiles, useUploadContractFile, useDeleteContractFile in hooks.ts
  • ContractsCard.tsx — Dateien-Sektion im Edit-Modal: Upload-Button, Dateiliste mit Download + Loeschen

Hinweis: Das Frontend kann vorab gegen den definierten API-Contract gebaut werden. Sobald die Backend-Endpoints live sind, ist die Integration sofort funktional.