automation template management and fix admin center

This commit is contained in:
ValueOn AG 2026-02-03 21:29:53 +01:00
parent 1863ca9fd0
commit 8d86c166d0
12 changed files with 1128 additions and 41 deletions

View file

@ -44,7 +44,7 @@ import { FeatureViewPage } from './pages/FeatureView';
import { AccessManagementHub, AdminMandatesPage, AdminUsersPage, AdminUserMandatesPage, AdminFeatureAccessPage, AdminInvitationsPage, AdminMandateRolesPage, AdminFeatureRolesPage, AdminFeatureInstanceUsersPage, AdminMandateRolePermissionsPage, AdminUserAccessOverviewPage } from './pages/admin';
// Workflow Pages (global)
import { PlaygroundPage, WorkflowsPage, AutomationsPage } from './pages/workflows';
import { PlaygroundPage, WorkflowsPage, AutomationsPage, AutomationTemplatesPage } from './pages/workflows';
// Basedata Pages (global)
import { PromptsPage, FilesPage, ConnectionsPage } from './pages/basedata';
@ -114,6 +114,7 @@ function App() {
<Route path="playground" element={<PlaygroundPage />} />
<Route path="list" element={<WorkflowsPage />} />
<Route path="automations" element={<AutomationsPage />} />
<Route path="automation-templates" element={<AutomationTemplatesPage />} />
</Route>
{/* ============================================== */}

View file

@ -32,17 +32,49 @@ export interface AutomationLog {
messages?: string[];
}
// Multilingual text type (matches backend TextMultilingual)
export interface TextMultilingual {
en: string;
ge?: string;
fr?: string;
it?: string;
}
// AutomationTemplate from DB
export interface AutomationTemplate {
template: {
overview?: string;
tasks?: Array<{
description?: string;
objective?: string;
[key: string]: any;
}>;
[key: string]: any;
id: string;
label: TextMultilingual;
overview?: TextMultilingual;
template: string; // JSON string with {{KEY:...}} placeholders
_createdAt?: number;
_createdBy?: string;
_createdByUserName?: string;
}
// Workflow action definition from backend
export interface WorkflowAction {
method: string;
action: string;
actionId: string;
description: string;
category?: string;
parameters: WorkflowActionParameter[];
exampleJson: {
execMethod: string;
execAction: string;
execParameters: Record<string, any>;
execResultLabel: string;
};
parameters?: Record<string, any>;
}
export interface WorkflowActionParameter {
name: string;
type: string;
frontendType: string;
required: boolean;
default?: any;
description: string;
frontendOptions?: string | string[];
}
export interface CreateAutomationRequest {
@ -188,34 +220,6 @@ export async function executeAutomationApi(
});
}
/**
* Fetch automation templates
* Endpoint: GET /api/automations/templates
*/
export async function fetchAutomationTemplates(
request: ApiRequestFunction
): Promise<AutomationTemplate[]> {
const data = await request({
url: '/api/automations/templates',
method: 'get'
});
if (Array.isArray(data)) {
return data;
}
if (data && typeof data === 'object') {
if (Array.isArray(data.sets)) {
return data.sets;
}
if (Array.isArray(data.templates)) {
return data.templates;
}
}
return [];
}
/**
* Fetch automation attributes for dynamic form generation
* Endpoint: GET /api/attributes/AutomationDefinition
@ -238,3 +242,133 @@ export async function fetchAutomationAttributes(
return [];
}
// ============================================================================
// AUTOMATION TEMPLATES API
// ============================================================================
/**
* Fetch all automation templates (RBAC-filtered: own templates)
* Endpoint: GET /api/automation-templates
*/
export async function fetchAutomationTemplates(
request: ApiRequestFunction
): Promise<AutomationTemplate[]> {
const data = await request({
url: '/api/automation-templates',
method: 'get'
});
if (data?.items && Array.isArray(data.items)) {
return data.items;
}
return Array.isArray(data) ? data : [];
}
/**
* Fetch single automation template by ID
* Endpoint: GET /api/automation-templates/{templateId}
*/
export async function fetchAutomationTemplateById(
request: ApiRequestFunction,
templateId: string
): Promise<AutomationTemplate | null> {
try {
return await request({
url: `/api/automation-templates/${templateId}`,
method: 'get'
});
} catch (error) {
console.error('Error fetching template:', error);
return null;
}
}
/**
* Create new automation template
* Endpoint: POST /api/automation-templates
*/
export async function createAutomationTemplateApi(
request: ApiRequestFunction,
templateData: Omit<AutomationTemplate, 'id' | '_createdAt' | '_createdBy'>
): Promise<AutomationTemplate> {
return await request({
url: '/api/automation-templates',
method: 'post',
data: templateData
});
}
/**
* Update automation template
* Endpoint: PUT /api/automation-templates/{templateId}
*/
export async function updateAutomationTemplateApi(
request: ApiRequestFunction,
templateId: string,
templateData: Partial<AutomationTemplate>
): Promise<AutomationTemplate> {
return await request({
url: `/api/automation-templates/${templateId}`,
method: 'put',
data: templateData
});
}
/**
* Delete automation template
* Endpoint: DELETE /api/automation-templates/{templateId}
*/
export async function deleteAutomationTemplateApi(
request: ApiRequestFunction,
templateId: string
): Promise<void> {
await request({
url: `/api/automation-templates/${templateId}`,
method: 'delete'
});
}
/**
* Fetch automation template attributes for dynamic form generation
* Endpoint: GET /api/automation-templates/attributes
*/
export async function fetchAutomationTemplateAttributes(
request: ApiRequestFunction
): Promise<any[]> {
const data = await request({
url: '/api/automation-templates/attributes',
method: 'get'
});
// Backend returns: { attributes: { model: "...", attributes: [...] } }
if (data?.attributes?.attributes && Array.isArray(data.attributes.attributes)) {
return data.attributes.attributes;
}
// Fallback: direct attributes array
if (data?.attributes && Array.isArray(data.attributes)) {
return data.attributes;
}
return Array.isArray(data) ? data : [];
}
// ============================================================================
// WORKFLOW ACTIONS API
// ============================================================================
/**
* Fetch available workflow actions (RBAC-filtered)
* Endpoint: GET /api/automations/actions
*/
export async function fetchWorkflowActions(
request: ApiRequestFunction
): Promise<WorkflowAction[]> {
const data = await request({
url: '/api/automations/actions',
method: 'get'
});
return data?.actions || [];
}

View file

@ -0,0 +1,289 @@
/* ActionsPanel Styles */
.panel {
display: flex;
flex-direction: column;
height: 100%;
background: var(--bg-secondary, #f5f5f5);
border-radius: 8px;
overflow: hidden;
}
.header {
padding: 1rem;
border-bottom: 1px solid var(--border-color, #e0e0e0);
background: var(--bg-primary, #ffffff);
}
.title {
margin: 0 0 0.75rem 0;
font-size: 1rem;
font-weight: 600;
color: var(--text-primary, #333);
}
.searchBox {
display: flex;
align-items: center;
background: var(--bg-secondary, #f5f5f5);
border-radius: 6px;
padding: 0.5rem 0.75rem;
}
.searchIcon {
color: var(--text-secondary, #666);
margin-right: 0.5rem;
font-size: 0.875rem;
}
.searchInput {
flex: 1;
border: none;
background: transparent;
font-size: 0.875rem;
color: var(--text-primary, #333);
outline: none;
}
.searchInput::placeholder {
color: var(--text-tertiary, #999);
}
.actionsList {
flex: 1;
overflow-y: auto;
padding: 0.5rem;
}
.loading,
.error,
.empty {
padding: 2rem;
text-align: center;
color: var(--text-secondary, #666);
}
.error {
color: var(--error-color, #dc3545);
}
.retryButton {
margin-top: 1rem;
padding: 0.5rem 1rem;
background: var(--primary-color, #007bff);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.retryButton:hover {
background: var(--primary-hover, #0056b3);
}
/* Method Groups */
.methodGroup {
margin-bottom: 0.5rem;
background: var(--bg-primary, #ffffff);
border-radius: 6px;
overflow: hidden;
}
.methodHeader {
display: flex;
align-items: center;
width: 100%;
padding: 0.75rem 1rem;
background: transparent;
border: none;
cursor: pointer;
text-align: left;
font-size: 0.875rem;
font-weight: 600;
color: var(--text-primary, #333);
transition: background 0.2s;
}
.methodHeader:hover {
background: var(--bg-hover, #f0f0f0);
}
.methodHeader svg {
margin-right: 0.5rem;
font-size: 0.75rem;
color: var(--text-secondary, #666);
}
.methodName {
flex: 1;
text-transform: capitalize;
}
.methodCount {
background: var(--primary-color, #007bff);
color: white;
padding: 0.125rem 0.5rem;
border-radius: 10px;
font-size: 0.75rem;
font-weight: 500;
}
/* Method Actions */
.methodActions {
border-top: 1px solid var(--border-color, #e0e0e0);
}
.actionItem {
border-bottom: 1px solid var(--border-light, #f0f0f0);
}
.actionItem:last-child {
border-bottom: none;
}
.actionHeader {
display: flex;
align-items: center;
padding: 0.75rem 1rem;
cursor: pointer;
transition: background 0.2s;
}
.actionHeader:hover {
background: var(--bg-hover, #f5f5f5);
}
.actionInfo {
flex: 1;
min-width: 0;
}
.actionName {
display: block;
font-weight: 500;
font-size: 0.875rem;
color: var(--text-primary, #333);
}
.actionDesc {
display: block;
font-size: 0.75rem;
color: var(--text-secondary, #666);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.copyButton {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
background: var(--bg-secondary, #f5f5f5);
border: 1px solid var(--border-color, #e0e0e0);
border-radius: 4px;
cursor: pointer;
color: var(--text-secondary, #666);
transition: all 0.2s;
}
.copyButton:hover {
background: var(--primary-color, #007bff);
border-color: var(--primary-color, #007bff);
color: white;
}
/* Action Details */
.actionDetails {
padding: 0.75rem 1rem;
background: var(--bg-secondary, #f8f9fa);
border-top: 1px solid var(--border-light, #f0f0f0);
}
.actionDetails h5 {
margin: 0 0 0.5rem 0;
font-size: 0.75rem;
font-weight: 600;
color: var(--text-secondary, #666);
text-transform: uppercase;
}
/* Parameters */
.parameters {
margin-bottom: 1rem;
}
.parameters ul {
margin: 0;
padding: 0;
list-style: none;
}
.param {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 0.5rem;
padding: 0.25rem 0;
font-size: 0.8125rem;
}
.paramName {
font-weight: 500;
color: var(--text-primary, #333);
}
.required {
color: var(--error-color, #dc3545);
margin-left: 2px;
}
.paramType {
font-family: monospace;
font-size: 0.75rem;
background: var(--bg-code, #e9ecef);
padding: 0.125rem 0.375rem;
border-radius: 3px;
color: var(--text-secondary, #666);
}
.paramDesc {
width: 100%;
font-size: 0.75rem;
color: var(--text-tertiary, #888);
}
/* Example JSON */
.exampleJson {
margin-bottom: 1rem;
}
.exampleJson pre {
margin: 0;
padding: 0.75rem;
background: var(--bg-code, #1e1e1e);
color: var(--text-code, #d4d4d4);
border-radius: 4px;
font-size: 0.75rem;
overflow-x: auto;
white-space: pre-wrap;
word-break: break-all;
}
.insertButton {
width: 100%;
padding: 0.5rem 1rem;
background: var(--primary-color, #007bff);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.875rem;
font-weight: 500;
transition: background 0.2s;
}
.insertButton:hover {
background: var(--primary-hover, #0056b3);
}

View file

@ -0,0 +1,216 @@
/**
* ActionsPanel
*
* Displays available workflow actions for copy/paste into templates.
* Groups actions by method and shows parameters + example JSON.
*/
import React, { useState, useMemo, useEffect } from 'react';
import { useWorkflowActions, type WorkflowAction } from '../../hooks/useAutomations';
import { FaSearch, FaCopy, FaChevronDown, FaChevronRight, FaCheck } from 'react-icons/fa';
import { useToast } from '../../contexts/ToastContext';
import styles from './ActionsPanel.module.css';
interface ActionsPanelProps {
/** Callback when action JSON is inserted (optional) */
onInsert?: (actionJson: string) => void;
/** Callback when action JSON is copied (optional) */
onCopy?: (actionJson: string) => void;
}
export const ActionsPanel: React.FC<ActionsPanelProps> = ({ onInsert, onCopy }) => {
const { actions, loading, error, fetchActions } = useWorkflowActions();
const { showSuccess } = useToast();
const [filter, setFilter] = useState('');
const [expandedMethods, setExpandedMethods] = useState<Set<string>>(new Set());
const [expandedAction, setExpandedAction] = useState<string | null>(null);
const [copiedAction, setCopiedAction] = useState<string | null>(null);
useEffect(() => {
fetchActions();
}, [fetchActions]);
// Filter actions by search term
const filteredActions = useMemo(() => {
if (!filter) return actions;
const lower = filter.toLowerCase();
return actions.filter(a =>
a.method.toLowerCase().includes(lower) ||
a.action.toLowerCase().includes(lower) ||
a.description.toLowerCase().includes(lower) ||
a.actionId.toLowerCase().includes(lower)
);
}, [actions, filter]);
// Group actions by method
const groupedActions = useMemo(() => {
const groups: Record<string, WorkflowAction[]> = {};
filteredActions.forEach(action => {
if (!groups[action.method]) {
groups[action.method] = [];
}
groups[action.method].push(action);
});
return groups;
}, [filteredActions]);
// Toggle method expansion
const toggleMethod = (method: string) => {
setExpandedMethods(prev => {
const newSet = new Set(prev);
if (newSet.has(method)) {
newSet.delete(method);
} else {
newSet.add(method);
}
return newSet;
});
};
// Toggle action details
const toggleAction = (actionId: string) => {
setExpandedAction(prev => prev === actionId ? null : actionId);
};
// Copy action JSON to clipboard
const handleCopy = async (action: WorkflowAction) => {
const json = JSON.stringify(action.exampleJson, null, 2);
try {
await navigator.clipboard.writeText(json);
setCopiedAction(action.actionId);
setTimeout(() => setCopiedAction(null), 2000);
showSuccess('JSON kopiert');
onCopy?.(json);
} catch (err) {
console.error('Failed to copy:', err);
}
};
// Insert action JSON
const handleInsert = (action: WorkflowAction) => {
const json = JSON.stringify(action.exampleJson, null, 2);
onInsert?.(json);
};
if (loading) {
return (
<div className={styles.panel}>
<div className={styles.loading}>Lade Actions...</div>
</div>
);
}
if (error) {
return (
<div className={styles.panel}>
<div className={styles.error}>Fehler: {error}</div>
<button className={styles.retryButton} onClick={() => fetchActions()}>
Erneut versuchen
</button>
</div>
);
}
return (
<div className={styles.panel}>
<div className={styles.header}>
<h3 className={styles.title}>Verfügbare Actions</h3>
<div className={styles.searchBox}>
<FaSearch className={styles.searchIcon} />
<input
type="text"
placeholder="Suchen..."
value={filter}
onChange={(e) => setFilter(e.target.value)}
className={styles.searchInput}
/>
</div>
</div>
<div className={styles.actionsList}>
{Object.keys(groupedActions).length === 0 ? (
<div className={styles.empty}>Keine Actions gefunden</div>
) : (
Object.entries(groupedActions).map(([method, methodActions]) => (
<div key={method} className={styles.methodGroup}>
<button
className={styles.methodHeader}
onClick={() => toggleMethod(method)}
>
{expandedMethods.has(method) ? <FaChevronDown /> : <FaChevronRight />}
<span className={styles.methodName}>{method}</span>
<span className={styles.methodCount}>{methodActions.length}</span>
</button>
{expandedMethods.has(method) && (
<div className={styles.methodActions}>
{methodActions.map(action => (
<div key={action.actionId} className={styles.actionItem}>
<div
className={styles.actionHeader}
onClick={() => toggleAction(action.actionId)}
>
<div className={styles.actionInfo}>
<span className={styles.actionName}>{action.action}</span>
<span className={styles.actionDesc}>{action.description}</span>
</div>
<button
className={styles.copyButton}
onClick={(e) => { e.stopPropagation(); handleCopy(action); }}
title="JSON kopieren"
>
{copiedAction === action.actionId ? <FaCheck /> : <FaCopy />}
</button>
</div>
{expandedAction === action.actionId && (
<div className={styles.actionDetails}>
{action.parameters.length > 0 && (
<div className={styles.parameters}>
<h5>Parameter:</h5>
<ul>
{action.parameters.map(param => (
<li key={param.name} className={styles.param}>
<span className={styles.paramName}>
{param.name}
{param.required && <span className={styles.required}>*</span>}
</span>
<span className={styles.paramType}>{param.type}</span>
{param.description && (
<span className={styles.paramDesc}>{param.description}</span>
)}
</li>
))}
</ul>
</div>
)}
<div className={styles.exampleJson}>
<h5>Beispiel JSON:</h5>
<pre>{JSON.stringify(action.exampleJson, null, 2)}</pre>
</div>
{onInsert && (
<button
className={styles.insertButton}
onClick={() => handleInsert(action)}
>
In Template einfügen
</button>
)}
</div>
)}
</div>
))}
</div>
)}
</div>
))
)}
</div>
</div>
);
};
export default ActionsPanel;

View file

@ -0,0 +1,2 @@
export { ActionsPanel } from './ActionsPanel';
export { default } from './ActionsPanel';

View file

@ -39,6 +39,7 @@ export const PAGE_ICONS: Record<string, React.ReactNode> = {
'page.system.playground': <FaPlay />,
'page.system.chats': <FaListAlt />,
'page.system.automations': <FaCogs />,
'page.system.automation-templates': <FaFileAlt />,
'page.system.prompts': <FaLightbulb />,
'page.system.files': <FaRegFileAlt />,
'page.system.connections': <FaLink />,

View file

@ -10,14 +10,29 @@ import {
deleteAutomationApi,
executeAutomationApi,
fetchAutomationTemplates as fetchTemplatesApi,
fetchAutomationTemplateById,
createAutomationTemplateApi,
updateAutomationTemplateApi,
deleteAutomationTemplateApi,
fetchAutomationTemplateAttributes,
fetchWorkflowActions as fetchWorkflowActionsApi,
type Automation,
type AutomationTemplate,
type TextMultilingual,
type WorkflowAction,
type CreateAutomationRequest,
type UpdateAutomationRequest
} from '../api/automationApi';
// Re-export types
export type { Automation, AutomationTemplate, CreateAutomationRequest, UpdateAutomationRequest };
export type {
Automation,
AutomationTemplate,
TextMultilingual,
WorkflowAction,
CreateAutomationRequest,
UpdateAutomationRequest
};
// Attribute definition interface
export interface AttributeDefinition {
@ -446,3 +461,143 @@ export function useAutomationOperations() {
updateError
};
}
// ============================================================================
// AUTOMATION TEMPLATES (DB) HOOK
// ============================================================================
/**
* Hook for managing AutomationTemplates from database
*/
export function useAutomationTemplates() {
const [templates, setTemplates] = useState<AutomationTemplate[]>([]);
const [attributes, setAttributes] = useState<AttributeDefinition[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const { request } = useApiRequest();
const { checkPermission } = usePermissions();
const [permissions, setPermissions] = useState<UserPermissions | null>(null);
const fetchTemplates = useCallback(async () => {
setLoading(true);
setError(null);
try {
const data = await fetchTemplatesApi(request);
setTemplates(data);
} catch (e: any) {
console.error('Error fetching templates:', e);
setError(e.message || 'Failed to fetch templates');
setTemplates([]);
} finally {
setLoading(false);
}
}, [request]);
const fetchAttributes = useCallback(async () => {
try {
const attrs = await fetchAutomationTemplateAttributes(request);
setAttributes(attrs);
return attrs;
} catch (e: any) {
console.error('Error fetching template attributes:', e);
setAttributes([]);
return [];
}
}, [request]);
const fetchPermissions = useCallback(async () => {
try {
const perms = await checkPermission('DATA', 'AutomationTemplate');
setPermissions(perms);
return perms;
} catch (e: any) {
console.error('Error fetching template permissions:', e);
const defaultPerms: UserPermissions = {
view: false,
read: 'n',
create: 'n',
update: 'n',
delete: 'n',
};
setPermissions(defaultPerms);
return defaultPerms;
}
}, [checkPermission]);
const getTemplate = useCallback(async (templateId: string) => {
return await fetchAutomationTemplateById(request, templateId);
}, [request]);
const createTemplate = useCallback(async (data: Omit<AutomationTemplate, 'id' | '_createdAt' | '_createdBy'>) => {
return await createAutomationTemplateApi(request, data);
}, [request]);
const updateTemplate = useCallback(async (templateId: string, data: Partial<AutomationTemplate>) => {
return await updateAutomationTemplateApi(request, templateId, data);
}, [request]);
const deleteTemplate = useCallback(async (templateId: string) => {
await deleteAutomationTemplateApi(request, templateId);
}, [request]);
const refetch = useCallback(async () => {
await Promise.all([
fetchTemplates(),
fetchAttributes(),
fetchPermissions()
]);
}, [fetchTemplates, fetchAttributes, fetchPermissions]);
return {
templates,
data: templates, // Alias for FormGenerator compatibility
attributes,
loading,
error,
permissions,
refetch,
fetchTemplates,
fetchAttributes,
fetchPermissions,
getTemplate,
createTemplate,
updateTemplate,
deleteTemplate,
};
}
// ============================================================================
// WORKFLOW ACTIONS HOOK
// ============================================================================
/**
* Hook for fetching available workflow actions (for Actions panel)
*/
export function useWorkflowActions() {
const [actions, setActions] = useState<WorkflowAction[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const { request } = useApiRequest();
const fetchActions = useCallback(async () => {
setLoading(true);
setError(null);
try {
const data = await fetchWorkflowActionsApi(request);
setActions(data);
} catch (e: any) {
console.error('Error fetching workflow actions:', e);
setError(e.message || 'Failed to fetch actions');
setActions([]);
} finally {
setLoading(false);
}
}, [request]);
return {
actions,
loading,
error,
fetchActions
};
}

View file

@ -398,6 +398,9 @@ export const AccessManagementHub: React.FC = () => {
<Link to="/admin/mandates" className={hubStyles.mandatesLink}>
<FaBuilding /> Mandanten verwalten
</Link>
<Link to="/admin/user-mandates" className={hubStyles.mandatesLink}>
<FaUsers /> Mandant-Benutzer
</Link>
</div>
{viewMode === 'hierarchy' ? (

View file

@ -5,13 +5,15 @@
*/
import React, { useState, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAdminMandates, type Mandate } from '../../hooks/useMandates';
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
import { FormGeneratorForm, type AttributeDefinition } from '../../components/FormGenerator/FormGeneratorForm';
import { FaPlus, FaSync, FaBuilding } from 'react-icons/fa';
import { FaPlus, FaSync, FaBuilding, FaUsers } from 'react-icons/fa';
import styles from './Admin.module.css';
export const AdminMandatesPage: React.FC = () => {
const navigate = useNavigate();
const {
mandates,
attributes,
@ -100,6 +102,13 @@ export const AdminMandatesPage: React.FC = () => {
<p className={styles.pageSubtitle}>Verwalten Sie alle Mandanten im System</p>
</div>
<div className={styles.headerActions}>
<button
type="button"
className={styles.secondaryButton}
onClick={() => navigate('/admin/user-mandates')}
>
<FaUsers /> Benutzer-Zuweisungen
</button>
<button
className={styles.secondaryButton}
onClick={() => refetch()}

View file

@ -9,7 +9,7 @@ import { useNavigate } from 'react-router-dom';
import { useOrgUsers, useUserOperations } from '../../hooks/useUsers';
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
import { FormGeneratorForm } from '../../components/FormGenerator/FormGeneratorForm';
import { FaPlus, FaSync, FaUsers, FaKey, FaEnvelopeOpenText } from 'react-icons/fa';
import { FaPlus, FaSync, FaUsers, FaKey, FaEnvelopeOpenText, FaUserShield } from 'react-icons/fa';
import styles from './Admin.module.css';
interface User {
@ -145,6 +145,13 @@ export const AdminUsersPage: React.FC = () => {
<p className={styles.pageSubtitle}>Verwalten Sie alle Benutzer im System</p>
</div>
<div className={styles.headerActions}>
<button
type="button"
className={styles.secondaryButton}
onClick={() => navigate('/admin/user-access-overview')}
>
<FaUserShield /> Zugriffsübersicht
</button>
<button
type="button"
className={styles.secondaryButton}

View file

@ -0,0 +1,269 @@
/**
* AutomationTemplatesPage
*
* Page for managing automation templates (CRUD).
* Uses FormGeneratorTable for listing and FormGeneratorForm for editing.
*/
import React, { useState, useMemo, useEffect } from 'react';
import { useAutomationTemplates, type AutomationTemplate } from '../../hooks/useAutomations';
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
import { FormGeneratorForm } from '../../components/FormGenerator/FormGeneratorForm';
import { FaSync, FaPlus, FaTimes, FaFileAlt } from 'react-icons/fa';
import { useToast } from '../../contexts/ToastContext';
import styles from '../admin/Admin.module.css';
export const AutomationTemplatesPage: React.FC = () => {
const {
templates,
attributes,
loading,
error,
permissions,
refetch,
createTemplate,
updateTemplate,
deleteTemplate,
getTemplate,
} = useAutomationTemplates();
const { showSuccess, showError } = useToast();
// Modal states
const [showCreateModal, setShowCreateModal] = useState(false);
const [editingTemplate, setEditingTemplate] = useState<AutomationTemplate | null>(null);
// 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: '_createdByUserName', label: 'Erstellt von', type: 'string' as const, width: 150 },
], []);
// Form attributes for create/edit
const formAttributes = useMemo(() => {
// Filter to editable fields
const editableFields = ['label', 'overview', 'template'];
return (attributes || []).filter(attr => editableFields.includes(attr.name));
}, [attributes]);
// Handle edit click
const handleEditClick = async (template: AutomationTemplate) => {
// Fetch full template data
const fullTemplate = await getTemplate(template.id);
if (fullTemplate) {
setEditingTemplate(fullTemplate);
} else {
setEditingTemplate(template);
}
};
// Handle create submit
const handleCreateSubmit = async (data: Partial<AutomationTemplate>) => {
try {
await createTemplate(data as any);
setShowCreateModal(false);
showSuccess('Vorlage erstellt');
await refetch();
} catch (err: any) {
showError(`Fehler: ${err.message}`);
}
};
// Handle edit submit
const handleEditSubmit = async (data: Partial<AutomationTemplate>) => {
if (!editingTemplate) return;
try {
await updateTemplate(editingTemplate.id, data);
setEditingTemplate(null);
showSuccess('Vorlage aktualisiert');
await refetch();
} catch (err: any) {
showError(`Fehler: ${err.message}`);
}
};
// 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;
}
};
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={() => setShowCreateModal(true)}
>
<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={() => setShowCreateModal(true)}
>
<FaPlus /> Vorlage erstellen
</button>
)}
</div>
) : (
<FormGeneratorTable
data={templates as any[]}
columns={columns}
loading={loading}
pagination={true}
pageSize={25}
searchable={true}
filterable={true}
sortable={true}
selectable={false}
actionButtons={[
...(canUpdate ? [{
type: 'edit' as const,
onAction: handleEditClick,
title: 'Bearbeiten',
}] : []),
...(canDelete ? [{
type: 'delete' as const,
title: 'Löschen',
}] : []),
]}
onDelete={(template) => handleDelete(template.id)}
hookData={{
refetch,
handleDelete,
attributes,
}}
emptyMessage="Keine Vorlagen gefunden"
/>
)}
</div>
{/* Create Modal */}
{showCreateModal && (
<div className={styles.modalOverlay} onClick={() => setShowCreateModal(false)}>
<div className={styles.modal} onClick={e => e.stopPropagation()}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>Neue Vorlage</h2>
<button className={styles.modalClose} onClick={() => setShowCreateModal(false)}>
<FaTimes />
</button>
</div>
<div className={styles.modalContent}>
{formAttributes.length === 0 ? (
<div className={styles.loadingContainer}>
<div className={styles.spinner} />
<span>Lade Formular...</span>
</div>
) : (
<FormGeneratorForm
attributes={formAttributes}
mode="create"
onSubmit={handleCreateSubmit}
onCancel={() => setShowCreateModal(false)}
submitButtonText="Erstellen"
cancelButtonText="Abbrechen"
/>
)}
</div>
</div>
</div>
)}
{/* Edit Modal */}
{editingTemplate && (
<div className={styles.modalOverlay} onClick={() => setEditingTemplate(null)}>
<div className={styles.modal} onClick={e => e.stopPropagation()}>
<div className={styles.modalHeader}>
<h2 className={styles.modalTitle}>Vorlage bearbeiten</h2>
<button className={styles.modalClose} onClick={() => setEditingTemplate(null)}>
<FaTimes />
</button>
</div>
<div className={styles.modalContent}>
{formAttributes.length === 0 ? (
<div className={styles.loadingContainer}>
<div className={styles.spinner} />
<span>Lade Formular...</span>
</div>
) : (
<FormGeneratorForm
attributes={formAttributes}
data={editingTemplate}
mode="edit"
onSubmit={handleEditSubmit}
onCancel={() => setEditingTemplate(null)}
submitButtonText="Speichern"
cancelButtonText="Abbrechen"
/>
)}
</div>
</div>
</div>
)}
</div>
);
};
export default AutomationTemplatesPage;

View file

@ -1,3 +1,4 @@
export { PlaygroundPage } from './PlaygroundPage';
export { WorkflowsPage } from './WorkflowsPage';
export { AutomationsPage } from './AutomationsPage';
export { AutomationTemplatesPage } from './AutomationTemplatesPage';