From c739dce161f8d7939cd11f002c9051e3ead06fe3 Mon Sep 17 00:00:00 2001 From: Thomas Reitz Date: Tue, 10 Mar 2026 19:13:02 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20CRM=20Frontend-Modul=20mit=20Kontakte,?= =?UTF-8?q?=20Deals,=20Pipelines=20und=20Aktivit=C3=A4ten?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Komplette CRM-Frontend-Integration in die bestehende React-GUI: - Types, API-Client und React Query Hooks für alle CRM-Entitäten - Kontakte: Liste mit Suche/Filter, Detail mit Aktivitäten-Timeline, Create/Edit Modal - Deals: Liste mit Pipeline/Stage/Status-Filter, Detail mit Fortschrittsbalken, Create/Edit Modal - Pipelines: Verwaltungsseite mit klappbaren Cards und Stage-Management - Aktivitäten: Formular-Modal für Notizen, Anrufe, E-Mails, Meetings, Aufgaben - CRM-Navigation in Sidebar (aufklappbar, mit Inline-SVG-Icons) - Routen in App.tsx für alle CRM-Seiten - Vite-Proxy für lokale CRM-API-Entwicklung Co-Authored-By: Claude Opus 4.6 --- .../src/crm/activities/ActivityFormModal.tsx | 291 +++++++++ packages/frontend/src/crm/api.ts | 159 +++++ .../crm/contacts/ContactDetailPage.module.css | 170 ++++++ .../src/crm/contacts/ContactDetailPage.tsx | 575 ++++++++++++++++++ .../src/crm/contacts/ContactFormModal.tsx | 377 ++++++++++++ .../src/crm/contacts/ContactsPage.module.css | 81 +++ .../src/crm/contacts/ContactsPage.tsx | 418 +++++++++++++ .../src/crm/deals/DealDetailPage.module.css | 108 ++++ .../frontend/src/crm/deals/DealDetailPage.tsx | 307 ++++++++++ .../frontend/src/crm/deals/DealFormModal.tsx | 517 ++++++++++++++++ .../src/crm/deals/DealsPage.module.css | 81 +++ packages/frontend/src/crm/deals/DealsPage.tsx | 472 ++++++++++++++ packages/frontend/src/crm/hooks.ts | 286 +++++++++ .../crm/pipelines/PipelinesPage.module.css | 123 ++++ .../src/crm/pipelines/PipelinesPage.tsx | 484 +++++++++++++++ packages/frontend/src/crm/types.ts | 232 +++++++ packages/frontend/src/shell/App.tsx | 11 + packages/frontend/src/shell/AppLayout.tsx | 96 +++ packages/frontend/vite.config.ts | 4 + 19 files changed, 4792 insertions(+) create mode 100644 packages/frontend/src/crm/activities/ActivityFormModal.tsx create mode 100644 packages/frontend/src/crm/api.ts create mode 100644 packages/frontend/src/crm/contacts/ContactDetailPage.module.css create mode 100644 packages/frontend/src/crm/contacts/ContactDetailPage.tsx create mode 100644 packages/frontend/src/crm/contacts/ContactFormModal.tsx create mode 100644 packages/frontend/src/crm/contacts/ContactsPage.module.css create mode 100644 packages/frontend/src/crm/contacts/ContactsPage.tsx create mode 100644 packages/frontend/src/crm/deals/DealDetailPage.module.css create mode 100644 packages/frontend/src/crm/deals/DealDetailPage.tsx create mode 100644 packages/frontend/src/crm/deals/DealFormModal.tsx create mode 100644 packages/frontend/src/crm/deals/DealsPage.module.css create mode 100644 packages/frontend/src/crm/deals/DealsPage.tsx create mode 100644 packages/frontend/src/crm/hooks.ts create mode 100644 packages/frontend/src/crm/pipelines/PipelinesPage.module.css create mode 100644 packages/frontend/src/crm/pipelines/PipelinesPage.tsx create mode 100644 packages/frontend/src/crm/types.ts diff --git a/packages/frontend/src/crm/activities/ActivityFormModal.tsx b/packages/frontend/src/crm/activities/ActivityFormModal.tsx new file mode 100644 index 0000000..fd7863a --- /dev/null +++ b/packages/frontend/src/crm/activities/ActivityFormModal.tsx @@ -0,0 +1,291 @@ +import { useState, useEffect } from 'react'; +import { Modal } from '../../components/Modal'; +import { useCreateActivity, useUpdateActivity } from '../hooks'; +import type { Activity, ActivityType } from '../types'; + +interface ActivityFormModalProps { + isOpen: boolean; + onClose: () => void; + contactId: string; + activity?: Activity | null; + onSuccess: () => void; +} + +const ACTIVITY_TYPE_LABELS: Record = { + NOTE: 'Notiz', + CALL: 'Anruf', + EMAIL: 'E-Mail', + MEETING: 'Meeting', + TASK: 'Aufgabe', +}; + +const ACTIVITY_TYPES: ActivityType[] = [ + 'NOTE', + 'CALL', + 'EMAIL', + 'MEETING', + 'TASK', +]; + +const labelStyle: React.CSSProperties = { + fontSize: '0.875rem', + fontWeight: 500, + color: 'var(--color-text)', + marginBottom: '0.25rem', + display: 'block', +}; + +const inputStyle: React.CSSProperties = { + width: '100%', + padding: '0.625rem 0.75rem', + border: '1px solid var(--color-border)', + borderRadius: 'var(--radius-sm)', + fontSize: '0.9375rem', + outline: 'none', + boxSizing: 'border-box', + background: 'var(--color-bg-card)', + color: 'var(--color-text)', +}; + +export function ActivityFormModal({ + isOpen, + onClose, + contactId, + activity, + onSuccess, +}: ActivityFormModalProps) { + const isEditMode = !!activity; + const createMutation = useCreateActivity(); + const updateMutation = useUpdateActivity(); + const mutation = isEditMode ? updateMutation : createMutation; + + const [error, setError] = useState(''); + const [type, setType] = useState('NOTE'); + const [subject, setSubject] = useState(''); + const [description, setDescription] = useState(''); + const [scheduledAt, setScheduledAt] = useState(''); + const [completedAt, setCompletedAt] = useState(''); + + useEffect(() => { + if (isOpen) { + setError(''); + if (activity) { + setType(activity.type); + setSubject(activity.subject); + setDescription(activity.description ?? ''); + setScheduledAt( + activity.scheduledAt + ? activity.scheduledAt.slice(0, 16) + : '', + ); + setCompletedAt( + activity.completedAt + ? activity.completedAt.slice(0, 16) + : '', + ); + } else { + setType('NOTE'); + setSubject(''); + setDescription(''); + setScheduledAt(''); + setCompletedAt(''); + } + } + }, [isOpen, activity]); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + setError(''); + + if (!subject.trim()) { + setError('Betreff ist ein Pflichtfeld'); + return; + } + + if (isEditMode && activity) { + updateMutation.mutate( + { + id: activity.id, + data: { + type, + subject: subject.trim(), + ...(description ? { description } : {}), + ...(scheduledAt ? { scheduledAt: new Date(scheduledAt).toISOString() } : {}), + ...(completedAt ? { completedAt: new Date(completedAt).toISOString() } : {}), + }, + }, + { + onSuccess: () => onSuccess(), + onError: (err: unknown) => { + const msg = + (err as { response?: { data?: { error?: { message?: string } } } }) + ?.response?.data?.error?.message ?? 'Fehler beim Speichern'; + setError(msg); + }, + }, + ); + } else { + createMutation.mutate( + { + contactId, + type, + subject: subject.trim(), + ...(description ? { description } : {}), + ...(scheduledAt ? { scheduledAt: new Date(scheduledAt).toISOString() } : {}), + ...(completedAt ? { completedAt: new Date(completedAt).toISOString() } : {}), + }, + { + onSuccess: () => onSuccess(), + onError: (err: unknown) => { + const msg = + (err as { response?: { data?: { error?: { message?: string } } } }) + ?.response?.data?.error?.message ?? 'Fehler beim Anlegen'; + setError(msg); + }, + }, + ); + } + }; + + return ( + +
+ {error && ( +
+ {error} +
+ )} + + {/* Typ */} +
+ + +
+ + {/* Betreff */} +
+ + setSubject(e.target.value)} + placeholder="Betreff der Aktivität" + required + /> +
+ + {/* Beschreibung */} +
+ +