diff --git a/docs/CRM_FRONTEND_INTEGRATION.md b/docs/CRM_FRONTEND_INTEGRATION.md new file mode 100644 index 0000000..44f59af --- /dev/null +++ b/docs/CRM_FRONTEND_INTEGRATION.md @@ -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; + +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).