- Phase 2.3 Forecast: probability-Feld in PipelineStage, GET /crm/deals/forecast Endpoint - Phase 2.2 Import: ImportModule mit preview/execute/history Endpoints (CSV, XLSX, vCard) - Phase 2.4 Enrichment: EnrichmentModule mit /enrich + /settings/integrations/north-data - Contracts: ContractsModule mit CRUD + File-Upload Endpoints (Multer, max 25MB) - Migrations: 20260312_contract_files, 20260312_phase23_forecast - docker-compose.crm.yml: uploads Volume für Vertragsdokumente Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
136 KiB
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
-
Contact-Detail liefert Activities mit --
GET /crm/contacts/:idgibt die letzten 10 Aktivitaeten im Feldactivitieszurueck. Das Frontend zeigt diese in der Timeline an. -
Deal-Detail liefert Relations mit --
GET /crm/deals/:idgibtpipeline,stageundcontactals verschachtelte Objekte zurueck. -
Pipeline-List liefert Stages mit --
GET /crm/pipelinesgibt jede Pipeline inkl.stages[]Array zurueck. Das Frontend nutzt diese fuer die Stage-Selektoren im Deal-Formular. -
Deal.value ist ein String -- Decimal kommt als String vom Backend (z.B.
"24000.00"). Das Frontend parst mitparseFloat(). -
Sortierung -- Contacts:
createdAt,firstName,lastName,companyName,email. Deals:createdAt,title,value,expectedCloseDate. -
Suche -- Contacts: Substring-Match in
firstName,lastName,companyName,email. Deals: Substring-Match intitle.
Bekannte Offene Punkte
- Traefik HTTPS-Router fuer CRM: Aktuell hat der CRM-Service nur einen HTTP-Router (
webEntrypoint). Fuer HTTPS (websecure) muesste ein zweiter Router mittls=trueangelegt werden (wie beicore-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-secureRouter angelegt mitentrypoints=websecure,tls=true, Priority 100. Deployed in Commitc9e2c4a. -
Pipeline-Stages bearbeiten: Neuer Endpoint
PATCH /crm/pipelines/:id/stages/:stageIdhinzugefuegt. 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/:idmit{ 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-EndpointDealDetail 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-frontendneu 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,isActivedeals[]— Top 10 Vorgaenge mit: alle Deal-Felder +pipeline+stageObjekte_count— Zaehler fuercontactsunddeals
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
-
Neue Seiten/Routen:
/crm/companies— Unternehmensliste (wie Kontakte, mit Suche/Filter/Paginierung)/crm/companies/:id— Unternehmensdetail (2-Spalten: Info links, Kontakte+Vorgaenge rechts)
-
Sidebar: Neuer NavLink "Unternehmen" in der CRM-Sektion (zwischen Kontakte und Vorgaenge oder davor)
-
Contact-Formular:
companyIdDropdown (Unternehmen-Suche) +positionTextfeld hinzufuegen -
Contact-Liste: Company-Name als Spalte anzeigen (kommt aus
contact.company.name) -
Deal-Formular:
companyIdDropdown (Unternehmen-Suche) hinzufuegen -
Deal-Liste: Company-Name als Spalte anzeigen
-
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
companyIdwird aufnullgesetzt (SetNull) - Pipeline loeschen: Alle verknuepften Vorgaenge werden geloescht (Cascade)
- Kontakt loeschen: Vorgaenge behalten ihre Daten,
contactIdwirdnull(SetNull)
Deployment-Info
- Branch:
feature/crm-service, Commit:56a9ed9 - Prisma Migration
20260310183117_add_companiesangewendet - 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 umcompanyId,company-Relation - api.ts:
companiesApimit 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
- CompaniesPage: Tabelle mit Name, Branche, Stadt, E-Mail, Kontakte-Anzahl, Vorgaenge-Anzahl, Status, Aktionen; Suchfeld (debounced 300ms); Paginierung; Erstellen/Bearbeiten/Loeschen-Modals
- CompanyFormModal: Name*, Branche, E-Mail, Telefon, Website, Adresse (Strasse, PLZ/Stadt, Land), Notizen, Tags (kommasepariert), Aktiv-Checkbox
- 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:
- Kontakt-Verknuepfung: Lexware-Kontakte suchen und mit CRM Companies/Contacts verknuepfen oder importieren
- Beleg-Anzeige: Angebote, Auftragsbestaetigungen, Rechnungen und Gutschriften aus Lexware — anzeigbar am Unternehmen, Kontakt UND am Vorgang
- 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
-
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
-
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
- Verknuepfte Belege als Tabelle (aus
-
Tags-Integration: "ERP"-Tag in Company/Contact-Formularen hervorheben (z.B. besondere Farbe), da es den automatischen Push nach Lexware aktiviert
-
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 erreichbardown: API-Key konfiguriert aber API nicht erreichbarunconfigured: 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.sqlinprisma/migrations/20260310_add_lexware_integration/ - Neue Tabellen:
lexware_vouchers,deal_vouchers - Neue Felder:
lexware_contact_id,lexware_contact_version,lexware_synced_ataufcompaniesundcontacts
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
- Company/Contact Detail: Lexware-Card mit Status-Badge (Verknuepft/Nicht verknuepft), Such-Button, Sync/Push/Refresh-Buttons, Voucher-Tabelle mit Typ-Filter
- Deal Detail: Belege-Card mit verknuepften Vouchers, Link/Unlink-Funktion, Zugriff auf Company/Contact-Vouchers
- Lexware Search Modal: Debounced Suche (400ms), Anzeige von Name/Email/Adresse, Ein-Klick-Verknuepfung
- CRM Settings: Lexware-Toggle zum Ein-/Ausblenden aller Lexware-Sektionen
- 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 === nullzeigt 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:
- Branchen: CRUD-Tabelle mit Name, Farb-Badge + Color-Picker, Sortier-Pfeile
- Kontotypen: CRUD-Tabelle mit Name, Sortier-Pfeile
- 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 (nachprisma 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:
- Prisma-Migration
20260310_add_lexware_integration— War bereits manuell angewendet (VoucherType Enum existierte), wurde alsappliedmarkiert viaprisma migrate resolve - Prisma-Migration
20260311_add_company_detail_overhaul— Erfolgreich angewendet. Neue Tabellen:industries,account_types,relationship_types,company_relationships,contracts. Aenderungen ancompaniesundactivities. - Prisma-Client regeneriert —
prisma generateim Container ausgefuehrt - 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}
- Seed-Daten geladen fuer beide Tenants (
3fc0e74d-...und11111111-...):- 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 deployedSeed-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-crmContainer laeuft im Dev-Modus mit Volume-Mount. Code-Aenderungen werden automatisch erkannt. - Nach Schema-Aenderungen muss
prisma generateim Container ausgefuehrt werden. - Die
LexwareSyncContent-Komponente wurde als separater Export ausLexwareSyncPage.tsxextrahiert 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
pushundsyncMethoden, falls diese ebenfallstenantIdsetzen
Reproduktion
/crm/settings→ Tab "Lexoffice Sync" → "Import (Lexware → CRM)"- Beliebigen Lexware-Kontakt suchen (z.B. "team")
- Auf "Unternehmen" Button klicken
- → 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
tenantIdhaben, 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()undimportAsContact()pruefen zusaetzlichif (!tenantId)und werfenBadRequestExceptionmit 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_integrationsTabelle inplatform_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 | |
| 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:
- Unternehmensregister.de (kostenlos, oeffentlich) — HR-Nummer, Registergericht, Rechtsform, Sitz, Eintragsdatum
- North Data API (kommerziell, API-Key pro Tenant) — Adresse, Branche, Umsatz, Mitarbeiterzahl, Gesellschafter, Verflechtungen
Backend-Aufgaben:
POST /crm/companies/:id/enrichoderPOST /crm/data-enrichment/companyEndpoint- Paralleler Abruf: Unternehmensregister.de + North Data API (Timeout je 10 Sek.)
- Ergebnisse normalisieren und zusammenfuehren
data_enriched_at+data_enriched_sourcein 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_reasonEnum +lost_reason_textFreitext 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 |
| 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/emailsEndpoint (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/calendarEndpoint- 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_idwird incrm_activitiesgespeichert
Richtung To Do -> INSIGHT:
- NestJS
@CronPolling 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:
- User klickt "Nach Outlook exportieren"
- Pruefung: MS-Verbindung aktiv? -> Wenn nein: Hinweis
- Existiert Kontakt in Outlook (ms_contact_id)? -> PATCH (Update) / POST (Neu)
- 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-outlookEndpoint- 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 pushauf dem Server nach Schema-Aenderung
1.2 Contact/Deal Owner (m:n Modell)
- Neue Tabellen:
crm_contact_ownersundcrm_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/enrichEndpoint- Paralleler Abruf: Unternehmensregister.de (kostenlos) + North Data API (API-Key pro Tenant)
- Ergebnis als Vorschlag zurueckgeben (Frontend zeigt Vergleich-Modal)
- Admin-Einstellung fuer North Data API-Key
- Referenz: Abschnitt A.2 oben
2.3 Kontakt-Import
- CSV-Import mit Spalten-Mapping (Backend parst, gibt Vorschau zurueck)
- Excel-Import (.xlsx) — gleiche Logik
- vCard-Import (.vcf + ZIP)
- Duplikat-Erkennung via E-Mail
- Referenz: Abschnitt A.7 oben
2.4 Berechtigungsmodell
- Sichtbarkeitsfilter in allen List-Queries: Eigene / Team / Alle
- Konfigurierbar pro Rolle via Tenant-Admin
- Referenz: Abschnitt A.8 oben
2.5 Forecast-Endpoint
GET /crm/deals/forecast— Aggregierter Dealwert pro Stage gewichtet mit Wahrscheinlichkeit- Referenz: Abschnitt A.4 oben
Phase 3 — SPAETER (abhaengig von CORE OAuth)
BLOCKER: Diese Features benoetigen die Microsoft 365 OAuth-Integration im Core-Service. Der Core-Entwickler arbeitet parallel an: OAuth-Flow,
user_integrations-Tabelle, Azure App-Registrierung. Erst wenn das steht, kann Phase 3 beginnen.
3.1 E-Mail Tab — GET /crm/contacts/:id/emails (Proxy zu MS Graph API, Redis 5 Min Cache)
3.2 Kalender Tab — GET /crm/contacts/:id/calendar (Graph API, Redis Cache)
3.3 Aufgaben Sync — Bidirektional mit Microsoft To Do (@Cron alle 5 Min, Redis-Lock pro User)
3.4 Kontakte Export — POST /crm/contacts/:id/export-to-outlook (Graph API)
Details: Abschnitte B.1–B.4 oben
Phase 4 — NICE-TO-HAVE
4.1 Visitenkarten-Scan — Anthropic Vision API, Rate Limit 50/Tag/Tenant 4.2 CRM Reporting — 5 Dashboards + CSV-Export (Pipeline, Aktivitaeten, Kontaktwachstum, Win/Loss, Mitarbeiter-Performance)
Wichtige technische Hinweise
-
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_companiesSplit. Entscheidung noetig: Schrittweise migrieren oder Big-Bang-Umbau? -
User-Referenzen: Contact/Deal-Owner referenzieren
user_idausplatform_core. Keine User-Daten kopieren — bei Bedarf per REST-Call an Core-Service (GET /api/users/:id) oder JWT-Payload nutzen. -
Neue Rollen: Das Berechtigungsmodell (A.8) definiert
team_leadundtenant_readonly— diese Rollen existieren im Core noch NICHT. Abstimmung mit Core-Entwickler noetig. -
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.tsundhooks.tsergaenzt 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 /contactsakzeptiert jetztemails: [{email, type, isPrimary}]undphones: [{phone, type, isPrimary}]PATCH /contacts/:idmit Replace-Strategie (delete all + recreate)- Analog fuer Companies
- Lexware-Import erzeugt automatisch Multi-Value Eintraege
statusFeld (ACTIVE/INACTIVE/BLOCKED) ersetztisActiveBoolean — beide werden synchron gehalten
DTOs:
CreateEmailDto:{email, type?: EmailType, isPrimary?: boolean}CreatePhoneDto:{phone, type?: PhoneType, isPrimary?: boolean}EntityStatus: ACTIVE, INACTIVE, BLOCKEDCompanySize: SIZE_1_10, SIZE_11_50, SIZE_51_200, SIZE_201_500, SIZE_500_PLUSContactSource: 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/:idmitstatus: 'LOST'erfordertlostReason(im DTO oder bereits gesetzt) — sonst 400 BadRequest.PATCH /deals/:idmitstatus: 'WON'loescht automatischlostReasonundlostReasonText.
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-Erstellungcrm.contact.updated— nach Contact-Updatecrm.deal.created— nach Deal-Erstellungcrm.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 + Enumssrc/common/dto/owner.dto.ts— AddOwnerDto + OwnerRolesrc/owners/owners.service.ts+owners.module.ts— Shared Owner CRUDsrc/events/crm-event-publisher.service.ts— CRM Event Publishersrc/events/crm-events.module.ts— Global Events Modulesrc/events/activity-due-soon.scheduler.ts— Cron-Job
TODO fuer Frontend
types.tsum neue Felder erweitern (emails[], phones[], owners[], status, linkedinUrl, birthday, source, department, lostReason, etc.)- Contact/Company Formulare um Multi-Value Email/Phone Eingabe erweitern
- Owner-Management UI (Zuweisen/Entfernen von Owners)
- Deal-Detail: Lost-Reason Feld bei Status LOST einblenden
- 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:
namewird auslabelautomatisch generiert (slugify: "Kundennummer" -> "kundennummer"). Muss pro Tenant+EntityType unique sein.- Bei
DELETEeiner Definition: Alle zugehoerigen Values loeschen (Cascade). - Custom Fields in Entity-Detail-Responses mitliefern (neues Feld
customFieldsals Array). - Position bestimmt Reihenfolge in Formularen — bei Drag&Drop im Admin wird nur
positionper PATCH aktualisiert. - Validierung: Bei
is_required: truepruefen, ob Wert gesetzt ist (beim Entity-Speichern). - Bei DROPDOWN/MULTI_SELECT:
optionsmuss 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):
- Upload: User laedt Datei hoch -> Backend parst und gibt Spalten + erste 10 Zeilen als Vorschau zurueck
- Mapping: User ordnet Datei-Spalten den CRM-Feldern zu -> Backend fuehrt Import aus
- 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:
xlsxoderexceljs(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 ueberspringenUPDATE: 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 * probabilityprobabilitykommt aus der Stage-Definition (neues Feldprobability: DECIMAL DEFAULT 0incrm_pipeline_stagesergaenzen!)- Nur Deals mit Status
OPENzaehlen period-Filter: basiert aufexpectedCloseDate- Wenn
pipelineIdfehlt: 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:
- Unternehmensregister.de abfragen (kostenlos, Scraping/API — Firmenname als Suchbegriff)
- North Data API abfragen (API-Key aus Tenant-Settings,
GET https://www.northdata.de/api/v1/company?name=...) - Ergebnisse zusammenfuehren (North Data hat Vorrang bei Konflikten)
- Timeout pro Quelle: 10 Sekunden, bei Fehler einzelner Quelle: verfuegbare Daten zurueckgeben
dataEnrichedAt+dataEnrichedSourcein Company aktualisieren (NACH User-Bestaetigung, d.h. beim naechsten PATCH)
Admin-Einstellung noetig:
- North Data API-Key pro Tenant speichern (
crm_settingsTabelle oder eigenecrm_integrationsTabelle) - Endpoint:
GET/PUT /crm/settings/integrations/north-datamit{ 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/findOneService-Methoden: Sichtbarkeitsfilter basierend auf User-Rolle + Tenant-Settings TenantGuarderweitern oder eigenenVisibilityGuarderstellen- Default-Werte:
tenant_admin-> ALL/ALL/ALL,tenant_member-> ALL (bisheriges Verhalten beibehalten!) - Endpoint:
GET/PUT /crm/settings/visibilityfuer 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
-
Prisma-Schema: Alle neuen Tabellen in
packages/crm-service/prisma/crm.schema.prisma. Migration mitprisma db pushoder eigener SQL-Migration. -
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)
-
File Upload (Import): NestJS
@UseInterceptors(FileInterceptor('file'))mit@UploadedFile(). Maximal 10 MB. Temporaere Dateien in/tmp/mit UUID-Prefix, nach Verarbeitung loeschen. -
Testing: Bei Import besonders wichtig: Edge Cases (leere Zeilen, Sonderzeichen, fehlende Pflichtfelder, doppelte E-Mails).
-
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, DEALCustomFieldType: 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
- Zwei getrennte Komponenten:
CustomFieldsDisplay(read-only, Detail-Pages) undCustomFieldsForm(editierbar, Formulare) — saubere Trennung von Anzeige und Bearbeitung. - Ref-basierter State in Formularen:
customFieldValuesRefstatt useState, um unnoetige Re-Renders zu vermeiden. Werte werden erst beim Submit ausgelesen. - 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. - DROPDOWN Fallback:
CustomFieldValueliefert 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
isRequiredclient-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 Feldprobability?: number(0-1, @Min(0) @Max(1))UpdatePipelineStageDto: Neues optionales Feldprobability?: number- Bestehende Stage-Responses muessen
probabilityenthalten
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:
- Alle Deals mit
status = 'OPEN'laden (im Tenant) - Optional nach
pipelineIdfiltern - Period-Filter auf
expectedCloseDate:quarter: Aktuelles Quartal (z.B. Q1 2026 = Jan-Mrz)month: Aktueller Monatyear: Aktuelles Jahr
- Nach Stage gruppieren
- Pro Stage:
dealCount,totalValue(Summevalue),weightedValue(totalValue * stage.probability) - 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:
probabilityFeld inPipelineStage - Migration erstellen und anwenden
CreatePipelineStageDto+UpdatePipelineStageDtoerweitern- Pipeline-Stage-Responses:
probabilitymitliefern (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:
- Format erkennen (Extension oder MIME-Type)
- CSV: Auto-detect Delimiter (
,;\t). UTF-8 mit BOM-Erkennung. Erste Zeile = Header. - XLSX: Erstes Sheet lesen. Erste Zeile = Header.
- vCard (.vcf): Felder extrahieren (FN, N, EMAIL, TEL, ORG, ADR, URL, NOTE)
- Datei mit UUID-Prefix in
/tmp/speichern fuer spaetere Verarbeitung - 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-FeldduplicateStrategy: @IsEnum(['SKIP', 'UPDATE', 'MARK'])options.skipFirstRow: @IsBoolean() @IsOptional()
Verarbeitungs-Logik:
- Temp-Datei ueber
importIdladen - Alle Zeilen parsen
- Pro Zeile: Felder gemaess Mapping zuordnen
- Duplikat-Erkennung ueber
emailFeld (case-insensitive):SKIP: Zeile ueberspringen, Zaehler erhoehenUPDATE: Bestehenden Datensatz aktualisieren (nur nicht-leere Felder ueberschreiben)MARK: Importieren und TagIMPORT_DUPLICATEsetzen
- Validierung pro Zeile (E-Mail-Format, Pflichtfelder je nach entityType)
- Batch-Insert via Prisma (nicht einzeln!)
- DSGVO: Temp-Datei nach Verarbeitung LOESCHEN
- 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 Requestmit 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.tsregistrieren - 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:
- Company aus DB laden (Name, aktuelle Felder)
- Unternehmensregister.de abfragen (kostenlos, immer verfuegbar):
- Suche nach Firmenname
- Felder: Handelsregisternr., Registergericht, Rechtsform, Sitz
- Timeout: 10 Sekunden
- 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
- Ergebnisse zusammenfuehren (North Data hat Vorrang bei Konflikten)
- NUR Vorschlaege zurueckgeben — NICHT automatisch in DB schreiben!
- 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.tsregistrieren - 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
- Reihenfolge: Phase 2.3 zuerst (kleinstes Delta), dann 2.2, dann 2.4
- Patterns: Alle bestehenden Patterns beibehalten (TenantGuard, singleResponse/paginatedResponse, class-validator DTOs, Swagger-Dekoratoren)
- Docker-Volume: Bei Schema-Aenderungen Container mit
-Vneu erstellen:docker compose -f docker-compose.yml -f docker-compose.crm.yml up -d crm --force-recreate -V - Tests: Import-Edge-Cases besonders gruendlich testen (leere Zeilen, Sonderzeichen, fehlende Pflichtfelder, doppelte E-Mails)
- DSGVO: Import-Temp-Dateien MUESSEN nach Verarbeitung geloescht werden
- 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.00–1.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 Pipelineperiod(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 PipelineStagesrc/pipelines/dto/create-pipeline.dto.ts— probability-Feldsrc/pipelines/dto/update-stage.dto.ts— probability-Feldsrc/pipelines/pipelines.service.ts— probability in create/addStage/updateStagesrc/pipelines/pipelines.controller.ts— probability durchreichensrc/deals/dto/forecast-query.dto.ts— NEU: ForecastQueryDto + ForecastPeriod Enumsrc/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 (0–100% 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-Supportxlsx: ^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:
fileFeld, 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 ueberspringenUPDATE— Bestehenden Datensatz aktualisierenMARK— 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.tssrc/import/import.controller.tssrc/import/import.service.tssrc/import/dto/import-preview.dto.tssrc/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
- Unternehmensregister.de (kostenfrei) — Handelsregister-Daten: registerNumber, registerCourt, Adresse
- 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)
NORTH_DATA_API_KEY— Globaler API-KeyNORTH_DATA_API_URL— Base-URL (Default: https://www.northdata.de/_api)
Health-Check
/healthResponse enthaelt neuen Service:enrichment: 'up' | 'down' | 'unconfigured'- Version auf
0.3.0erhoeht
Neue Dateien
src/enrichment/enrichment.module.tssrc/enrichment/enrichment.controller.tssrc/enrichment/enrichment.service.tssrc/enrichment/dto/enrich-response.dto.tssrc/enrichment/dto/enrichment-settings.dto.ts
Geaenderte Dateien
src/app.module.ts— EnrichmentModule registriertsrc/config/env.validation.ts— NORTH_DATA_API_KEY + _URLsrc/health/health.module.ts— EnrichmentModule importiertsrc/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+completedAtFilter-Endpoints (GET /crm/activities?type=TASK&completedAt=null)
TODO Backend (naechste Session)
- Vertraege-API:
GET/POST/PATCH/DELETE /crm/companies/:id/contracts— DONE - Prisma-Migration: Contract-Tabelle bereits im Schema vorhanden, keine Migration noetig
- Activity-Filter
?type=TASK— Bereits implementiert inQueryActivitiesDto+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 registriertSummarize.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 — DONE (2026-03-12)ContractsCard.tsx implementieren.
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 |
|---|---|---|
| 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) — setztContent-Disposition: inlinestattattachment - Default:
Content-Disposition: attachment; filename="Wartungsvertrag_2026.pdf" - Content-Type: aus DB (
mimeType) - Implementierung:
@Res() res: Response,res.sendFile(absolutePath)oder NestJSStreamableFile - Sicherheit: Pruefung dass fileId zum tenantId gehoert, KEIN direktes Auslesen von
storagePathaus 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
- Path Traversal:
originalNameNIEMALS direkt als Pfad verwenden. Immer UUID-Prefix auf Disk. - Tenant-Isolation: Jeder File-Zugriff muss
tenantIdaus JWT gegencontract.tenantIdpruefen. - Upload-Limit: Max 10 Dateien pro Vertrag — pruefe Anzahl vor Upload.
- 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 erganzencontracts.service.ts—uploadFile,listFiles,downloadFile,deleteFileMethodenmulterbereits als@nestjs/platform-expressPeer-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 intypes.tscontractFilesApiinapi.ts(upload, list, download als Blob, delete)useContractFiles,useUploadContractFile,useDeleteContractFileinhooks.tsContractsCard.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.
2026-03-12 | Backend: Vertragsdokumente (Contract Files) — Fertiggestellt
Neue/geaenderte Dateien:
| Datei | Aenderung |
|---|---|
prisma/crm.schema.prisma |
ContractFile Model + files-Relation auf Contract |
prisma/migrations/20260312_contract_files/migration.sql |
contract_files Tabelle + Index |
src/contracts/contracts.service.ts |
+uploadFile, listFiles, getFile, deleteFile; remove() loescht nun auch Dateien auf Disk |
src/contracts/contracts.controller.ts |
+4 File-Endpoints (Upload, Liste, Download, Delete) |
Summarize.md |
ContractFile dokumentiert |
Endpoints:
| Methode | Pfad | Beschreibung |
|---|---|---|
| POST | /api/v1/crm/companies/:id/contracts/:cid/files | Datei hochladen (multipart, field: file) |
| GET | /api/v1/crm/companies/:id/contracts/:cid/files | Datei-Liste |
| GET | /api/v1/crm/companies/:id/contracts/:cid/files/:fid/download | Download (?inline=true fuer Inline-Anzeige) |
| DELETE | /api/v1/crm/companies/:id/contracts/:cid/files/:fid | Datei loeschen (DB + Disk) |
Sicherheit:
- Tenant-Isolation: tenantId aus JWT, nicht aus Request
- Path-Traversal-Schutz: UUID-Prefix auf Disk,
path.basename()fuer Dateinamen - MIME-Filter: nur PDF, Word, Excel erlaubt
- Max 25 MB pro Datei, max 10 Dateien pro Vertrag
- Cascade: Vertrag loeschen → Dateien auf Disk + DB werden entfernt
Speicherort: /app/uploads/contracts/{tenantId}/{contractId}/{uuid}-{originalName}
Docker-Volume: ./uploads:/app/uploads in docker-compose.crm.yml ergaenzen
TypeScript-Check: 0 Fehler