/** * 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 { FormGeneratorForm } from '../../components/FormGenerator/FormGeneratorForm'; import { FaSync, FaRobot, FaPlay, FaPlus, FaToggleOn, FaToggleOff, FaFileAlt, FaStop, FaList, FaTimes, FaCheck, FaExclamationCircle, FaSpinner } from 'react-icons/fa'; import { useToast } from '../../contexts/ToastContext'; import { useApiRequest } from '../../hooks/useApi'; import styles from '../admin/Admin.module.css'; interface WorkflowLog { id: string; timestamp: number; message: string; status?: string; progress?: number; } export const AutomationsPage: React.FC = () => { // Data hook const { data: automations, attributes, permissions, pagination, loading, error, refetch, fetchAutomationById, updateOptimistically, } = useAutomations(); // Operations hook const { handleAutomationCreate, handleAutomationUpdate, handleAutomationDelete, handleAutomationExecute, handleAutomationToggleActive, handleInlineUpdate, fetchTemplates, deletingAutomations, executingAutomations, } = useAutomationOperations(); const { showSuccess, showError, showInfo } = useToast(); const { request } = useApiRequest(); // Modal states const [showCreateModal, setShowCreateModal] = useState(false); const [editingAutomation, setEditingAutomation] = useState(null); 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 ID fields from display const columns = useMemo(() => { const hiddenColumns = ['id', 'mandateId', '_createdBy', '_createdAt', '_modifiedAt', 'template', 'executionLogs']; return (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, })); }, [attributes]); // Check permissions const canCreate = permissions?.create !== 'n'; const canUpdate = permissions?.update !== 'n'; const canDelete = permissions?.delete !== 'n'; // Handle edit click const handleEditClick = async (automation: Automation) => { const fullAutomation = await fetchAutomationById(automation.id); if (fullAutomation) { setEditingAutomation(fullAutomation as Automation); } }; // Handle create submit const handleCreateSubmit = async (data: Partial) => { const result = await handleAutomationCreate(data as any); if (result) { setShowCreateModal(false); showSuccess('Automatisierung erstellt'); refetch(); } }; // Handle edit submit const handleEditSubmit = async (data: Partial) => { if (!editingAutomation) return; const success = await handleAutomationUpdate(editingAutomation.id, data as any); if (success) { setEditingAutomation(null); showSuccess('Automatisierung aktualisiert'); refetch(); } }; // Handle delete single automation const handleDelete = async (automation: Automation) => { if (window.confirm(`Möchten Sie die Automatisierung "${automation.label}" wirklich löschen?`)) { const success = await handleAutomationDelete(automation.id); if (success) { showSuccess('Automatisierung gelöscht'); 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 const handleTemplateSelect = async (template: AutomationTemplate) => { setShowTemplateModal(false); // Pre-fill form with template data const prefillData: Partial = { label: template.template?.overview || 'Neue Automatisierung', template: JSON.stringify(template.template, null, 2), placeholders: template.parameters || {}, active: false, schedule: '0 */4 * * *', }; // Create automation directly const result = await handleAutomationCreate(prefillData as any); if (result) { showSuccess('Automatisierung aus Vorlage erstellt'); refetch(); } }; // 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 => ({ ...prev, logs: [...prev.logs, ...logs], })); 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: [], }); }; // Handle toggle active const handleToggleActive = async (automation: Automation) => { updateOptimistically(automation.id, { active: !automation.active }); const success = await handleAutomationToggleActive(automation.id, automation.active); if (success) { showSuccess(automation.active ? 'Automatisierung deaktiviert' : 'Automatisierung aktiviert'); } else { updateOptimistically(automation.id, { active: automation.active }); showError('Fehler beim Ändern des Status'); } }; // Show logs modal const handleShowLogs = async (automation: Automation) => { const fullAutomation = await fetchAutomationById(automation.id); setLogsModal({ visible: true, automation: fullAutomation as Automation || automation, }); }; // Form attributes for create/edit modal const formAttributes = useMemo(() => { const excludedFields = ['id', 'mandateId', '_createdBy', '_createdAt', '_modifiedAt', 'status', 'executionLogs']; return (attributes || []) .filter(attr => !excludedFields.includes(attr.name)); }, [attributes]); // 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: 'toggleActive', icon: (row: any) => row.active ? : , onClick: handleToggleActive, title: (row: any) => row.active ? 'Deaktivieren' : 'Aktivieren', } as any, { id: 'logs', icon: , onClick: handleShowLogs, title: 'Ausführungsverlauf', }, ]} onDelete={handleDelete} hookData={{ refetch, permissions, pagination, handleDelete: handleAutomationDelete, handleInlineUpdate, updateOptimistically, }} emptyMessage="Keine Automatisierungen gefunden" /> )}
{/* Create Modal */} {showCreateModal && (
setShowCreateModal(false)}>
e.stopPropagation()}>

Neue Automatisierung

{formAttributes.length === 0 ? (
Lade Formular...
) : ( setShowCreateModal(false)} submitButtonText="Erstellen" cancelButtonText="Abbrechen" /> )}
)} {/* Edit Modal */} {editingAutomation && (
setEditingAutomation(null)}>
e.stopPropagation()}>

Automatisierung bearbeiten

{formAttributes.length === 0 ? (
Lade Formular...
) : ( setEditingAutomation(null)} submitButtonText="Speichern" cancelButtonText="Abbrechen" /> )}
)} {/* Template Selection Modal */} {showTemplateModal && (
setShowTemplateModal(false)}>
e.stopPropagation()}>

Vorlage auswählen

{templates.map((template, index) => (

{template.template?.overview || `Vorlage ${index + 1}`}

{template.template?.tasks?.[0]?.description || template.template?.tasks?.[0]?.objective || 'Keine Beschreibung'}

))}
)} {/* 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;