/** * GraphicalEditorWorkflowsTasksPage * Tasks only (no workflow grouping). * Open tasks at top, completed tasks at bottom (expandable, scrollable). * Each task shows workflow, created, due, step, type, and action. * Right column: active workflows with manual or form entry point — start via execute (same as Workflows page). */ import React, { useState, useEffect, useCallback, useRef } from 'react'; import { Link } from 'react-router-dom'; import { FaChevronDown, FaChevronRight, FaPlay, FaSpinner, FaTimes, FaUpload } from 'react-icons/fa'; import { useInstanceId } from '../../../hooks/useCurrentInstance'; import { useApiRequest } from '../../../hooks/useApi'; import { fetchTasks, cancelPendingTaskStopRun, completeTask, fetchCompletedRuns, fetchWorkflows, executeGraph, type Automation2Task, type Automation2Workflow, type CompletedRun, } from '../../../api/workflowApi'; import { useToast } from '../../../contexts/ToastContext'; import { Popup } from '../../../components/UiComponents/Popup'; import { getAcceptStringFromConfig, fileMatchesAccept } from '../../../components/FlowEditor'; import { useFileOperations } from '../../../hooks/useFiles'; import styles from './Automation2WorkflowsTasks.module.css'; import { WorkflowRuntimeFormFields, useWorkflowRuntimeFormRequiredOk, type WorkflowRuntimeFormFieldRow, } from '../../../components/FlowEditor/workflowRuntime/WorkflowRuntimeFormFields'; import { useLanguage } from '../../../providers/language/LanguageContext'; function _nodeTypeLabel(nodeType: string, t: (k: string) => string): string { switch (nodeType) { case 'input.form': return t('Formular'); case 'input.approval': return t('Genehmigung'); case 'input.upload': return t('Upload'); case 'input.comment': return t('Kommentar'); case 'input.review': return t('Prüfung'); case 'input.selection': return t('Auswahl'); case 'input.confirmation': return t('Bestätigung'); default: return nodeType; } } function formatTimestamp(ts?: number): string { if (ts == null || ts <= 0) return '—'; const d = new Date(typeof ts === 'number' && ts < 1e12 ? ts * 1000 : ts); return d.toLocaleString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', }); } function getNodeStepLabel(config: Record): string { const title = config?.title; if (typeof title === 'string' && title.trim()) return title; const label = config?.label; if (typeof label === 'string' && label.trim()) return label; if (typeof label === 'object' && label != null && 'de' in (label as Record)) { return (label as Record).de ?? (label as Record).en ?? ''; } return ''; } /** Active workflow with at least one enabled manual or form start (same idea as Tasks / editor on-demand). */ function hasManualOrFormInvocation(wf: Automation2Workflow): boolean { const invs = wf.invocations || []; return invs.some( (i) => i.enabled !== false && (i.kind === 'manual' || i.kind === 'form') ); } /** * Primary entry for execute — align with first start node in graph order (backend-driven), * then fall back to manual / form / api on invocations list. */ function getPrimaryEntryPoint(wf: Automation2Workflow) { const invs = wf.invocations || []; const nodes = wf.graph?.nodes ?? []; for (const n of nodes) { const nodeType = n.type; if (typeof nodeType === 'string' && nodeType.startsWith('trigger.')) { const inv = invs.find((i) => i.enabled !== false && i.id === n.id); if (inv) return inv; } } return ( invs.find((i) => i.enabled !== false && i.kind === 'manual') || invs.find((i) => i.enabled !== false && (i.kind === 'form' || i.kind === 'api')) ); } /** Form field rows from graph trigger.form for workflow list (parameters.formFields). */ function getTriggerFormFieldsForWorkflow(wf: Automation2Workflow): WorkflowRuntimeFormFieldRow[] { const primary = getPrimaryEntryPoint(wf); if (!primary || primary.kind !== 'form') return []; const nodes = wf.graph?.nodes ?? []; let node = nodes.find((n) => n.id === primary.id && n.type === 'trigger.form'); if (!node) node = nodes.find((n) => n.type === 'trigger.form'); if (!node) return []; const raw = (node.parameters as Record | undefined)?.formFields; if (!Array.isArray(raw)) return []; return raw as WorkflowRuntimeFormFieldRow[]; } function primaryKindLabel(kind: string): string { if (kind === 'form') return 'Formular'; if (kind === 'manual') return 'Manuell'; return kind; } export const GraphicalEditorWorkflowsTasksPage: React.FC = () => { const { t } = useLanguage(); const instanceId = useInstanceId(); const { request } = useApiRequest(); const { showSuccess, showError } = useToast(); const [tasks, setTasks] = useState([]); const [completedRuns, setCompletedRuns] = useState([]); const [startableWorkflows, setStartableWorkflows] = useState([]); const [loading, setLoading] = useState(true); const [completedExpanded, setCompletedExpanded] = useState(false); const [outputExpanded, setOutputExpanded] = useState(true); const [submitting, setSubmitting] = useState(null); const [dismissingTaskId, setDismissingTaskId] = useState(null); const [executingWorkflowId, setExecutingWorkflowId] = useState(null); const [formStartWorkflow, setFormStartWorkflow] = useState(null); const [formStartFields, setFormStartFields] = useState([]); const [startFormData, setStartFormData] = useState>({}); const load = useCallback(async () => { if (!instanceId) return; setLoading(true); try { const [taskList, runs] = await Promise.all([ fetchTasks(request, instanceId), fetchCompletedRuns(request, instanceId, 20), ]); setTasks(taskList); setCompletedRuns(runs); try { const activeWfs = await fetchWorkflows(request, instanceId, { active: true }); const list: Automation2Workflow[] = Array.isArray(activeWfs) ? activeWfs : (activeWfs && typeof activeWfs === 'object' && 'items' in activeWfs && Array.isArray((activeWfs as { items: Automation2Workflow[] }).items) ? (activeWfs as { items: Automation2Workflow[] }).items : []); setStartableWorkflows( list.filter( (w) => w.active !== false && hasManualOrFormInvocation(w) ) ); } catch (we) { console.error('[graphicalEditor] load startable workflows failed', we); setStartableWorkflows([]); } } catch (e) { console.error('[graphicalEditor] load failed', e); } finally { setLoading(false); } }, [instanceId, request]); useEffect(() => { load(); }, [load]); const handleComplete = async (taskId: string, result: Record) => { if (!instanceId) return; setSubmitting(taskId); try { await completeTask(request, instanceId, taskId, result); await load(); } catch (e) { console.error('[graphicalEditor] complete failed', e); } finally { setSubmitting(null); } }; const handleDismissOpenTask = async (taskId: string) => { if (!instanceId) return; setDismissingTaskId(taskId); try { const res = await cancelPendingTaskStopRun(request, instanceId, taskId); if (res.success) { showSuccess(t('Ausführung abgebrochen')); await load(); } else { showError(t('Abbrechen fehlgeschlagen')); } } catch (e: unknown) { const msg = (e as { message?: string })?.message ?? t('Abbrechen fehlgeschlagen'); showError(msg); console.error('[graphicalEditor] cancel task failed', e); } finally { setDismissingTaskId(null); } }; const handleStartWorkflow = useCallback( async (wf: Automation2Workflow) => { if (!instanceId || !wf.graph) return; const primary = getPrimaryEntryPoint(wf); if (primary?.kind === 'form') { setFormStartFields(getTriggerFormFieldsForWorkflow(wf)); setStartFormData({}); setFormStartWorkflow(wf); return; } setExecutingWorkflowId(wf.id); try { const result = await executeGraph(request, instanceId, wf.graph, wf.id, { ...(primary ? { entryPointId: primary.id } : {}), }); if (result?.success) { if (result?.paused) { showSuccess(t('Workflow gestartet und bei Human Task pausiert.')); } else { showSuccess(t('Workflow gestartet')); } await load(); } else { showError(result?.error || t('Ausführung fehlgeschlagen')); } } catch (e: unknown) { const msg = (e as { message?: string })?.message ?? t('Ausführung fehlgeschlagen'); showError(msg); } finally { setExecutingWorkflowId(null); } }, [instanceId, request, showSuccess, showError, load, t] ); const formStartRequiredOk = useWorkflowRuntimeFormRequiredOk(formStartFields, startFormData); const handleFormStartSubmit = useCallback(async () => { if (!instanceId || !formStartWorkflow?.graph) return; const wf = formStartWorkflow; const primary = getPrimaryEntryPoint(wf); const payload = { ...startFormData }; setExecutingWorkflowId(wf.id); try { const result = await executeGraph(request, instanceId, wf.graph, wf.id, { ...(primary ? { entryPointId: primary.id } : {}), payload, }); if (result?.success) { if (result?.paused) { showSuccess(t('Workflow gestartet und bei Human Task pausiert.')); } else { showSuccess(t('Workflow gestartet')); } await load(); } else { showError(result?.error || t('Ausführung fehlgeschlagen')); } } catch (e: unknown) { const msg = (e as { message?: string })?.message ?? t('Ausführung fehlgeschlagen'); showError(msg); } finally { setExecutingWorkflowId(null); setFormStartWorkflow(null); } }, [ instanceId, formStartWorkflow, startFormData, request, showSuccess, showError, load, t, ]); const openTasks = tasks.filter((task) => task.status === 'pending'); const completedTasks = tasks.filter((task) => task.status !== 'pending'); if (!instanceId) { return (

{t('keine Featureinstanz gefunden')}

); } if (loading) { return (

{t('lade Tasks')}

); } return (
{/* Open tasks */}

{t('Offene Tasks')} {openTasks.length > 0 && {openTasks.length}}

{openTasks.length === 0 ? (

{t('keine offenen Tasks')}

) : (
{openTasks.map((task) => ( handleComplete(task.id, result)} submitting={submitting === task.id} showDismiss onDismiss={() => handleDismissOpenTask(task.id)} dismissing={dismissingTaskId === task.id} /> ))}
)}
{/* Completed tasks */}
{completedExpanded && (
{completedTasks.length === 0 ? (

{t('keine erledigten Tasks')}

) : ( completedTasks.map((task) => ( handleComplete(task.id, result)} submitting={submitting === task.id} readOnly /> )) )}
)}
{/* Output – abgeschlossene Workflows mit Ergebnis */}
{outputExpanded && (
{completedRuns.length === 0 ? (

{t('Keine abgeschlossenen Workflows. Führen Sie einen Workflow aus (z.B. im Editor), um hier die Ergebnisse zu sehen.')}

) : ( completedRuns.map((run) => ( )) )}
)}
setFormStartWorkflow(null)} closable={ !(formStartWorkflow != null && executingWorkflowId === formStartWorkflow.id) } closeOnEscape={ !(formStartWorkflow != null && executingWorkflowId === formStartWorkflow.id) } size="medium" footerContent={ } >
); }; /** Output card for completed workflow runs – zeigt nur die erstellten Dateien (mit fileId). */ const OutputCard: React.FC<{ run: CompletedRun; instanceId?: string; }> = ({ run }) => { const { t } = useLanguage(); const ts = run.sysModifiedAt ?? run.sysCreatedAt ?? 0; const files: Array<{ name: string; fileId: string }> = []; const nodeOutputs = run.nodeOutputs ?? {}; for (const [, out] of Object.entries(nodeOutputs)) { if (!out || typeof out !== 'object') continue; const o = out as Record; const docs = (o.documents ?? o.documentList ?? []) as Array>; if (!Array.isArray(docs)) continue; for (const d of docs) { const fileId = (d.validationMetadata as Record)?.fileId as string | undefined; if (fileId) { files.push({ name: String(d.documentName ?? d.fileName ?? t('Datei')), fileId, }); } } } return (
{t('Workflow')} {run.workflowLabel || run.workflowId || '—'}
{t('Abgeschlossen')} {formatTimestamp(ts)}
{files.length > 0 ? (
{t('Dateien')}
    {files.map((f, j) => (
  • {f.name}
  • ))}
) : (

{t('kein Output, z.B. Workflow ohne')}

)}
); }; interface TaskCardProps { task: Automation2Task; instanceId?: string; onSubmit: (result: Record) => void; submitting: boolean; readOnly?: boolean; /** Open-task card: show top-right control to cancel run and remove from list. */ showDismiss?: boolean; onDismiss?: () => void; dismissing?: boolean; } const TaskCard: React.FC = ({ task, instanceId, onSubmit, submitting, readOnly = false, showDismiss = false, onDismiss, dismissing = false, }) => { const { t } = useLanguage(); const { handleFileUpload } = useFileOperations(); const [formData, setFormData] = useState>({}); const [formPopupOpen, setFormPopupOpen] = useState(false); const [uploadedFiles, setUploadedFiles] = useState }>>([]); const [uploading, setUploading] = useState(false); const [uploadError, setUploadError] = useState(null); const fileInputRef = useRef(null); const config = task.config ?? {}; const nodeType = task.nodeType; const stepLabel = getNodeStepLabel(config); const inputFormFields: WorkflowRuntimeFormFieldRow[] = nodeType === 'input.form' ? ((config.fields as WorkflowRuntimeFormFieldRow[]) ?? []) : []; const inputFormRequiredOk = useWorkflowRuntimeFormRequiredOk(inputFormFields, formData); useEffect(() => { setUploadedFiles([]); setUploadError(null); }, [task.id]); const renderInput = () => { if (readOnly) return null; switch (nodeType) { case 'input.form': { const formContent = ( ); return ( <> setFormPopupOpen(false)} size="medium" footerContent={ } > {formContent} ); } case 'input.approval': return (
{config.title != null && String(config.title) !== '' &&

{String(config.title)}

} {config.description != null && String(config.description) !== '' &&

{String(config.description)}

}
); case 'input.comment': return (