# 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; 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; 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 { success: true; data: T[]; pagination: { page: number; pageSize: number; total: number; totalPages: number; }; meta: { timestamp: string }; } interface CrmSingleResponse { 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>('/crm/contacts', { params: { search: 'Mustermann', page: 1, pageSize: 25 } }); // Kontakt erstellen const { data } = await api.post>('/crm/contacts', { type: 'PERSON', firstName: 'Max', lastName: 'Mustermann', email: 'max@example.com', companyName: 'Xinion GmbH', }); // Kontakt aktualisieren const { data } = await api.patch>(`/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>('/crm/deals', { params: { pipelineId: '...', status: 'OPEN' } }); // Deal erstellen const { data } = await api.post>('/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 }> } /> } /> } /> } /> } /> } /> ``` 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).