/** * GraphicalEditorTemplatesPage * * Template management with scope tabs (Meine / Instanz / Mandant / System). * Uses FormGeneratorTable for the data list. * Actions: Copy to my workflows, Share (scope upgrade), Delete. */ import React, { useState, useCallback, useEffect, useMemo } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { FaCopy, FaSync, FaShareAlt, 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 { fetchTemplates, copyTemplate, shareTemplate, deleteWorkflow, updateWorkflow, type AutoWorkflowTemplate, type AutoTemplateScope, } from '../../../api/workflowApi'; import { fetchAttributes } from '../../../api/attributesApi'; import type { AttributeDefinition } from '../../../api/attributesApi'; import { resolveColumnTypes } from '../../../utils/columnTypeResolver'; 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 GraphicalEditorTemplatesPage: React.FC = () => { const { t } = useLanguage(); const scopeLabels = useMemo( (): Record => ({ user: t('Meine'), instance: t('Instanz'), mandate: t('Mandant'), system: t('System'), }), [t], ); const instanceId = useInstanceId(); const { mandateId } = useParams<{ mandateId: string }>(); const { request } = useApiRequest(); const navigate = useNavigate(); const { showSuccess, showError } = useToast(); const { prompt: promptInput, PromptDialog } = usePrompt(); const [templates, setTemplates] = useState([]); const [loading, setLoading] = useState(true); const [activeScope, setActiveScope] = useState('all'); const [copyingId, setCopyingId] = useState(null); const [sharingId, setSharingId] = useState(null); const [paginationMeta, setPaginationMeta] = useState(null); const [backendAttributes, setBackendAttributes] = useState([]); useEffect(() => { fetchAttributes(request, 'Automation2WorkflowView') .then(setBackendAttributes) .catch((err) => { console.error('[graphicalEditor] fetchAttributes Automation2WorkflowView failed', err); }); }, [request]); const load = useCallback(async (paginationParams?: any) => { if (!instanceId) return; setLoading(true); try { const scope = activeScope === 'all' ? undefined : activeScope; const result = await fetchTemplates(request, instanceId, scope, paginationParams); if (result && typeof result === 'object' && 'items' in result) { setTemplates(result.items as AutoWorkflowTemplate[]); setPaginationMeta(result.pagination); } else { setTemplates(result as AutoWorkflowTemplate[]); setPaginationMeta(null); } } catch (e) { console.error('[graphicalEditor] load templates failed', e); showError(t('Fehler beim Laden der Vorlagen')); } finally { setLoading(false); } }, [instanceId, request, showError, activeScope, t]); useEffect(() => { load(); }, [load]); const handleDelete = useCallback( async (templateId: string): Promise => { if (!instanceId) return false; try { await deleteWorkflow(request, instanceId, templateId); showSuccess(t('Vorlage 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 handleCopy = useCallback( async (row: AutoWorkflowTemplate) => { if (!instanceId) return; setCopyingId(row.id); try { await copyTemplate(request, instanceId, row.id); showSuccess(t('Vorlage als Workflow kopiert')); } catch (e: any) { showError(t('Fehler: {msg}', { msg: e?.message || t('Kopieren fehlgeschlagen') })); } finally { setCopyingId(null); } }, [instanceId, request, showSuccess, showError, t] ); const handleShare = useCallback( async (row: AutoWorkflowTemplate, targetScope: AutoTemplateScope) => { if (!instanceId) return; setSharingId(row.id); try { await shareTemplate(request, instanceId, row.id, targetScope); showSuccess(t('Scope geändert: {scope}', { scope: scopeLabels[targetScope] })); await load(); } catch (e: any) { showError(t('Fehler: {msg}', { msg: e?.message || t('Scope-Änderung fehlgeschlagen') })); } finally { setSharingId(null); } }, [instanceId, request, showSuccess, showError, load, scopeLabels, t] ); const [scopeMenuId, setScopeMenuId] = useState(null); const handleRename = useCallback( async (row: AutoWorkflowTemplate) => { if (!instanceId) return; const newLabel = await promptInput(t('Neuer Name:'), { title: t('Vorlage umbenennen'), defaultValue: row.label, placeholder: t('Vorlagen-Name'), }); if (!newLabel || newLabel.trim() === row.label) return; try { await updateWorkflow(request, instanceId, row.id, { label: newLabel.trim() }); showSuccess(t('Vorlage umbenannt')); await load(); } catch (e: any) { showError(t('Fehler: {msg}', { msg: e?.message || t('Umbenennen fehlgeschlagen') })); } }, [instanceId, request, promptInput, showSuccess, showError, load, t] ); const handleEdit = useCallback( (row: AutoWorkflowTemplate) => { if (!mandateId || !instanceId) return; navigate(`/mandates/${mandateId}/graphicalEditor/${instanceId}/editor?workflowId=${row.id}`); }, [mandateId, instanceId, navigate] ); const _rawColumns: ColumnConfig[] = useMemo( () => [ { key: 'label', label: t('Vorlage'), width: 220, sortable: true, filterable: true }, { key: 'templateScope', width: 100, sortable: true, filterable: true }, { key: 'sharedReadOnly', width: 100, sortable: true, filterable: true }, { key: 'sysCreatedBy', width: 140, sortable: true, filterable: true, displayField: 'sysCreatedByLabel' }, { key: 'sysCreatedAt', width: 140, sortable: true, filterable: true, formatter: (v: number) => _formatTs(v) }, ], [t], ); const columns = useMemo( () => resolveColumnTypes(_rawColumns, backendAttributes), [_rawColumns, backendAttributes], ); if (!instanceId) { return (

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

); } return (

{t('Vorlagen verwalten, kopieren und freigeben')}

{(['all', 'user', 'instance', 'mandate', 'system'] as const).map((s) => ( ))}
data={templates} columns={columns} loading={loading} pagination={true} pageSize={25} searchable={true} filterable={true} sortable={true} selectable={true} apiEndpoint={`/api/workflows/${instanceId}/templates`} actionButtons={[ { type: 'edit', title: t('Im Editor öffnen'), onAction: handleEdit, }, { type: 'delete', title: t('Löschen'), }, ]} customActions={[ { id: 'rename', icon: , title: t('Umbenennen'), onClick: (row) => handleRename(row), visible: (row) => (row.templateScope || 'user') !== 'system', }, { id: 'copy', icon: , title: t('Als Workflow kopieren'), onClick: (row) => handleCopy(row), loading: (row) => copyingId === row.id, }, { id: 'scope', icon: , title: t('Bereich ändern'), onClick: (row) => setScopeMenuId(scopeMenuId === row.id ? null : row.id), loading: (row) => sharingId === row.id, visible: (row) => (row.templateScope || 'user') !== 'system', }, ]} onDelete={(row) => handleDelete(row.id)} hookData={{ refetch: load, handleDelete: (id: string) => handleDelete(id), pagination: paginationMeta }} emptyMessage={t('Keine Vorlagen gefunden. Erstelle eine.')} />
{/* Scope change dropdown overlay */} {scopeMenuId && (() => { const tpl = templates.find((row) => row.id === scopeMenuId); if (!tpl) return null; const currentScope = (tpl.templateScope || 'user') as AutoTemplateScope; const scopes: AutoTemplateScope[] = ['user', 'instance', 'mandate']; return (
setScopeMenuId(null)} >
e.stopPropagation()} >

{t('Bereich ändern')}

{t('Aktuell:')} {scopeLabels[currentScope]}

{scopes.map(s => ( ))}
); })()}
); };