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