frontend_nyla/src/pages/views/graphicalEditor/GraphicalEditorTemplatesPage.tsx
2026-04-07 00:49:12 +02:00

269 lines
8.6 KiB
TypeScript

/**
* 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 } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { FaCopy, FaSync, FaShareAlt } from 'react-icons/fa';
import { FormGeneratorTable, type ColumnConfig } from '../../../components/FormGenerator';
import { useInstanceId } from '../../../hooks/useCurrentInstance';
import { useApiRequest } from '../../../hooks/useApi';
import {
fetchTemplates,
copyTemplate,
shareTemplate,
deleteWorkflow,
type AutoWorkflowTemplate,
type AutoTemplateScope,
} from '../../../api/workflowApi';
import { useToast } from '../../../contexts/ToastContext';
import { formatUnixTimestamp } from '../../../utils/time';
import styles from '../../../pages/admin/Admin.module.css';
const SCOPE_LABELS: Record<AutoTemplateScope, string> = {
user: 'Meine',
instance: 'Instanz',
mandate: 'Mandant',
system: 'System',
};
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 instanceId = useInstanceId();
const { mandateId } = useParams<{ mandateId: string }>();
const { request } = useApiRequest();
const navigate = useNavigate();
const { showSuccess, showError } = useToast();
const [templates, setTemplates] = useState<AutoWorkflowTemplate[]>([]);
const [loading, setLoading] = useState(true);
const [activeScope, setActiveScope] = useState<AutoTemplateScope | 'all'>('all');
const [copyingId, setCopyingId] = useState<string | null>(null);
const [sharingId, setSharingId] = useState<string | null>(null);
const [paginationMeta, setPaginationMeta] = useState<any>(null);
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('Fehler beim Laden der Vorlagen');
} finally {
setLoading(false);
}
}, [instanceId, request, showError, activeScope]);
useEffect(() => {
load();
}, [load]);
const handleDelete = useCallback(
async (templateId: string): Promise<boolean> => {
if (!instanceId) return false;
try {
await deleteWorkflow(request, instanceId, templateId);
showSuccess('Vorlage gelöscht');
await load();
return true;
} catch (e: any) {
showError(`Fehler: ${e?.message || 'Löschen fehlgeschlagen'}`);
return false;
}
},
[instanceId, request, showSuccess, showError, load]
);
const handleCopy = useCallback(
async (row: AutoWorkflowTemplate) => {
if (!instanceId) return;
setCopyingId(row.id);
try {
await copyTemplate(request, instanceId, row.id);
showSuccess('Vorlage als Workflow kopiert');
} catch (e: any) {
showError(`Fehler: ${e?.message || 'Kopieren fehlgeschlagen'}`);
} finally {
setCopyingId(null);
}
},
[instanceId, request, showSuccess, showError]
);
const handleShare = useCallback(
async (row: AutoWorkflowTemplate) => {
if (!instanceId) return;
const currentScope = row.templateScope || 'user';
const nextScope: AutoTemplateScope =
currentScope === 'user' ? 'instance' : currentScope === 'instance' ? 'mandate' : 'mandate';
setSharingId(row.id);
try {
await shareTemplate(request, instanceId, row.id, nextScope);
showSuccess(`Vorlage freigegeben (Scope: ${SCOPE_LABELS[nextScope]})`);
await load();
} catch (e: any) {
showError(`Fehler: ${e?.message || 'Freigabe fehlgeschlagen'}`);
} finally {
setSharingId(null);
}
},
[instanceId, request, showSuccess, showError, load]
);
const handleEdit = useCallback(
(row: AutoWorkflowTemplate) => {
if (!mandateId || !instanceId) return;
navigate(`/mandates/${mandateId}/graphicalEditor/${instanceId}/editor?workflowId=${row.id}`);
},
[mandateId, instanceId, navigate]
);
const columns: ColumnConfig[] = [
{ key: 'label', label: 'Vorlage', type: 'string', width: 220, sortable: true },
{
key: 'templateScope',
label: 'Scope',
type: 'string',
width: 100,
formatter: (v: string) => SCOPE_LABELS[v as AutoTemplateScope] ?? v ?? '—',
},
{
key: 'sharedReadOnly',
label: 'Freigegeben',
type: 'boolean',
width: 100,
formatter: (v: boolean) =>
v ? (
<span style={{ color: 'var(--primary-color, #007bff)', fontWeight: 600 }}>Ja</span>
) : (
<span style={{ color: 'var(--text-secondary, #666)' }}>Nein</span>
),
},
{
key: 'sysCreatedBy',
label: 'Erstellt von',
type: 'string',
width: 140,
},
{
key: 'sysCreatedAt',
label: 'Erstellt',
type: 'number',
width: 140,
formatter: (v: number) => _formatTs(v),
},
];
if (!instanceId) {
return (
<div className={styles.adminPage}>
<p>Keine Feature-Instanz gefunden.</p>
</div>
);
}
return (
<div className={`${styles.adminPage} ${styles.adminPageFill}`}>
<div className={styles.pageHeader}>
<div>
<h1 className={styles.pageTitle}>Workflow-Vorlagen</h1>
<p className={styles.pageSubtitle}>
Vorlagen verwalten, kopieren und freigeben
</p>
</div>
<div className={styles.headerActions} style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<div style={{ display: 'flex', gap: 4 }}>
{(['all', 'user', 'instance', 'mandate', 'system'] as const).map((s) => (
<button
key={s}
className={activeScope === s ? styles.primaryButton : styles.secondaryButton}
onClick={() => setActiveScope(s)}
disabled={loading}
>
{s === 'all' ? 'Alle' : SCOPE_LABELS[s as AutoTemplateScope]}
</button>
))}
</div>
<button
className={styles.secondaryButton}
onClick={() => load()}
disabled={loading}
>
<FaSync className={loading ? 'spinning' : ''} /> Aktualisieren
</button>
</div>
</div>
<div className={styles.tableContainer}>
<FormGeneratorTable<AutoWorkflowTemplate>
data={templates}
columns={columns}
loading={loading}
pagination={true}
pageSize={25}
searchable={true}
filterable={true}
sortable={true}
selectable={false}
actionButtons={[
{
type: 'edit',
title: 'Im Editor öffnen',
onAction: handleEdit,
},
{
type: 'delete',
title: 'Löschen',
},
]}
customActions={[
{
id: 'copy',
icon: <FaCopy />,
title: 'Als Workflow kopieren',
onClick: (row) => handleCopy(row),
loading: (row) => copyingId === row.id,
},
{
id: 'share',
icon: <FaShareAlt />,
title: 'Scope erweitern (freigeben)',
onClick: (row) => handleShare(row),
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="Keine Vorlagen gefunden. Erstelle eine Vorlage aus einem bestehenden Workflow."
/>
</div>
</div>
);
};