/** * AutomationsPage * * Page for viewing and managing workflow automations using FormGeneratorTable. * 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 styles from '../admin/Admin.module.css'; interface WorkflowLog { id: string; timestamp: number; message: string; status?: string; progress?: number; } export const AutomationsPage: React.FC = () => { // Get mandate and feature instance from store (first chatbot instance or first available) const { getAllInstances } = useFeatureStore(); const instances = getAllInstances(); // Find first chatbot instance, or fall back to first available instance const chatbotInstance = instances.find(i => i.featureCode === 'chatbot') || instances[0]; const mandateId = chatbotInstance?.mandateId; const featureInstanceId = chatbotInstance?.id; // Data hook const { data: automations, attributes, permissions, pagination, loading, error, refetch, fetchAutomationById, updateOptimistically, } = useAutomations(); // Operations hook const { handleAutomationCreate, handleAutomationUpdate, handleAutomationDelete, handleAutomationExecute, handleInlineUpdate, fetchTemplates, deletingAutomations, executingAutomations, } = useAutomationOperations(); const { showSuccess, showError, showInfo } = useToast(); const { request } = useApiRequest(); // Editor states const [showEditor, setShowEditor] = useState(false); const [editingAutomation, setEditingAutomation] = useState(null); const [editorSaving, setEditorSaving] = useState(false); // Template selection states const [showTemplateModal, setShowTemplateModal] = useState(false); const [templates, setTemplates] = useState([]); const [loadingTemplates, setLoadingTemplates] = useState(false); // Execution modal state const [executionModal, setExecutionModal] = useState<{ visible: boolean; automationId: string | null; automationLabel: string; workflowId: string | null; status: 'starting' | 'running' | 'completed' | 'stopped' | 'error'; logs: WorkflowLog[]; }>({ visible: false, automationId: null, automationLabel: '', workflowId: null, status: 'starting', logs: [], }); // Logs modal state const [logsModal, setLogsModal] = useState<{ visible: boolean; automation: Automation | null; }>({ visible: false, automation: null, }); // Refs for polling const pollIntervalRef = useRef(null); const lastLogIdRef = useRef(null); const logContainerRef = useRef(null); // Initial fetch useEffect(() => { refetch(); }, []); // Cleanup polling on unmount useEffect(() => { return () => { if (pollIntervalRef.current) { clearInterval(pollIntervalRef.current); } }; }, []); // Auto-scroll logs useEffect(() => { if (logContainerRef.current) { logContainerRef.current.scrollTop = logContainerRef.current.scrollHeight; } }, [executionModal.logs]); // Generate columns from attributes - exclude internal fields, add enriched display columns const columns = useMemo(() => { const hiddenColumns = [ 'id', 'mandateId', 'featureInstanceId', '_createdBy', '_createdAt', '_modifiedAt', 'template', 'executionLogs', 'placeholders', // Hide enriched fields from attribute list (added manually below) '_createdByUserName', '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, })); // Add enriched display columns (from backend enrichment) 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: '_createdByUserName', label: 'Erstellt von', type: 'text' as any, sortable: true, filterable: true, searchable: true, width: 150, minWidth: 100, maxWidth: 250 }, ]; return [...attrColumns, ...enrichedColumns]; }, [attributes]); // Check permissions const canCreate = permissions?.create !== 'n'; const canUpdate = permissions?.update !== 'n'; const canDelete = permissions?.delete !== 'n'; // Handle edit click - open editor with automation data const handleEditClick = async (automation: Automation) => { const fullAutomation = await fetchAutomationById(automation.id); setEditingAutomation(fullAutomation as Automation || automation); setShowEditor(true); }; // Handle create click - open editor for new automation const handleCreateClick = () => { // Pre-fill with context const newAutomation: Partial = { mandateId: mandateId, featureInstanceId: featureInstanceId, label: '', schedule: '0 22 * * *', active: false, placeholders: {}, }; setEditingAutomation(newAutomation as Automation); setShowEditor(true); }; // Handle editor save const handleEditorSave = async (data: Partial) => { // Validate context if (!mandateId || !featureInstanceId) { showError('Fehler: Kein aktiver Mandant oder Feature-Instanz gefunden'); return; } setEditorSaving(true); try { // Add required context fields const saveData = { ...data, mandateId: mandateId, featureInstanceId: featureInstanceId, }; if (editingAutomation?.id) { // Update existing - include id in payload for backend validation 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 { // Create new 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); } }; // Handle editor cancel const handleEditorCancel = () => { setShowEditor(false); setEditingAutomation(null); }; // Handle delete single automation (confirmation handled by DeleteActionButton) const handleDelete = async (automation: Automation) => { const success = await handleAutomationDelete(automation.id); if (success) { showSuccess('Automatisierung gelöscht'); await refetch(); } }; // Load templates 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); } }; // Handle template selection - open editor with template data pre-filled const handleTemplateSelect = (template: AutomationTemplate) => { setShowTemplateModal(false); // Validate context - mandateId and featureInstanceId are required if (!mandateId || !featureInstanceId) { showError('Fehler: Kein aktiver Mandant oder Feature-Instanz gefunden'); return; } // Get label from template (can be multilingual) let templateLabel = 'Neue Automatisierung'; if (template.label) { if (typeof template.label === 'string') { templateLabel = template.label; } else if (typeof template.label === 'object') { // TextMultilingual - use German or English templateLabel = (template.label as any).de || (template.label as any).en || 'Neue Automatisierung'; } } else if (template.overview) { // TextMultilingual - use German or English templateLabel = typeof template.overview === 'string' ? template.overview : ((template.overview as any).de || (template.overview as any).en || 'Neue Automatisierung'); } // Convert placeholder values to strings (backend expects Dict[str, str]) 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); } } // Pre-fill form with template data and open editor for user to customize const prefillData: Partial = { mandateId: mandateId, featureInstanceId: featureInstanceId, label: templateLabel, template: typeof template.template === 'string' ? template.template : JSON.stringify(template.template, null, 2), placeholders: convertedPlaceholders, active: false, schedule: '0 22 * * *', }; // Open editor with pre-filled data (no id = create mode) setEditingAutomation(prefillData as Automation); setShowEditor(true); }; // Poll workflow logs const pollWorkflowLogs = useCallback(async (workflowId: string) => { try { const response = await request({ url: `/api/workflows/${workflowId}/logs`, method: 'get', params: lastLogIdRef.current ? { afterId: lastLogIdRef.current } : {}, }); const logs: WorkflowLog[] = response?.items || response || []; if (logs.length > 0) { setExecutionModal(prev => { // Deduplicate logs by ID 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; } // Check workflow status const statusResponse = await request({ url: `/api/workflows/${workflowId}`, method: 'get', }); const workflowStatus = statusResponse?.status; if (workflowStatus === 'completed' || workflowStatus === 'stopped' || workflowStatus === 'error' || workflowStatus === 'failed') { // Stop polling 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]); // Handle execute automation with modal const handleExecute = async (automation: Automation) => { // Reset and show modal lastLogIdRef.current = null; setExecutionModal({ visible: true, automationId: automation.id, automationLabel: automation.label, 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; if (workflowId) { setExecutionModal(prev => ({ ...prev, workflowId, status: 'running', logs: [...prev.logs, { id: 'started', timestamp: Date.now() / 1000, message: `Workflow ${workflowId} gestartet`, status: 'running', }], })); // Start polling pollIntervalRef.current = setInterval(() => { pollWorkflowLogs(workflowId); }, 2000); } } 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}`); } }; // Handle stop workflow const handleStopWorkflow = async () => { if (!executionModal.workflowId) return; try { await request({ url: `/api/workflows/${executionModal.workflowId}/stop`, method: 'post', }); 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}`); } }; // Close execution modal const closeExecutionModal = () => { if (pollIntervalRef.current) { clearInterval(pollIntervalRef.current); pollIntervalRef.current = null; } setExecutionModal({ visible: false, automationId: null, automationLabel: '', workflowId: null, status: 'starting', logs: [], }); }; // Show logs modal const handleShowLogs = async (automation: Automation) => { const fullAutomation = await fetchAutomationById(automation.id); setLogsModal({ visible: true, automation: fullAutomation as Automation || automation, }); }; // Format timestamp const formatTimestamp = (timestamp: number) => { if (!timestamp) return ''; const date = new Date(timestamp * 1000); return date.toLocaleString('de-DE'); }; // Format time only const formatTime = (timestamp: number) => { if (!timestamp) return ''; const date = new Date(timestamp * 1000); return date.toLocaleTimeString('de-DE'); }; // Get status icon 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" /> )}
{/* Automation Editor */} {showEditor && editingAutomation && ( )} {/* Template Selection Modal */} {showTemplateModal && (
setShowTemplateModal(false)}>
e.stopPropagation()}>

Vorlage auswählen

{templates.map((template, index) => { // Get label from TextMultilingual 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}`; // Get overview from TextMultilingual const overviewText = template.overview ? (typeof template.overview === 'string' ? template.overview : (template.overview as any).de || (template.overview as any).en || '') : ''; // Try to parse template JSON for additional info let parsedTemplate: any = null; try { if (template.template) { parsedTemplate = typeof template.template === 'string' ? JSON.parse(template.template) : template.template; } } catch { /* ignore parse errors */ } const description = overviewText || parsedTemplate?.overview || parsedTemplate?.tasks?.[0]?.objective || 'Keine Beschreibung'; return (

{labelText}

{description}

); })}
)} {/* Execution Modal */} {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' && ( )}
)} {/* Logs History Modal */} {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}
))}
)}
))}
)}
)}
); }; export default AutomationsPage;