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>
16 KiB
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
{
"success": true,
"data": { ... },
"meta": {
"timestamp": "2026-03-10T16:42:04.559Z"
}
}
Liste (paginiert)
{
"success": true,
"data": [ ... ],
"pagination": {
"page": 1,
"pageSize": 25,
"total": 42,
"totalPages": 2
},
"meta": {
"timestamp": "2026-03-10T16:42:04.559Z"
}
}
Fehler
{
"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
type ContactType = 'PERSON' | 'ORGANIZATION';
type ActivityType = 'NOTE' | 'CALL' | 'EMAIL' | 'MEETING' | 'TASK';
type DealStatus = 'OPEN' | 'WON' | 'LOST';
Contact
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
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
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
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
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:
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)
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:
// 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
valuewird als Decimal gespeichert, kommt aber als String zurueck (z.B."24000")- Beim Anzeigen
parseFloat()oderNumber()verwenden - Beim Senden als Number senden (z.B.
value: 24000)
Automatische Felder
closedAtbei Deals wird automatisch gesetzt wennstatusaufWONoderLOSTgeaendert wirdcreatedBy/updatedBywerden automatisch aus dem JWT-Token befuellttenantIdwird automatisch aus dem JWT-Token gefiltert (Multi-Tenancy)
Loeschverhalten (Cascade)
- Kontakt loeschen -> alle Aktivitaeten dieses Kontakts werden mitgeloescht
- Kontakt loeschen ->
contactIdbei Deals wird aufnullgesetzt (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).