/** * GraphicalEditorWorkflowsPage * List of saved workflows with FormGeneratorTable. * Shows: label, active, isRunning, stuckAt, createdAt, lastStartedAt, runCount. * Filter: Alle | Aktiv | Inaktiv. * Actions: Edit, Delete, Aktivieren/Deaktivieren, Ausführen (nur bei manuellem Trigger). */ import React, { useState, useCallback, useEffect } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { FaPlay, FaSync, FaCheck, FaBan, FaPen } from 'react-icons/fa'; import { usePrompt } from '../../../hooks/usePrompt'; import { FormGeneratorTable, type ColumnConfig } from '../../../components/FormGenerator'; import { useInstanceId } from '../../../hooks/useCurrentInstance'; import { useApiRequest } from '../../../hooks/useApi'; import { fetchWorkflows, deleteWorkflow, executeGraph, updateWorkflow, type Automation2Workflow, } from '../../../api/workflowApi'; import { useToast } from '../../../contexts/ToastContext'; import { formatUnixTimestamp } from '../../../utils/time'; import styles from '../../../pages/admin/Admin.module.css'; import { useLanguage } from '../../../providers/language/LanguageContext'; function formatTs(ts?: number): string { if (ts == null || ts <= 0) return '—'; const sec = ts < 1e12 ? ts : ts / 1000; const { time } = formatUnixTimestamp(sec, undefined, { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', }); return time; } export const GraphicalEditorWorkflowsPage: React.FC = () => { const { t } = useLanguage(); const instanceId = useInstanceId(); const { mandateId } = useParams<{ mandateId: string }>(); const { request } = useApiRequest(); const navigate = useNavigate(); const { showSuccess, showError } = useToast(); const { prompt: promptInput, PromptDialog } = usePrompt(); const [workflows, setWorkflows] = useState([]); const [loading, setLoading] = useState(true); const [executingId, setExecutingId] = useState(null); const [togglingId, setTogglingId] = useState(null); const [activeFilter, setActiveFilter] = useState<'all' | 'active' | 'inactive'>('all'); const [paginationMeta, setPaginationMeta] = useState(null); const load = useCallback(async (paginationParams?: any) => { if (!instanceId) return; setLoading(true); try { const active = activeFilter === 'active' ? true : activeFilter === 'inactive' ? false : undefined; const result = await fetchWorkflows(request, instanceId, { active, pagination: paginationParams }); if (result && typeof result === 'object' && 'items' in result && !Array.isArray(result)) { setWorkflows((result as any).items); setPaginationMeta((result as any).pagination); } else { setWorkflows(result as Automation2Workflow[]); setPaginationMeta(null); } } catch (e) { console.error('[graphicalEditor] load workflows failed', e); showError(t('Fehler beim Laden der Workflows')); } finally { setLoading(false); } }, [instanceId, request, showError, activeFilter, t]); useEffect(() => { load(); }, [load]); const handleDelete = useCallback( async (workflowId: string): Promise => { if (!instanceId) return false; try { await deleteWorkflow(request, instanceId, workflowId); showSuccess(t('Workflow gelöscht')); await load(); return true; } catch (e: any) { showError(t('Fehler: {msg}', { msg: e?.message || t('Löschen fehlgeschlagen') })); return false; } }, [instanceId, request, showSuccess, showError, load, t] ); const handleEdit = useCallback( (row: Automation2Workflow) => { if (!mandateId || !instanceId) return; navigate(`/mandates/${mandateId}/graphicalEditor/${instanceId}/editor?workflowId=${row.id}`); }, [mandateId, instanceId, navigate] ); const hasManualTrigger = useCallback((row: Automation2Workflow): boolean => { const invs = row.invocations || []; return invs.some((i) => i.enabled && (i.kind === 'manual' || i.kind === 'api')); }, []); const handleToggleActive = useCallback( async (row: Automation2Workflow) => { if (!instanceId) return; const next = !(row.active !== false); setTogglingId(row.id); try { await updateWorkflow(request, instanceId, row.id, { active: next }); showSuccess(next ? t('Workflow aktiviert') : t('Workflow deaktiviert')); await load(); } catch (e: any) { showError(t('Fehler: {msg}', { msg: e?.message || t('Status-Update fehlgeschlagen') })); } finally { setTogglingId(null); } }, [instanceId, request, showSuccess, showError, load, t] ); const handleRename = useCallback( async (row: Automation2Workflow) => { if (!instanceId) return; const newLabel = await promptInput(t('Neuer Name:'), { title: t('Workflow umbenennen'), defaultValue: row.label, placeholder: t('Workflow-Name'), }); if (!newLabel || newLabel.trim() === row.label) return; try { await updateWorkflow(request, instanceId, row.id, { label: newLabel.trim() }); showSuccess(t('Workflow umbenannt')); await load(); } catch (e: any) { showError(t('Fehler: {msg}', { msg: e?.message || t('Umbenennen fehlgeschlagen') })); } }, [instanceId, request, promptInput, showSuccess, showError, load, t] ); const handleExecute = useCallback( async (row: Automation2Workflow) => { if (!instanceId) return; setExecutingId(row.id); try { const invs = row.invocations || []; const primary = invs.find((i) => i.enabled && i.kind === 'manual') || invs.find((i) => i.enabled && (i.kind === 'form' || i.kind === 'api')); const result = await executeGraph(request, instanceId, row.graph!, row.id, { ...(primary ? { entryPointId: primary.id } : {}), }); if (result?.success) { if (result?.paused) { showSuccess(t('Workflow gestartet und bei Human Task pausiert. Öffne Workflows & Tasks.')); } else { showSuccess(t('Workflow ausgeführt')); } await load(); } else { showError(result?.error || t('Ausführung fehlgeschlagen')); } } catch (e: any) { showError(t('Fehler: {msg}', { msg: e?.message || t('Ausführung fehlgeschlagen') })); } finally { setExecutingId(null); } }, [instanceId, request, showSuccess, showError, load, t] ); const columns: ColumnConfig[] = [ { key: 'label', label: t('Workflow'), type: 'string', width: 200, sortable: true }, { key: 'active', label: t('Aktiv (Spalte)'), type: 'boolean', width: 80, formatter: (value: boolean) => value !== false ? ( Ja ) : ( Nein ), }, { key: 'isRunning', label: t('läuft'), type: 'boolean', width: 80, formatter: (value: boolean) => value ? ( {t('Ja')} ) : ( Nein ), }, { key: 'stuckAtNodeLabel', label: t('steht bei'), type: 'string', width: 160, formatter: (value: string, row: Automation2Workflow) => row.isRunning && (value || row.stuckAtNodeId) ? value || row.stuckAtNodeId || '—' : '—', }, { key: 'createdAt', label: t('Erstellt'), type: 'number', width: 140, formatter: (v: number) => formatTs(v), }, { key: 'lastStartedAt', label: t('zuletzt gestartet'), type: 'number', width: 160, formatter: (v: number) => formatTs(v), }, { key: 'runCount', label: t('Läufe'), type: 'number', width: 80, formatter: (v: number) => (v != null ? String(v) : '0'), }, ]; const hookData = { refetch: load, handleDelete: (id: string) => handleDelete(id), pagination: paginationMeta, }; if (!instanceId) { return (

{t('Keine Feature-Instanz gefunden')}

); } return (

{t('Workflows verwalten, ausführen und bearbeiten')}

{(['all', 'active', 'inactive'] as const).map((f) => ( ))}
data={workflows} columns={columns} loading={loading} pagination={true} pageSize={25} searchable={true} filterable={true} sortable={true} selectable={true} actionButtons={[ { type: 'edit', title: t('bearbeiten'), onAction: handleEdit, }, { type: 'delete', title: t('löschen'), }, ]} customActions={[ { id: 'rename', icon: , title: t('umbenennen'), onClick: (row) => handleRename(row), }, { id: 'activate', icon: , title: t('aktivieren'), onClick: (row) => handleToggleActive(row), loading: (row) => togglingId === row.id, visible: (row) => row.active === false, }, { id: 'deactivate', icon: , title: t('deaktivieren'), onClick: (row) => handleToggleActive(row), loading: (row) => togglingId === row.id, visible: (row) => row.active !== false, }, { id: 'execute', icon: , title: t('ausführen'), onClick: (row) => handleExecute(row), loading: (row) => executingId === row.id, visible: (row) => hasManualTrigger(row), }, ]} onDelete={(row) => handleDelete(row.id)} hookData={hookData} emptyMessage={t('Keine Workflows gefunden. Erstelle einen.')} />
); };