/** * AutomationDefinitionsView * * View for viewing and managing workflow automation definitions. * Includes template selection, execution modal with live logs, and execution history. */ import React, { useState, useMemo, useEffect, useCallback, useRef } from 'react'; import { useAutomations, useAutomationOperations, AutomationTemplate, Automation } from '../../../hooks/useAutomations'; import { FormGeneratorTable } from '../../../components/FormGenerator/FormGeneratorTable'; import { AutomationEditor } from '../../../components/AutomationEditor'; import { FaSync, FaRobot, FaRocket, FaPlus, FaFileAlt, FaStop, FaList, FaTimes, FaCheck, FaExclamationCircle, FaSpinner } from 'react-icons/fa'; import { useToast } from '../../../contexts/ToastContext'; import { useApiRequest } from '../../../hooks/useApi'; import { useFeatureStore } from '../../../stores/featureStore'; import { useCurrentInstance } from '../../../hooks/useCurrentInstance'; import styles from '../../admin/Admin.module.css'; interface WorkflowLog { id: string; timestamp: number; message: string; status?: string; progress?: number; } export const AutomationDefinitionsView: React.FC = () => { const { instanceId: routeInstanceId, featureCode: routeFeatureCode } = useCurrentInstance(); const { getAllInstances } = useFeatureStore(); const instances = getAllInstances(); const chatbotInstance = instances.find(i => i.featureCode === 'chatbot') || instances[0]; const automationInstance = instances.find(i => i.featureCode === 'automation'); // When under automation feature route, use route context; otherwise use featureStore const mandateId = routeFeatureCode === 'automation' && routeInstanceId ? (automationInstance?.mandateId ?? chatbotInstance?.mandateId) : chatbotInstance?.mandateId; const featureInstanceId = routeFeatureCode === 'automation' && routeInstanceId ? routeInstanceId : (chatbotInstance?.id ?? automationInstance?.id); const automationWorkflowInstanceId = routeFeatureCode === 'automation' ? routeInstanceId : undefined; const { data: automations, attributes, permissions, pagination, loading, error, refetch, fetchAutomationById, updateOptimistically, } = useAutomations(); const { handleAutomationCreate, handleAutomationUpdate, handleAutomationDelete, handleAutomationExecute, handleInlineUpdate, fetchTemplates, deletingAutomations, executingAutomations, } = useAutomationOperations(); const { showSuccess, showError, showInfo } = useToast(); const { request } = useApiRequest(); const [showEditor, setShowEditor] = useState(false); const [editingAutomation, setEditingAutomation] = useState(null); const [editorSaving, setEditorSaving] = useState(false); const [showTemplateModal, setShowTemplateModal] = useState(false); const [templates, setTemplates] = useState([]); const [loadingTemplates, setLoadingTemplates] = useState(false); const [executionModal, setExecutionModal] = useState<{ visible: boolean; automationId: string | null; automationLabel: string; featureInstanceId: string | null; workflowId: string | null; status: 'starting' | 'running' | 'completed' | 'stopped' | 'error'; logs: WorkflowLog[]; }>({ visible: false, automationId: null, automationLabel: '', featureInstanceId: null, workflowId: null, status: 'starting', logs: [], }); const [logsModal, setLogsModal] = useState<{ visible: boolean; automation: Automation | null; }>({ visible: false, automation: null, }); const pollIntervalRef = useRef(null); const lastLogIdRef = useRef(null); const logContainerRef = useRef(null); useEffect(() => { refetch(); }, []); useEffect(() => () => { if (pollIntervalRef.current) clearInterval(pollIntervalRef.current); }, []); useEffect(() => { if (logContainerRef.current) logContainerRef.current.scrollTop = logContainerRef.current.scrollHeight; }, [executionModal.logs]); const columns = useMemo(() => { const hiddenColumns = [ 'id', 'mandateId', 'featureInstanceId', 'sysCreatedBy', 'sysCreatedAt', 'sysModifiedAt', 'template', 'executionLogs', 'placeholders', 'sysCreatedByUserName', 'mandateName', 'featureInstanceName', ]; const attrColumns = (attributes || []) .filter(attr => !hiddenColumns.includes(attr.name)) .map(attr => ({ key: attr.name, label: attr.label || attr.name, type: attr.type as any, sortable: attr.sortable !== false, filterable: attr.filterable !== false, searchable: attr.searchable !== false, width: attr.width || 150, minWidth: attr.minWidth || 100, maxWidth: attr.maxWidth || 400, })); const enrichedColumns = [ { key: 'mandateName', label: 'Mandant', type: 'text' as any, sortable: true, filterable: true, searchable: true, width: 150, minWidth: 100, maxWidth: 250 }, { key: 'featureInstanceName', label: 'Feature-Instanz', type: 'text' as any, sortable: true, filterable: true, searchable: true, width: 160, minWidth: 100, maxWidth: 250 }, { key: 'sysCreatedByUserName', label: 'Erstellt von', type: 'text' as any, sortable: true, filterable: true, searchable: true, width: 150, minWidth: 100, maxWidth: 250 }, ]; return [...attrColumns, ...enrichedColumns]; }, [attributes]); const canCreate = permissions?.create !== 'n'; const canUpdate = permissions?.update !== 'n'; const canDelete = permissions?.delete !== 'n'; const handleEditClick = async (automation: Automation) => { const fullAutomation = await fetchAutomationById(automation.id); setEditingAutomation(fullAutomation as Automation || automation); setShowEditor(true); }; const handleCreateClick = () => { setEditingAutomation({ mandateId: mandateId!, featureInstanceId: featureInstanceId!, label: '', schedule: '0 22 * * *', active: false, placeholders: {}, } as Automation); setShowEditor(true); }; const handleEditorSave = async (data: Partial) => { if (!mandateId || !featureInstanceId) { showError('Fehler: Kein aktiver Mandant oder Feature-Instanz gefunden'); return; } setEditorSaving(true); try { const saveData = { ...data, mandateId, featureInstanceId }; if (editingAutomation?.id) { saveData.id = editingAutomation.id; const success = await handleAutomationUpdate(editingAutomation.id, saveData as any); if (success) { showSuccess('Automatisierung aktualisiert'); setShowEditor(false); setEditingAutomation(null); await refetch(); } } else { const result = await handleAutomationCreate(saveData as any); if (result) { showSuccess('Automatisierung erstellt'); setShowEditor(false); setEditingAutomation(null); await refetch(); } } } catch (err: any) { showError(`Fehler: ${err.message}`); } finally { setEditorSaving(false); } }; const handleEditorCancel = () => { setShowEditor(false); setEditingAutomation(null); }; const handleDelete = async (automation: Automation) => { const success = await handleAutomationDelete(automation.id); if (success) { showSuccess('Automatisierung gelöscht'); await refetch(); } }; const handleDuplicate = async (automation: Automation) => { try { await request({ url: `/api/automations/${automation.id}/duplicate`, method: 'post' }); showSuccess('Automatisierung dupliziert'); await refetch(); } catch (err: any) { showError(`Fehler beim Duplizieren: ${err.message}`); } }; const handleLoadTemplates = async () => { setLoadingTemplates(true); try { const loadedTemplates = await fetchTemplates(); setTemplates(loadedTemplates); if (loadedTemplates.length === 0) { showInfo('Keine Vorlagen verfügbar'); } else { setShowTemplateModal(true); } } catch (err) { showError('Fehler beim Laden der Vorlagen'); } finally { setLoadingTemplates(false); } }; const handleTemplateSelect = (template: AutomationTemplate) => { setShowTemplateModal(false); if (!mandateId || !featureInstanceId) { showError('Fehler: Kein aktiver Mandant oder Feature-Instanz gefunden'); return; } let templateLabel = 'Neue Automatisierung'; if (template.label) { templateLabel = typeof template.label === 'string' ? template.label : (template.label as any).de || (template.label as any).en || 'Neue Automatisierung'; } else if (template.overview) { templateLabel = typeof template.overview === 'string' ? template.overview : ((template.overview as any).de || (template.overview as any).en || 'Neue Automatisierung'); } const convertedPlaceholders: Record = {}; const templateParams = (template as any).parameters || {}; for (const [key, value] of Object.entries(templateParams)) { if (value === null || value === undefined) { convertedPlaceholders[key] = ''; } else if (Array.isArray(value) || (typeof value === 'object' && value !== null)) { convertedPlaceholders[key] = JSON.stringify(value); } else { convertedPlaceholders[key] = String(value); } } const prefillData: Partial = { mandateId, featureInstanceId, label: templateLabel, template: typeof template.template === 'string' ? template.template : JSON.stringify(template.template, null, 2), placeholders: convertedPlaceholders, active: false, schedule: '0 22 * * *', }; setEditingAutomation(prefillData as Automation); setShowEditor(true); }; const pollWorkflowLogs = useCallback(async (workflowId: string, instanceId: string) => { try { const contextHeaders: Record = {}; if (mandateId) contextHeaders['X-Mandate-Id'] = mandateId; const logsUrl = `/api/automations/${instanceId}/workflows/${workflowId}/logs`; const workflowUrl = `/api/automations/${instanceId}/workflows/${workflowId}`; const response = await request({ url: logsUrl, method: 'get', params: lastLogIdRef.current ? { logId: lastLogIdRef.current } : {}, additionalConfig: { headers: contextHeaders }, }); const logs: WorkflowLog[] = response?.items || response || []; if (logs.length > 0) { setExecutionModal(prev => { const existingIds = new Set(prev.logs.map(l => l.id)); const newLogs = logs.filter(l => !existingIds.has(l.id)); return { ...prev, logs: [...prev.logs, ...newLogs] }; }); lastLogIdRef.current = logs[logs.length - 1].id; } const statusResponse = await request({ url: workflowUrl, method: 'get', additionalConfig: { headers: contextHeaders }, }); const workflowStatus = statusResponse?.status; if (workflowStatus === 'completed' || workflowStatus === 'stopped' || workflowStatus === 'error' || workflowStatus === 'failed') { if (pollIntervalRef.current) { clearInterval(pollIntervalRef.current); pollIntervalRef.current = null; } setExecutionModal(prev => ({ ...prev, status: workflowStatus === 'completed' ? 'completed' : workflowStatus === 'error' || workflowStatus === 'failed' ? 'error' : 'stopped', })); if (workflowStatus === 'completed') showSuccess('Automatisierung erfolgreich abgeschlossen'); else if (workflowStatus === 'error' || workflowStatus === 'failed') showError('Automatisierung fehlgeschlagen'); else showInfo('Automatisierung gestoppt'); refetch(); } } catch (err) { console.error('Error polling workflow logs:', err); } }, [request, refetch, showSuccess, showError, showInfo, mandateId]); const handleExecute = async (automation: Automation) => { lastLogIdRef.current = null; setExecutionModal({ visible: true, automationId: automation.id, automationLabel: automation.label, featureInstanceId: automation.featureInstanceId ?? automationWorkflowInstanceId ?? null, workflowId: null, status: 'starting', logs: [{ id: 'init', timestamp: Date.now() / 1000, message: 'Automatisierung wird gestartet...' }], }); try { const result = await handleAutomationExecute(automation.id); const workflowId = result?.id; const instanceId = automation.featureInstanceId ?? automationWorkflowInstanceId; if (workflowId && instanceId) { setExecutionModal(prev => ({ ...prev, workflowId, status: 'running', logs: [...prev.logs, { id: 'started', timestamp: Date.now() / 1000, message: `Workflow ${workflowId} gestartet`, status: 'running' }], })); pollIntervalRef.current = setInterval(() => pollWorkflowLogs(workflowId, instanceId), 2000); } else if (workflowId && !instanceId) { setExecutionModal(prev => ({ ...prev, status: 'error', logs: [...prev.logs, { id: 'error', timestamp: Date.now() / 1000, message: 'Keine Feature-Instanz für Polling', status: 'error' }] })); } } catch (err: any) { setExecutionModal(prev => ({ ...prev, status: 'error', logs: [...prev.logs, { id: 'error', timestamp: Date.now() / 1000, message: `Fehler: ${err.message || 'Unbekannter Fehler'}`, status: 'error' }], })); showError(`Fehler beim Ausführen: ${err.message}`); } }; const handleStopWorkflow = async () => { if (!executionModal.workflowId) return; const instanceId = executionModal.featureInstanceId ?? automationWorkflowInstanceId; if (!instanceId) { showError('Keine Feature-Instanz für Stopp verfügbar'); return; } try { const stopHeaders: Record = {}; if (mandateId) stopHeaders['X-Mandate-Id'] = mandateId; const stopUrl = `/api/automations/${instanceId}/workflows/${executionModal.workflowId}/stop`; await request({ url: stopUrl, method: 'post', additionalConfig: { headers: stopHeaders }, }); setExecutionModal(prev => ({ ...prev, logs: [...prev.logs, { id: 'stopping', timestamp: Date.now() / 1000, message: 'Workflow wird gestoppt...' }], })); } catch (err: any) { showError(`Fehler beim Stoppen: ${err.message}`); } }; const closeExecutionModal = () => { if (pollIntervalRef.current) { clearInterval(pollIntervalRef.current); pollIntervalRef.current = null; } setExecutionModal({ visible: false, automationId: null, automationLabel: '', featureInstanceId: null, workflowId: null, status: 'starting', logs: [], }); }; const handleShowLogs = async (automation: Automation) => { const fullAutomation = await fetchAutomationById(automation.id); setLogsModal({ visible: true, automation: fullAutomation as Automation || automation }); }; const formatTimestamp = (timestamp: number) => { if (!timestamp) return ''; return new Date(timestamp * 1000).toLocaleString('de-DE'); }; const formatTime = (timestamp: number) => { if (!timestamp) return ''; return new Date(timestamp * 1000).toLocaleTimeString('de-DE'); }; const getStatusIcon = (status: string) => { switch (status) { case 'completed': return ; case 'error': case 'failed': return ; case 'running': case 'starting': return ; case 'stopped': return ; default: return null; } }; if (error) { return (
⚠️

Fehler beim Laden der Automatisierungen: {error}

); } return (

Automatisierungen

Geplante und automatisierte Workflows

{canCreate && ( <> )}
{loading && (!automations || automations.length === 0) ? (
Lade Automatisierungen...
) : !automations || automations.length === 0 ? (

Keine Automatisierungen vorhanden

Erstellen Sie eine neue Automatisierung, um Workflows zeitgesteuert auszuführen.

{canCreate && (
)}
) : ( deletingAutomations.has(row.id) }] : []), ]} customActions={[ { id: 'execute', icon: , onClick: handleExecute, title: 'Ausführen', loading: (row: any) => executingAutomations.has(row.id) }, { id: 'logs', icon: , onClick: handleShowLogs, title: 'Ausführungsverlauf' }, ]} onDelete={handleDelete} hookData={{ refetch, permissions, pagination, handleDelete: handleAutomationDelete, handleInlineUpdate, updateOptimistically }} emptyMessage="Keine Automatisierungen gefunden" /> )}
{showEditor && editingAutomation && ( )} {showTemplateModal && (
setShowTemplateModal(false)}>
e.stopPropagation()}>

Vorlage auswählen

{templates.map((template, index) => { const labelText = template.label ? (typeof template.label === 'string' ? template.label : (template.label as any).de || (template.label as any).en || `Vorlage ${index + 1}`) : `Vorlage ${index + 1}`; const overviewText = template.overview ? (typeof template.overview === 'string' ? template.overview : (template.overview as any).de || (template.overview as any).en || '') : ''; let parsedTemplate: any = null; try { if (template.template) parsedTemplate = typeof template.template === 'string' ? JSON.parse(template.template) : template.template; } catch { /* ignore */ } const description = overviewText || parsedTemplate?.overview || parsedTemplate?.tasks?.[0]?.objective || 'Keine Beschreibung'; return (

{labelText}

{description}

); })}
)} {executionModal.visible && (
e.stopPropagation()} style={{ maxWidth: '700px' }}>

{getStatusIcon(executionModal.status)} Ausführung: {executionModal.automationLabel}

{executionModal.status === 'starting' && 'Wird gestartet...'} {executionModal.status === 'running' && 'Läuft...'} {executionModal.status === 'completed' && 'Abgeschlossen'} {executionModal.status === 'stopped' && 'Gestoppt'} {executionModal.status === 'error' && 'Fehler'} {executionModal.workflowId && Workflow: {executionModal.workflowId}}
{executionModal.logs.map((log, index) => (
[{formatTime(log.timestamp)}] {log.status && {log.status}:} {log.message} {log.progress !== undefined && log.progress !== null && log.progress < 1 && ({Math.round(log.progress * 100)}%)}
))}
{executionModal.status === 'running' && }
)} {logsModal.visible && logsModal.automation && (
setLogsModal({ visible: false, automation: null })}>
e.stopPropagation()} style={{ maxWidth: '700px' }}>

Ausführungsverlauf: {logsModal.automation.label}

{(!logsModal.automation.executionLogs || logsModal.automation.executionLogs.length === 0) ? (

Keine Ausführungen vorhanden

) : (
{[...logsModal.automation.executionLogs].reverse().map((log, index) => (
{formatTimestamp(log.timestamp)} {log.status || 'Unbekannt'} {log.workflowId && Workflow: {log.workflowId}}
{log.messages && log.messages.length > 0 && (
{log.messages.map((msg, msgIndex) =>
{msg}
)}
)}
))}
)}
)}
); };