import { asError } from '@/utils/errorTypes'; import React, { useState, useEffect } from 'react'; // TODO: P2-F21 — Replace hardcoded triggerEventOptions with backend config import { useTranslation } from 'react-i18next'; import { Modal } from '@/components/ui/Modal'; import { Button } from '@/components/ui/Button'; import { Input } from '@/components/ui/Input'; import { Select } from '@/components/ui/Select'; import { useToast } from '@/components/ui/Toast'; import { StepConfigPanel } from './StepConfigPanel'; import { useCreateWorkflow, useUpdateWorkflow, } from '@/api/workflows'; import type { Workflow, WorkflowStep, WorkflowStepType, WorkflowCreateInput, WorkflowUpdateInput } from '@/api/workflows'; import { ArrowUp, ArrowDown, Plus, Trash2, Code2, FormInput, LayoutTemplate } from 'lucide-react'; const triggerEventOptions = [ { value: '', label: '— Kein Trigger —' }, { value: 'contact.created', label: 'contact.created' }, { value: 'contact.updated', label: 'contact.updated' }, { value: 'deal.created', label: 'deal.created' }, { value: 'deal.stage_changed', label: 'deal.stage_changed' }, { value: 'deal.won', label: 'deal.won' }, { value: 'deal.lost', label: 'deal.lost' }, { value: 'task.completed', label: 'task.completed' }, { value: 'task.overdue', label: 'task.overdue' }, { value: 'manual', label: 'manual' }, ]; interface EditorFormState { name: string; description: string; trigger_event: string; is_active: boolean; steps: WorkflowStep[]; } const emptyStep = (): WorkflowStep => ({ name: '', type: 'action', config: {}, description: null, }); const emptyForm: EditorFormState = { name: '', description: '', trigger_event: '', is_active: true, steps: [], }; interface WorkflowTemplate { id: string; name: string; description: string; form: EditorFormState; } const templates: WorkflowTemplate[] = [ { id: 'welcome-email', name: 'Welcome Email', description: 'Sendet eine Willkommens-Mail an neue Kontakte.', form: { name: 'Welcome Email', description: 'Sendet eine Willkommens-Mail an neue Kontakte.', trigger_event: 'contact.created', is_active: true, steps: [ { name: 'Willkommens-Mail senden', type: 'mail', config: { to: '{{contact.email}}', subject: 'Willkommen bei uns!', body: 'Hallo {{contact.first_name}}, willkommen bei unserem Unternehmen!', }, description: 'Sendet die Willkommens-Mail an den neuen Kontakt.', }, ], }, }, { id: 'contact-follow-up', name: 'Contact Follow-Up', description: 'Wartet 3 Tage und sendet dann ein Follow-Up.', form: { name: 'Contact Follow-Up', description: 'Wartet 3 Tage und sendet dann ein Follow-Up.', trigger_event: 'contact.created', is_active: true, steps: [ { name: '3 Tage warten', type: 'wait', config: { duration_seconds: 259200 }, description: 'Wartet 3 Tage nach Kontakterstellung.', }, { name: 'Follow-Up senden', type: 'mail', config: { to: '{{contact.email}}', subject: 'Wie können wir helfen?', body: 'Hallo {{contact.first_name}}, wir wollten nachfragen, ob wir helfen können.', }, description: 'Sendet das Follow-Up an den Kontakt.', }, ], }, }, { id: 'approval-chain', name: 'Approval Chain', description: 'Deal-Genehmigung mit zweistufiger Freigabe.', form: { name: 'Approval Chain', description: 'Deal-Genehmigung mit zweistufiger Freigabe.', trigger_event: 'deal.stage_changed', is_active: true, steps: [ { name: 'Vertriebsleiter-Genehmigung', type: 'approval', config: { approver_role: 'sales_manager', timeout_hours: 24 }, description: 'Erste Freigabe durch den Vertriebsleiter.', }, { name: 'Geschäftsführer-Genehmigung', type: 'approval', config: { approver_role: 'ceo', timeout_hours: 48 }, description: 'Zweite Freigabe durch die Geschäftsführung.', }, { name: 'Bestätigungs-Mail', type: 'notification', config: { channel: 'email', template: 'deal_approved', recipients: '{{deal.owner_email}}' }, description: 'Benachrichtigt den Deal-Owner über die Freigabe.', }, ], }, }, ]; type EditorMode = 'form' | 'json'; export interface WorkflowEditorProps { open: boolean; workflow?: Workflow | null; onClose: () => void; } export function WorkflowEditor({ open, workflow, onClose }: WorkflowEditorProps) { const { t } = useTranslation(); const toast = useToast(); const createMutation = useCreateWorkflow(); const updateMutation = useUpdateWorkflow(); const isEdit = !!workflow; const [form, setForm] = useState(emptyForm); const [mode, setMode] = useState('form'); const [jsonText, setJsonText] = useState(''); const [jsonError, setJsonError] = useState(undefined); const [showTemplates, setShowTemplates] = useState(false); useEffect(() => { if (open) { if (workflow) { setForm({ name: workflow.name, description: workflow.description ?? '', trigger_event: workflow.trigger_event ?? '', is_active: workflow.is_active, steps: workflow.steps?.length ? workflow.steps.map((s) => ({ name: s.name, type: s.type, config: s.config ?? {}, description: s.description ?? null, })) : [], }); } else { setForm({ ...emptyForm }); } setMode('form'); setShowTemplates(false); setJsonError(undefined); } }, [open, workflow]); useEffect(() => { setJsonText(JSON.stringify(form, null, 2)); setJsonError(undefined); }, [form]); const updateField = ( key: K, value: EditorFormState[K] ) => { setForm((prev) => ({ ...prev, [key]: value })); }; const addStep = () => { setForm((prev) => ({ ...prev, steps: [...prev.steps, emptyStep()] })); }; const updateStep = (index: number, updated: WorkflowStep) => { setForm((prev) => { const steps = [...prev.steps]; steps[index] = updated; return { ...prev, steps }; }); }; const removeStep = (index: number) => { setForm((prev) => ({ ...prev, steps: prev.steps.filter((_, i) => i !== index), })); }; const moveStep = (index: number, dir: 'up' | 'down') => { setForm((prev) => { const steps = [...prev.steps]; const target = dir === 'up' ? index - 1 : index + 1; if (target < 0 || target >= steps.length) return prev; [steps[index], steps[target]] = [steps[target], steps[index]]; return { ...prev, steps }; }); }; const applyTemplate = (template: WorkflowTemplate) => { setForm({ name: template.form.name, description: template.form.description, trigger_event: template.form.trigger_event, is_active: template.form.is_active, steps: template.form.steps.map((s) => ({ ...s, config: { ...s.config } })), }); setShowTemplates(false); }; const handleJsonChange = (value: string) => { setJsonText(value); try { const parsed = JSON.parse(value) as EditorFormState; if (!Array.isArray(parsed.steps)) { setJsonError('steps muss ein Array sein'); return; } setJsonError(undefined); setForm(parsed); } catch { setJsonError('Ungültiges JSON'); } }; const requiredConfigFields: Partial> = { wait: ['duration_seconds'], http: ['url'], mail: ['to', 'subject'], calendar: ['action'], dms: ['action'], search: ['query'], agent: ['agent_id'], crm: ['action'], }; const validateSteps = (steps: WorkflowStep[]): string | null => { if (steps.length === 0) { return 'Mindestens ein Schritt ist erforderlich'; } for (let i = 0; i < steps.length; i++) { const s = steps[i]; if (!s.name.trim()) { return `Schritt ${i + 1}: Name ist erforderlich`; } const required = requiredConfigFields[s.type]; if (required) { for (const field of required) { const v = s.config[field]; if (v === undefined || v === null || v === '') { return `Schritt ${i + 1} (${s.type}): Feld "${field}" ist erforderlich`; } } } } return null; }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!form.name.trim()) { toast.error('Name ist erforderlich'); return; } const stepError = validateSteps(form.steps); if (stepError) { toast.error(stepError); return; } try { if (isEdit && workflow) { const data: WorkflowUpdateInput = { name: form.name, description: form.description || null, trigger_event: form.trigger_event || null, steps: form.steps, is_active: form.is_active, }; await updateMutation.mutateAsync({ id: workflow.id, data }); toast.success('Workflow aktualisiert'); } else { const data: WorkflowCreateInput = { name: form.name, description: form.description || null, trigger_event: form.trigger_event || null, steps: form.steps, is_active: form.is_active, }; await createMutation.mutateAsync(data); toast.success('Workflow erstellt'); } onClose(); } catch (err: unknown) { const errObj = asError(err); // Show detailed validation errors from backend (422) if (errObj.validationErrors) { const details = Object.entries(errObj.validationErrors) .map(([field, msgs]) => `${field}: ${(msgs as string[]).join(', ')}`) .join('; '); toast.error(`Validierungsfehler: ${details}`); } else if (errObj.detail) { toast.error(errObj.detail); } else { toast.error(errObj.message || 'Fehler beim Speichern'); } } }; const isSaving = createMutation.isPending || updateMutation.isPending; return (
{/* Mode toggle + template gallery */}
{!isEdit && ( )}
{/* Template gallery */} {showTemplates && !isEdit && (
{templates.map((template) => ( ))}
)} {mode === 'form' ? ( <>
updateField('name', e.target.value)} placeholder="z.B. Deal-Genehmigungsprozess" />