docs(crm): add frontend integration guide for CRM module

Comprehensive developer handoff document covering all CRM API endpoints,
TypeScript interfaces, response formats, React Query hook patterns,
and recommended frontend structure.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Thomas Reitz 2026-03-10 18:17:11 +01:00
parent 43877bbb4a
commit f65b9fb930

View file

@ -0,0 +1,602 @@
# CRM-Modul - Frontend-Integrationsleitfaden
## Stand: 2026-03-10
---
## 1. Uebersicht
Das CRM-Backend laeuft als eigenstaendiger Container (`insight-crm`) auf Port 3100.
Traefik routet alle Anfragen unter `/api/v1/crm/*` automatisch an den CRM-Service.
**Base-URL:** `/api/v1/crm`
**Swagger-Docs:** `http://172.20.10.59/api/v1/crm/docs/`
**Authentifizierung:** Gleicher JWT-Token wie Core-Service (Bearer Token im Header)
Der bestehende Axios-Client (`src/api/client.ts`) funktioniert ohne Aenderung,
da er bereits `/api/v1` als Base-URL nutzt und den Authorization-Header automatisch setzt.
---
## 2. API-Endpunkte
### Kontakte
| Methode | Pfad | Beschreibung |
|---------|------|-------------|
| GET | `/crm/contacts` | Liste (paginiert, suchbar) |
| POST | `/crm/contacts` | Kontakt erstellen |
| GET | `/crm/contacts/:id` | Kontakt abrufen |
| PATCH | `/crm/contacts/:id` | Kontakt aktualisieren |
| DELETE | `/crm/contacts/:id` | Kontakt loeschen |
### Aktivitaeten
| Methode | Pfad | Beschreibung |
|---------|------|-------------|
| GET | `/crm/activities` | Liste (filterbar nach contactId, type) |
| POST | `/crm/activities` | Aktivitaet erstellen |
| GET | `/crm/activities/:id` | Aktivitaet abrufen |
| PATCH | `/crm/activities/:id` | Aktivitaet aktualisieren |
| DELETE | `/crm/activities/:id` | Aktivitaet loeschen |
### Pipelines
| Methode | Pfad | Beschreibung |
|---------|------|-------------|
| GET | `/crm/pipelines` | Alle Pipelines (inkl. Stages) |
| POST | `/crm/pipelines` | Pipeline erstellen (optional mit Stages) |
| GET | `/crm/pipelines/:id` | Pipeline abrufen |
| PATCH | `/crm/pipelines/:id` | Pipeline aktualisieren |
| DELETE | `/crm/pipelines/:id` | Pipeline loeschen |
| POST | `/crm/pipelines/:id/stages` | Stage hinzufuegen |
| DELETE | `/crm/pipelines/:id/stages/:stageId` | Stage entfernen |
### Deals
| Methode | Pfad | Beschreibung |
|---------|------|-------------|
| GET | `/crm/deals` | Liste (filterbar nach pipeline, stage, status, contact) |
| POST | `/crm/deals` | Deal erstellen |
| GET | `/crm/deals/:id` | Deal abrufen |
| PATCH | `/crm/deals/:id` | Deal aktualisieren |
| DELETE | `/crm/deals/:id` | Deal loeschen |
---
## 3. Response-Format
Alle Antworten folgen einem einheitlichen Format:
### Einzelner Datensatz
```json
{
"success": true,
"data": { ... },
"meta": {
"timestamp": "2026-03-10T16:42:04.559Z"
}
}
```
### Liste (paginiert)
```json
{
"success": true,
"data": [ ... ],
"pagination": {
"page": 1,
"pageSize": 25,
"total": 42,
"totalPages": 2
},
"meta": {
"timestamp": "2026-03-10T16:42:04.559Z"
}
}
```
### Fehler
```json
{
"success": false,
"error": {
"code": "BAD_REQUEST",
"message": "Validierungsfehler",
"details": ["email must be an email"]
},
"meta": {
"timestamp": "2026-03-10T16:42:04.559Z"
}
}
```
**Moegliche Error-Codes:** `BAD_REQUEST`, `UNAUTHORIZED`, `FORBIDDEN`, `NOT_FOUND`, `CONFLICT`, `INTERNAL_ERROR`
---
## 4. TypeScript-Interfaces
### Enums
```typescript
type ContactType = 'PERSON' | 'ORGANIZATION';
type ActivityType = 'NOTE' | 'CALL' | 'EMAIL' | 'MEETING' | 'TASK';
type DealStatus = 'OPEN' | 'WON' | 'LOST';
```
### Contact
```typescript
interface Contact {
id: string;
tenantId: string;
type: ContactType;
firstName: string | null;
lastName: string | null;
companyName: string | null;
email: string | null;
phone: string | null;
mobile: string | null;
website: string | null;
street: string | null;
zip: string | null;
city: string | null;
state: string | null;
country: string; // Default: 'DE'
notes: string | null;
tags: string[];
isActive: boolean;
createdBy: string;
updatedBy: string | null;
createdAt: string; // ISO 8601
updatedAt: string; // ISO 8601
}
interface CreateContactPayload {
type?: ContactType; // Default: 'PERSON'
firstName?: string; // max 100
lastName?: string; // max 100
companyName?: string; // max 200
email?: string; // valides E-Mail-Format
phone?: string; // max 50
mobile?: string; // max 50
website?: string; // valide URL
street?: string; // max 200
zip?: string; // max 20
city?: string; // max 100
state?: string; // max 100
country?: string; // max 2, Default: 'DE'
notes?: string;
tags?: string[];
isActive?: boolean; // Default: true
}
type UpdateContactPayload = Partial<CreateContactPayload>;
interface ContactQueryParams {
page?: number; // Default: 1
pageSize?: number; // Default: 25, max: 100
search?: string; // Suche in Name, E-Mail, Firma
type?: ContactType;
sort?: string; // Default: 'createdAt'
order?: 'asc' | 'desc'; // Default: 'desc'
}
```
### Activity
```typescript
interface Activity {
id: string;
tenantId: string;
contactId: string;
type: ActivityType;
subject: string;
description: string | null;
scheduledAt: string | null; // ISO 8601
completedAt: string | null; // ISO 8601
createdBy: string;
updatedBy: string | null;
createdAt: string;
updatedAt: string;
contact?: { // Nur bei Detail-Abfrage
id: string;
firstName: string | null;
lastName: string | null;
companyName: string | null;
};
}
interface CreateActivityPayload {
contactId: string; // Pflicht, UUID
type: ActivityType; // Pflicht
subject: string; // Pflicht, max 500
description?: string;
scheduledAt?: string; // ISO 8601
completedAt?: string; // ISO 8601
}
// contactId kann NICHT geaendert werden
interface UpdateActivityPayload {
type?: ActivityType;
subject?: string;
description?: string;
scheduledAt?: string;
completedAt?: string;
}
interface ActivityQueryParams {
page?: number;
pageSize?: number;
contactId?: string; // Filter nach Kontakt
type?: ActivityType;
sort?: string; // Default: 'createdAt'
order?: 'asc' | 'desc'; // Default: 'desc'
}
```
### Pipeline & Stage
```typescript
interface PipelineStage {
id: string;
pipelineId: string;
name: string;
sortOrder: number;
color: string; // Hex: '#3B82F6'
createdAt: string;
updatedAt: string;
}
interface Pipeline {
id: string;
tenantId: string;
name: string;
isDefault: boolean;
isActive: boolean;
createdBy: string;
updatedBy: string | null;
createdAt: string;
updatedAt: string;
stages: PipelineStage[];
}
interface CreatePipelinePayload {
name: string; // Pflicht, max 200
isDefault?: boolean; // Default: false
stages?: { // Optional: Stages direkt miterstellen
name: string; // Pflicht, max 200
sortOrder?: number; // Default: 0
color?: string; // Hex #RRGGBB, Default: '#6B7280'
}[];
}
interface UpdatePipelinePayload {
name?: string;
isDefault?: boolean;
isActive?: boolean;
}
// Fuer POST /crm/pipelines/:id/stages
interface AddStagePayload {
name: string; // Pflicht, max 200
sortOrder?: number;
color?: string; // Hex #RRGGBB
}
```
### Deal
```typescript
interface Deal {
id: string;
tenantId: string;
pipelineId: string;
stageId: string;
contactId: string | null;
title: string;
value: string | null; // Decimal als String (z.B. "24000")
currency: string; // Default: 'EUR'
status: DealStatus;
expectedCloseDate: string | null;
closedAt: string | null; // Wird automatisch gesetzt bei WON/LOST
notes: string | null;
createdBy: string;
updatedBy: string | null;
createdAt: string;
updatedAt: string;
pipeline?: { // Nur bei Detail/Liste
id: string;
name: string;
};
stage?: {
id: string;
name: string;
color: string;
};
contact?: {
id: string;
firstName: string | null;
lastName: string | null;
companyName: string | null;
} | null;
}
interface CreateDealPayload {
pipelineId: string; // Pflicht, UUID
stageId: string; // Pflicht, UUID
title: string; // Pflicht, max 500
contactId?: string; // Optional, UUID
value?: number; // Decimal, min 0, max 2 Nachkommastellen
currency?: string; // max 3, Default: 'EUR'
status?: DealStatus; // Default: 'OPEN'
expectedCloseDate?: string; // ISO 8601
notes?: string;
}
type UpdateDealPayload = Partial<CreateDealPayload>;
interface DealQueryParams {
page?: number;
pageSize?: number;
pipelineId?: string; // Filter nach Pipeline
stageId?: string; // Filter nach Stage
contactId?: string; // Filter nach Kontakt
status?: DealStatus; // Filter nach Status
search?: string; // Suche im Titel
sort?: string; // Default: 'createdAt'
order?: 'asc' | 'desc'; // Default: 'desc'
}
```
### API-Response Wrapper
```typescript
interface CrmPaginatedResponse<T> {
success: true;
data: T[];
pagination: {
page: number;
pageSize: number;
total: number;
totalPages: number;
};
meta: { timestamp: string };
}
interface CrmSingleResponse<T> {
success: true;
data: T;
meta: { timestamp: string };
}
interface CrmErrorResponse {
success: false;
error: {
code: string;
message: string;
details?: string[];
};
meta: { timestamp: string };
}
```
---
## 5. API-Client Beispiele
Der vorhandene Axios-Client (`src/api/client.ts`) kann direkt verwendet werden:
```typescript
import api from '../api/client';
// --- Contacts ---
// Liste abrufen (mit Suche und Pagination)
const { data } = await api.get<CrmPaginatedResponse<Contact>>('/crm/contacts', {
params: { search: 'Mustermann', page: 1, pageSize: 25 }
});
// Kontakt erstellen
const { data } = await api.post<CrmSingleResponse<Contact>>('/crm/contacts', {
type: 'PERSON',
firstName: 'Max',
lastName: 'Mustermann',
email: 'max@example.com',
companyName: 'Xinion GmbH',
});
// Kontakt aktualisieren
const { data } = await api.patch<CrmSingleResponse<Contact>>(`/crm/contacts/${id}`, {
city: 'Berlin',
tags: ['VIP', 'Partner'],
});
// Kontakt loeschen
await api.delete(`/crm/contacts/${id}`);
// --- Deals ---
// Deal-Liste mit Pipeline-Filter
const { data } = await api.get<CrmPaginatedResponse<Deal>>('/crm/deals', {
params: { pipelineId: '...', status: 'OPEN' }
});
// Deal erstellen
const { data } = await api.post<CrmSingleResponse<Deal>>('/crm/deals', {
title: 'Enterprise Lizenz',
pipelineId: '...',
stageId: '...',
contactId: '...',
value: 50000,
currency: 'EUR',
expectedCloseDate: '2026-06-30T00:00:00.000Z',
});
// Deal-Stage verschieben (z.B. Drag & Drop im Kanban)
await api.patch(`/crm/deals/${id}`, { stageId: newStageId });
// Deal als gewonnen markieren (closedAt wird automatisch gesetzt)
await api.patch(`/crm/deals/${id}`, { status: 'WON' });
```
---
## 6. React-Query Hooks (Vorschlag)
```typescript
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import api from '../api/client';
// Query-Keys
const crmKeys = {
contacts: ['crm', 'contacts'] as const,
contact: (id: string) => ['crm', 'contacts', id] as const,
activities: ['crm', 'activities'] as const,
pipelines: ['crm', 'pipelines'] as const,
deals: ['crm', 'deals'] as const,
deal: (id: string) => ['crm', 'deals', id] as const,
};
// Kontakte abrufen
function useContacts(params?: ContactQueryParams) {
return useQuery({
queryKey: [...crmKeys.contacts, params],
queryFn: () => api.get('/crm/contacts', { params }).then(r => r.data),
});
}
// Kontakt erstellen
function useCreateContact() {
const qc = useQueryClient();
return useMutation({
mutationFn: (payload: CreateContactPayload) =>
api.post('/crm/contacts', payload).then(r => r.data),
onSuccess: () => qc.invalidateQueries({ queryKey: crmKeys.contacts }),
});
}
// Pipelines (selten geaendert, laenger cachen)
function usePipelines() {
return useQuery({
queryKey: crmKeys.pipelines,
queryFn: () => api.get('/crm/pipelines').then(r => r.data),
staleTime: 10 * 60 * 1000, // 10 Minuten
});
}
// Deals mit Filtern
function useDeals(params?: DealQueryParams) {
return useQuery({
queryKey: [...crmKeys.deals, params],
queryFn: () => api.get('/crm/deals', { params }).then(r => r.data),
});
}
// Deal-Stage verschieben (Kanban Drag & Drop)
function useMoveDeal() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ id, stageId }: { id: string; stageId: string }) =>
api.patch(`/crm/deals/${id}`, { stageId }).then(r => r.data),
onSuccess: () => qc.invalidateQueries({ queryKey: crmKeys.deals }),
});
}
```
---
## 7. Frontend-Struktur (Vorschlag)
```
src/
crm/ # CRM-Modul
types.ts # Alle CRM TypeScript-Interfaces
api.ts # API-Aufrufe (axios calls)
hooks.ts # React-Query Hooks
CrmLayout.tsx # Layout mit Sidebar/Tabs
CrmLayout.module.css
contacts/
ContactsPage.tsx # Kontaktliste mit Suche + Filter
ContactDetailPage.tsx # Kontaktdetail mit Aktivitaeten
ContactForm.tsx # Erstellen/Bearbeiten-Formular
deals/
DealsPage.tsx # Kanban-Board oder Listenansicht
DealDetailPage.tsx # Deal-Detail
DealForm.tsx # Erstellen/Bearbeiten-Formular
KanbanBoard.tsx # Drag & Drop Pipeline-Ansicht
KanbanBoard.module.css
pipelines/
PipelineSettings.tsx # Pipeline-Konfiguration (Admin)
activities/
ActivityTimeline.tsx # Aktivitaeten-Timeline (in Kontaktdetail)
ActivityForm.tsx # Neue Aktivitaet erstellen
```
---
## 8. Routing-Integration
In `src/shell/App.tsx` neue Routen hinzufuegen:
```tsx
// Innerhalb des geschuetzten PrivateRoute-Bereichs
<Route path="/crm" element={<CrmLayout />}>
<Route index element={<Navigate to="contacts" replace />} />
<Route path="contacts" element={<ContactsPage />} />
<Route path="contacts/:id" element={<ContactDetailPage />} />
<Route path="deals" element={<DealsPage />} />
<Route path="deals/:id" element={<DealDetailPage />} />
<Route path="pipelines" element={<PipelineSettings />} />
</Route>
```
Sidebar-Navigation: Neuen Menuepunkt "CRM" mit Untereintraegen (Kontakte, Deals, Pipelines).
---
## 9. Wichtige Hinweise
### Validierung
- Das Backend validiert streng: Unbekannte Felder werden abgelehnt (`forbidNonWhitelisted`)
- Nur die dokumentierten Felder im Request-Body senden
- UUIDs muessen valides UUID-v4-Format haben
### Deal-Werte
- `value` wird als Decimal gespeichert, kommt aber als **String** zurueck (z.B. `"24000"`)
- Beim Anzeigen `parseFloat()` oder `Number()` verwenden
- Beim Senden als Number senden (z.B. `value: 24000`)
### Automatische Felder
- `closedAt` bei Deals wird automatisch gesetzt wenn `status` auf `WON` oder `LOST` geaendert wird
- `createdBy` / `updatedBy` werden automatisch aus dem JWT-Token befuellt
- `tenantId` wird automatisch aus dem JWT-Token gefiltert (Multi-Tenancy)
### Loeschverhalten (Cascade)
- Kontakt loeschen -> alle Aktivitaeten dieses Kontakts werden mitgeloescht
- Kontakt loeschen -> `contactId` bei Deals wird auf `null` gesetzt (Deal bleibt)
- Pipeline loeschen -> alle Stages und Deals dieser Pipeline werden mitgeloescht
### Suche
- Kontakte: Sucht in `firstName`, `lastName`, `companyName`, `email`
- Deals: Sucht in `title`
- Keine Volltextsuche, nur LIKE-basiert (Teilstring-Match)
---
## 10. Testdaten & Swagger
**Swagger UI:** http://172.20.10.59/api/v1/crm/docs/
Dort koennen alle Endpunkte interaktiv getestet werden.
Oben rechts "Authorize" klicken und einen gueltigen JWT-Token eingeben.
Ein Token kann ueber die normale Plattform-Anmeldung generiert werden
(Login -> Token aus Browser-DevTools/Network-Tab kopieren).