INSIGHT-MVP/docs/CRM_FRONTEND_INTEGRATION.md
Thomas Reitz f65b9fb930 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>
2026-03-10 18:17:11 +01:00

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

  • 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).