264 lines
10 KiB
TypeScript
264 lines
10 KiB
TypeScript
/**
|
||
* AutomationTemplatesPage
|
||
*
|
||
* Page for managing automation templates (CRUD).
|
||
* System templates (isSystem=true) are read-only for non-SysAdmin, with duplicate option.
|
||
* Instance templates can be managed by instance admins/editors.
|
||
*/
|
||
|
||
import React, { useState, useMemo, useEffect } from 'react';
|
||
import { useAutomationTemplates, type AutomationTemplate } from '../../hooks/useAutomations';
|
||
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
|
||
import { AutomationEditor } from '../../components/AutomationEditor';
|
||
import { FaSync, FaPlus, FaFileAlt, FaCopy, FaLock } from 'react-icons/fa';
|
||
import { useToast } from '../../contexts/ToastContext';
|
||
import { useCurrentUser } from '../../hooks/useUsers';
|
||
import styles from '../admin/Admin.module.css';
|
||
|
||
export const AutomationTemplatesPage: React.FC = () => {
|
||
const {
|
||
templates,
|
||
attributes,
|
||
loading,
|
||
error,
|
||
permissions,
|
||
refetch,
|
||
createTemplate,
|
||
updateTemplate,
|
||
deleteTemplate,
|
||
duplicateTemplate,
|
||
getTemplate,
|
||
} = useAutomationTemplates();
|
||
const { user: currentUser } = useCurrentUser();
|
||
const isSysAdmin = currentUser?.isSysAdmin || false;
|
||
|
||
const { showSuccess, showError } = useToast();
|
||
|
||
// Editor states
|
||
const [showEditor, setShowEditor] = useState(false);
|
||
const [editingTemplate, setEditingTemplate] = useState<AutomationTemplate | null>(null);
|
||
const [saving, setSaving] = useState(false);
|
||
|
||
// Initial fetch
|
||
useEffect(() => {
|
||
refetch();
|
||
}, []);
|
||
|
||
// Check permissions
|
||
const canCreate = permissions?.create !== 'n';
|
||
const canUpdate = permissions?.update !== 'n';
|
||
const canDelete = permissions?.delete !== 'n';
|
||
|
||
// Table columns - FormGeneratorTable auto-renders TextMultilingual in user language
|
||
const columns = useMemo(() => [
|
||
{ key: 'label', label: 'Label', type: 'string' as const, sortable: true, searchable: true, width: 200 },
|
||
{ key: 'overview', label: 'Beschreibung', type: 'string' as const, width: 300 },
|
||
{ key: 'isSystem', label: 'Typ', type: 'custom' as const, width: 100, render: (value: boolean) =>
|
||
value ? <span style={{ display: 'inline-flex', alignItems: 'center', gap: 4, fontSize: '0.75rem', padding: '0.125rem 0.5rem', borderRadius: 10, background: 'var(--info-color, #3182ce)', color: '#fff' }}><FaLock style={{ fontSize: '0.625rem' }} /> System</span>
|
||
: <span style={{ fontSize: '0.75rem', padding: '0.125rem 0.5rem', borderRadius: 10, background: 'var(--success-color, #38a169)', color: '#fff' }}>Instanz</span>
|
||
},
|
||
{ key: '_createdByUserName', label: 'Erstellt von', type: 'string' as const, width: 150 },
|
||
], []);
|
||
|
||
// Handle edit click - open editor with template data
|
||
const handleEditClick = async (template: AutomationTemplate) => {
|
||
// Fetch full template data
|
||
const fullTemplate = await getTemplate(template.id);
|
||
setEditingTemplate(fullTemplate || template);
|
||
setShowEditor(true);
|
||
};
|
||
|
||
// Handle create click - open editor for new template
|
||
const handleCreateClick = () => {
|
||
setEditingTemplate(null);
|
||
setShowEditor(true);
|
||
};
|
||
|
||
// Handle editor save
|
||
const handleEditorSave = async (data: Partial<AutomationTemplate>) => {
|
||
setSaving(true);
|
||
try {
|
||
if (editingTemplate) {
|
||
await updateTemplate(editingTemplate.id, data);
|
||
showSuccess('Vorlage aktualisiert');
|
||
} else {
|
||
await createTemplate(data as any);
|
||
showSuccess('Vorlage erstellt');
|
||
}
|
||
setShowEditor(false);
|
||
setEditingTemplate(null);
|
||
await refetch();
|
||
} catch (err: any) {
|
||
showError(`Fehler: ${err.message}`);
|
||
} finally {
|
||
setSaving(false);
|
||
}
|
||
};
|
||
|
||
// Handle editor cancel
|
||
const handleEditorCancel = () => {
|
||
setShowEditor(false);
|
||
setEditingTemplate(null);
|
||
};
|
||
|
||
// Handle delete by ID (used by DeleteActionButton via hookData)
|
||
const handleDelete = async (templateId: string): Promise<boolean> => {
|
||
try {
|
||
await deleteTemplate(templateId);
|
||
showSuccess('Vorlage gelöscht');
|
||
return true;
|
||
} catch (err: any) {
|
||
showError(`Fehler: ${err.message}`);
|
||
return false;
|
||
}
|
||
};
|
||
|
||
// Handle duplicate
|
||
const handleDuplicate = async (template: AutomationTemplate) => {
|
||
try {
|
||
await duplicateTemplate(template.id);
|
||
showSuccess('Vorlage dupliziert');
|
||
await refetch();
|
||
} catch (err: any) {
|
||
showError(`Fehler beim Duplizieren: ${err.message}`);
|
||
}
|
||
};
|
||
|
||
// Check if template is editable (system templates only by SysAdmin)
|
||
const _canEditTemplate = (template: AutomationTemplate) => {
|
||
if ((template as any).isSystem) return isSysAdmin;
|
||
return canUpdate;
|
||
};
|
||
|
||
const _canDeleteTemplate = (template: AutomationTemplate) => {
|
||
if ((template as any).isSystem) return isSysAdmin;
|
||
return canDelete;
|
||
};
|
||
|
||
if (error) {
|
||
return (
|
||
<div className={styles.adminPage}>
|
||
<div className={styles.errorContainer}>
|
||
<span className={styles.errorIcon}>⚠️</span>
|
||
<p className={styles.errorMessage}>Fehler beim Laden der Vorlagen: {error}</p>
|
||
<button className={styles.secondaryButton} onClick={() => refetch()}>
|
||
<FaSync /> Erneut versuchen
|
||
</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className={styles.adminPage}>
|
||
<div className={styles.pageHeader}>
|
||
<div>
|
||
<h1 className={styles.pageTitle}>Automation-Vorlagen</h1>
|
||
<p className={styles.pageSubtitle}>Verwalten Sie Ihre Workflow-Vorlagen</p>
|
||
</div>
|
||
<div className={styles.headerActions}>
|
||
<button
|
||
className={styles.secondaryButton}
|
||
onClick={() => refetch()}
|
||
disabled={loading}
|
||
>
|
||
<FaSync className={loading ? 'spinning' : ''} /> Aktualisieren
|
||
</button>
|
||
{canCreate && (
|
||
<button
|
||
className={styles.primaryButton}
|
||
onClick={handleCreateClick}
|
||
>
|
||
<FaPlus /> Neue Vorlage
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
<div className={styles.tableContainer}>
|
||
{loading && (!templates || templates.length === 0) ? (
|
||
<div className={styles.loadingContainer}>
|
||
<div className={styles.spinner} />
|
||
<span>Lade Vorlagen...</span>
|
||
</div>
|
||
) : !templates || templates.length === 0 ? (
|
||
<div className={styles.emptyState}>
|
||
<FaFileAlt className={styles.emptyIcon} />
|
||
<h3 className={styles.emptyTitle}>Keine Vorlagen vorhanden</h3>
|
||
<p className={styles.emptyDescription}>
|
||
Erstellen Sie eine neue Vorlage für Ihre Workflows.
|
||
</p>
|
||
{canCreate && (
|
||
<button
|
||
className={styles.primaryButton}
|
||
onClick={handleCreateClick}
|
||
>
|
||
<FaPlus /> Vorlage erstellen
|
||
</button>
|
||
)}
|
||
</div>
|
||
) : (
|
||
<FormGeneratorTable
|
||
data={templates as any[]}
|
||
columns={columns}
|
||
apiEndpoint="/api/automation-templates"
|
||
loading={loading}
|
||
pagination={true}
|
||
pageSize={25}
|
||
searchable={true}
|
||
filterable={true}
|
||
sortable={true}
|
||
selectable={false}
|
||
actionButtons={[
|
||
{
|
||
type: 'custom' as const,
|
||
icon: <FaCopy />,
|
||
title: 'Duplizieren',
|
||
onAction: handleDuplicate,
|
||
},
|
||
{
|
||
type: 'edit' as const,
|
||
onAction: handleEditClick,
|
||
title: 'Bearbeiten',
|
||
disabled: (row: any) => row.isSystem && !isSysAdmin
|
||
? { disabled: true, message: 'System-Vorlagen können nur vom SysAdmin bearbeitet werden' }
|
||
: !canUpdate
|
||
? { disabled: true, message: 'Keine Berechtigung' }
|
||
: false,
|
||
},
|
||
{
|
||
type: 'delete' as const,
|
||
title: 'Löschen',
|
||
disabled: (row: any) => row.isSystem && !isSysAdmin
|
||
? { disabled: true, message: 'System-Vorlagen können nur vom SysAdmin gelöscht werden' }
|
||
: !canDelete
|
||
? { disabled: true, message: 'Keine Berechtigung' }
|
||
: false,
|
||
},
|
||
]}
|
||
onDelete={(template) => handleDelete(template.id)}
|
||
hookData={{
|
||
refetch,
|
||
handleDelete,
|
||
attributes,
|
||
}}
|
||
emptyMessage="Keine Vorlagen gefunden"
|
||
/>
|
||
)}
|
||
</div>
|
||
|
||
{/* Automation Editor */}
|
||
{showEditor && (
|
||
<AutomationEditor
|
||
mode="template"
|
||
initialData={editingTemplate}
|
||
onSave={handleEditorSave}
|
||
onCancel={handleEditorCancel}
|
||
saving={saving}
|
||
/>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default AutomationTemplatesPage;
|