mirror of
http://172.20.10.11:3000/gitadmin/INSIGHT-MVP.git
synced 2026-06-25 03:06:39 +02:00
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>
This commit is contained in:
parent
43877bbb4a
commit
f65b9fb930
1 changed files with 602 additions and 0 deletions
602
docs/CRM_FRONTEND_INTEGRATION.md
Normal file
602
docs/CRM_FRONTEND_INTEGRATION.md
Normal file
|
|
@ -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<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).
|
||||||
Loading…
Add table
Reference in a new issue