).en ?? '';
+ };
+
+ const renderConfig = () => {
+ switch (node.type) {
+ case 'input.form': {
+ const fields = (params.fields as FormField[]) ?? [];
+ const moveField = (fromIndex: number, toIndex: number) => {
+ if (fromIndex < 0 || toIndex < 0 || fromIndex >= fields.length || toIndex >= fields.length) return;
+ const next = [...fields];
+ const [removed] = next.splice(fromIndex, 1);
+ next.splice(toIndex, 0, removed);
+ updateParam('fields', next);
+ };
+ return (
+
+
+
+ {fields.map((f, i) => (
+
{
+ e.preventDefault();
+ e.dataTransfer.dropEffect = 'move';
+ }}
+ onDrop={(e) => {
+ e.preventDefault();
+ const from = parseInt(e.dataTransfer.getData('text/plain'), 10);
+ if (!Number.isNaN(from) && from !== i) moveField(from, i);
+ }}
+ >
+
+
+
+
+
+
+ ))}
+
+
+
+ );
+ }
+ case 'input.approval':
+ return (
+ <>
+
+
+ updateParam('title', e.target.value)}
+ placeholder="Genehmigungstitel"
+ />
+
+
+
+
+ >
+ );
+ case 'input.upload':
+ return (
+ <>
+
+
+ updateParam('accept', e.target.value)}
+ placeholder=".pdf,image/*"
+ />
+
+
+
+ updateParam('maxSize', parseFloat(e.target.value) || 0)}
+ />
+
+
+
+
+ >
+ );
+ case 'input.comment':
+ return (
+ <>
+
+
+ updateParam('placeholder', e.target.value)}
+ placeholder="Kommentar eingeben..."
+ />
+
+
+
+
+ >
+ );
+ case 'input.review':
+ return (
+ <>
+
+
+ updateParam('contentRef', e.target.value)}
+ placeholder="{{nodeId.field}}"
+ />
+
+ >
+ );
+ case 'input.selection': {
+ const options = (params.options as Array<{ value?: string; label?: string }>) ?? [];
+ return (
+
+ );
+ }
+ case 'input.confirmation':
+ return (
+ <>
+
+
+ updateParam('question', e.target.value)}
+ placeholder="Möchten Sie bestätigen?"
+ />
+
+
+
+ updateParam('confirmLabel', e.target.value)}
+ />
+
+
+
+ updateParam('rejectLabel', e.target.value)}
+ />
+
+ >
+ );
+ default:
+ return Keine Konfiguration für {node.type}
;
+ }
+ };
+
+ return (
+
+
{getLabel(nt?.label) || node.type}
+ {renderConfig()}
+
+ );
+};
diff --git a/src/components/Automation2FlowEditor/index.ts b/src/components/Automation2FlowEditor/index.ts
new file mode 100644
index 0000000..961dd07
--- /dev/null
+++ b/src/components/Automation2FlowEditor/index.ts
@@ -0,0 +1 @@
+export { Automation2FlowEditor } from './Automation2FlowEditor';
diff --git a/src/config/pageRegistry.tsx b/src/config/pageRegistry.tsx
index 21a8b40..00074e6 100644
--- a/src/config/pageRegistry.tsx
+++ b/src/config/pageRegistry.tsx
@@ -111,6 +111,9 @@ export const PAGE_ICONS: Record = {
'feature.realestate': ,
'feature.chatworkflow': ,
'feature.automation': ,
+ 'feature.automation2': ,
+ 'page.feature.automation2.editor': ,
+ 'page.feature.automation2.workflows-tasks': ,
'page.feature.chatbot.conversations': ,
'feature.chatbot': ,
'feature.teamsbot': ,
diff --git a/src/pages/FeatureView.tsx b/src/pages/FeatureView.tsx
index 56deae2..db41888 100644
--- a/src/pages/FeatureView.tsx
+++ b/src/pages/FeatureView.tsx
@@ -6,6 +6,7 @@
*/
import React from 'react';
+import { Navigate, useParams } from 'react-router-dom';
import { useCurrentInstance } from '../hooks/useCurrentInstance';
import { useCanViewFeatureView } from '../hooks/useInstancePermissions';
import { useLanguage } from '../providers/language/LanguageContext';
@@ -30,6 +31,10 @@ import { RealEstatePekView, RealEstateInstanceRolesPlaceholder } from './views/r
// Automation Views
import { AutomationDefinitionsView, AutomationTemplatesView, AutomationLogsView } from './views/automation';
+// Automation2 Views
+import { Automation2Page } from './views/automation2/Automation2Page';
+import { Automation2WorkflowsTasksPage } from './views/automation2/Automation2WorkflowsTasksPage';
+
// Workspace Views
import { WorkspacePage } from './views/workspace/WorkspacePage';
import { WorkspaceEditorPage } from './views/workspace/WorkspaceEditorPage';
@@ -128,6 +133,10 @@ const VIEW_COMPONENTS: Record> = {
templates: AutomationTemplatesView,
logs: AutomationLogsView,
},
+ automation2: {
+ editor: Automation2Page,
+ 'workflows-tasks': Automation2WorkflowsTasksPage,
+ },
workspace: {
dashboard: WorkspacePage,
editor: WorkspaceEditorPage,
@@ -161,7 +170,13 @@ interface FeatureViewPageProps {
export const FeatureViewPage: React.FC = ({ view }) => {
const { instance, featureCode, isValid } = useCurrentInstance();
const { currentLanguage } = useLanguage();
-
+ const { mandateId, instanceId } = useParams<{ mandateId?: string; instanceId?: string }>();
+
+ // automation2: Dashboard entfernt → Index/Base-URL auf Editor umleiten
+ if (featureCode === 'automation2' && view === 'dashboard' && mandateId && instanceId) {
+ return ;
+ }
+
// Berechtigungs-Check
const viewCode = `${featureCode}-${view}`;
const canView = useCanViewFeatureView(viewCode);
diff --git a/src/pages/Store.tsx b/src/pages/Store.tsx
index ac68e57..fd0e467 100644
--- a/src/pages/Store.tsx
+++ b/src/pages/Store.tsx
@@ -7,7 +7,7 @@
*/
import React from 'react';
-import { FaCogs, FaComments, FaHeadset } from 'react-icons/fa';
+import { FaCogs, FaComments, FaHeadset, FaProjectDiagram } from 'react-icons/fa';
import { useLanguage } from '../providers/language/LanguageContext';
import { useStore } from '../hooks/useStore';
import type { StoreFeature } from '../api/storeApi';
@@ -15,6 +15,7 @@ import styles from './Store.module.css';
const FEATURE_ICONS: Record = {
automation: ,
+ automation2: ,
teamsbot: ,
workspace: ,
commcoach: ,
@@ -26,6 +27,11 @@ const FEATURE_DESCRIPTIONS: Record> = {
en: 'Create and manage automations to handle recurring tasks efficiently.',
fr: 'Creer et gerer des automatisations pour traiter efficacement les taches recurrentes.',
},
+ automation2: {
+ de: 'n8n-style Flow-Automatisierung mit grafischem Editor, RAG und Tools.',
+ en: 'n8n-style flow automation with visual editor, RAG and tools.',
+ fr: 'Automatisation de flux style n8n avec editeur visuel, RAG et outils.',
+ },
teamsbot: {
de: 'Integriere einen AI-Bot in deine Microsoft Teams Meetings und Channels.',
en: 'Integrate an AI bot into your Microsoft Teams meetings and channels.',
diff --git a/src/pages/views/automation2/Automation2Page.tsx b/src/pages/views/automation2/Automation2Page.tsx
new file mode 100644
index 0000000..9fe7b6b
--- /dev/null
+++ b/src/pages/views/automation2/Automation2Page.tsx
@@ -0,0 +1,31 @@
+/**
+ * Automation2Page
+ *
+ * n8n-style flow builder with backend-driven node list.
+ */
+import React from 'react';
+import { useInstanceId } from '../../../hooks/useCurrentInstance';
+import { useLanguage } from '../../../providers/language/LanguageContext';
+import { Automation2FlowEditor } from '../../../components/Automation2FlowEditor';
+import styles from '../../FeatureView.module.css';
+
+export const Automation2Page: React.FC = () => {
+ const instanceId = useInstanceId();
+ const { currentLanguage } = useLanguage();
+ const language = (currentLanguage?.slice(0, 2) || 'de') as string;
+
+ if (!instanceId) {
+ return (
+
+
Automation 2
+
Keine Feature-Instanz gefunden.
+
+ );
+ }
+
+ return (
+
+ );
+};
diff --git a/src/pages/views/automation2/Automation2WorkflowsTasks.module.css b/src/pages/views/automation2/Automation2WorkflowsTasks.module.css
new file mode 100644
index 0000000..7cd698a
--- /dev/null
+++ b/src/pages/views/automation2/Automation2WorkflowsTasks.module.css
@@ -0,0 +1,210 @@
+.container {
+ padding: 1.5rem;
+ max-width: 800px;
+}
+
+.container h2 {
+ margin: 0 0 1rem 0;
+ font-size: 1.25rem;
+}
+
+.loading {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 1rem;
+ padding: 3rem;
+ color: var(--text-secondary, #666);
+}
+
+.spinner {
+ animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+ from { transform: rotate(0deg); }
+ to { transform: rotate(360deg); }
+}
+
+.placeholder {
+ padding: 2rem;
+ text-align: center;
+ color: var(--text-secondary, #666);
+}
+
+.workflowList {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+}
+
+.workflowItem {
+ border: 1px solid var(--border-color, #e0e0e0);
+ border-radius: 8px;
+ overflow: hidden;
+ background: var(--bg-primary, #fff);
+}
+
+.workflowHeader {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ width: 100%;
+ padding: 0.75rem 1rem;
+ text-align: left;
+ background: var(--bg-secondary, #f8f9fa);
+ border: none;
+ cursor: pointer;
+ font-size: 1rem;
+}
+
+.workflowHeader:hover {
+ background: var(--bg-hover, #e9ecef);
+}
+
+.badge {
+ margin-left: auto;
+ background: var(--primary-color, #007bff);
+ color: white;
+ padding: 0.2rem 0.5rem;
+ border-radius: 12px;
+ font-size: 0.8rem;
+}
+
+.taskList {
+ padding: 1rem;
+ border-top: 1px solid var(--border-color, #e0e0e0);
+}
+
+.empty {
+ color: var(--text-tertiary, #999);
+ font-size: 0.9rem;
+ margin: 0;
+}
+
+.taskCard {
+ padding: 1rem;
+ margin-bottom: 0.75rem;
+ border: 1px solid var(--border-color, #e0e0e0);
+ border-radius: 6px;
+ background: var(--bg-primary, #fff);
+}
+
+.taskCard:last-child {
+ margin-bottom: 0;
+}
+
+.taskType {
+ font-size: 0.75rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ color: var(--text-secondary, #666);
+ margin-bottom: 0.5rem;
+}
+
+.formFields {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+}
+
+.formFields button {
+ margin-top: 0.75rem;
+ align-self: flex-start;
+}
+
+.formFields label,
+.taskCard label {
+ display: block;
+ font-size: 0.875rem;
+ margin-top: 0.5rem;
+ margin-bottom: 0.25rem;
+}
+
+.formFields input[type='text'],
+.formFields input[type='number'],
+.formFields input[type='date'],
+.taskCard input[type='text'],
+.taskCard input[type='number'],
+.taskCard textarea {
+ width: 100%;
+ padding: 0.5rem;
+ border: 1px solid var(--border-color, #e0e0e0);
+ border-radius: 4px;
+}
+
+.taskCard textarea {
+ min-height: 80px;
+ margin-bottom: 0.5rem;
+}
+
+.openFormButton {
+ margin-top: 0.5rem;
+ padding: 0.5rem 1rem;
+ background: var(--primary-color, #007bff);
+ color: white;
+ border: none;
+ border-radius: 6px;
+ font-size: 0.9rem;
+ cursor: pointer;
+}
+
+.openFormButton:hover:not(:disabled) {
+ opacity: 0.9;
+}
+
+.openFormButton:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+}
+
+.popupSubmitButton {
+ padding: 0.5rem 1.25rem;
+ background: var(--success-color, #28a745);
+ color: white;
+ border: none;
+ border-radius: 6px;
+ font-size: 0.9rem;
+ cursor: pointer;
+}
+
+.popupSubmitButton:hover:not(:disabled) {
+ opacity: 0.9;
+}
+
+.popupSubmitButton:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+}
+
+.approvalButtons {
+ display: flex;
+ gap: 0.5rem;
+ margin-top: 0.75rem;
+}
+
+.approvalButtons button,
+.taskCard button {
+ padding: 0.5rem 1rem;
+ border-radius: 6px;
+ border: none;
+ cursor: pointer;
+ font-size: 0.9rem;
+}
+
+.approvalButtons button:first-child,
+.taskCard button[type='button'] {
+ background: var(--primary-color, #007bff);
+ color: white;
+}
+
+.approvalButtons button:last-of-type:not(:first-child) {
+ background: var(--danger-color, #dc3545);
+ color: white;
+}
+
+.approvalButtons button:disabled,
+.taskCard button:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+}
diff --git a/src/pages/views/automation2/Automation2WorkflowsTasksPage.tsx b/src/pages/views/automation2/Automation2WorkflowsTasksPage.tsx
new file mode 100644
index 0000000..8bd2426
--- /dev/null
+++ b/src/pages/views/automation2/Automation2WorkflowsTasksPage.tsx
@@ -0,0 +1,361 @@
+/**
+ * Automation2WorkflowsTasksPage
+ * Workflows (collapsible) with runs and tasks. Complete tasks by type (form, approval, upload, etc.)
+ * Form tasks open in Popup for fill-in and submit.
+ */
+import React, { useState, useEffect, useCallback } from 'react';
+import { FaChevronDown, FaChevronRight, FaSpinner } from 'react-icons/fa';
+import { useInstanceId } from '../../../hooks/useCurrentInstance';
+import { useApiRequest } from '../../../hooks/useApi';
+import {
+ fetchWorkflows,
+ fetchTasks,
+ completeTask,
+ type Automation2Workflow,
+ type Automation2Task,
+} from '../../../api/automation2Api';
+import { Popup } from '../../../components/UiComponents/Popup';
+import styles from './Automation2WorkflowsTasks.module.css';
+
+export const Automation2WorkflowsTasksPage: React.FC = () => {
+ const instanceId = useInstanceId();
+ const { request } = useApiRequest();
+ const [workflows, setWorkflows] = useState([]);
+ const [tasks, setTasks] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [expandedWorkflows, setExpandedWorkflows] = useState>(new Set());
+ const [submitting, setSubmitting] = useState(null);
+
+ const load = useCallback(async () => {
+ if (!instanceId) return;
+ setLoading(true);
+ try {
+ const [wfList, taskList] = await Promise.all([
+ fetchWorkflows(request, instanceId),
+ fetchTasks(request, instanceId, { status: 'pending' }),
+ ]);
+ setWorkflows(wfList);
+ setTasks(taskList);
+ } catch (e) {
+ console.error('[Automation2] load failed', e);
+ } finally {
+ setLoading(false);
+ }
+ }, [instanceId, request]);
+
+ useEffect(() => {
+ load();
+ }, [load]);
+
+ const toggleWorkflow = (id: string) => {
+ setExpandedWorkflows((prev) => {
+ const next = new Set(prev);
+ if (next.has(id)) next.delete(id);
+ else next.add(id);
+ return next;
+ });
+ };
+
+ 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('[Automation2] complete failed', e);
+ } finally {
+ setSubmitting(null);
+ }
+ };
+
+ const tasksByWorkflow = tasks.reduce>((acc, t) => {
+ const w = t.workflowId;
+ if (!acc[w]) acc[w] = [];
+ acc[w].push(t);
+ return acc;
+ }, {});
+
+ const workflowLabel = (wf: Automation2Workflow) => wf.label || wf.id;
+
+ if (!instanceId) {
+ return (
+
+
Workflows & Tasks
+
Keine Feature-Instanz gefunden.
+
+ );
+ }
+
+ if (loading) {
+ return (
+
+
+
Lade Workflows und Tasks…
+
+ );
+ }
+
+ return (
+
+
Workflows & Tasks
+
+ {workflows.map((wf) => {
+ const isExpanded = expandedWorkflows.has(wf.id);
+ const wfTasks = tasksByWorkflow[wf.id] ?? [];
+ return (
+
+
+ {isExpanded && (
+
+ {wfTasks.length === 0 ? (
+
Keine offenen Tasks
+ ) : (
+ wfTasks.map((task) => (
+
handleComplete(task.id, result)}
+ submitting={submitting === task.id}
+ />
+ ))
+ )}
+
+ )}
+
+ );
+ })}
+
+ {workflows.length === 0 && (
+
Keine Workflows. Erstelle einen im Editor und speichere ihn.
+ )}
+
+ );
+};
+
+interface TaskCardProps {
+ task: Automation2Task;
+ onSubmit: (result: Record) => void;
+ submitting: boolean;
+}
+
+const TaskCard: React.FC = ({ task, onSubmit, submitting }) => {
+ const [formData, setFormData] = useState>({});
+ const [formPopupOpen, setFormPopupOpen] = useState(false);
+ const config = task.config ?? {};
+ const nodeType = task.nodeType;
+
+ const renderInput = () => {
+ switch (nodeType) {
+ case 'input.form': {
+ const fields = (config.fields as Array<{ name: string; type: string; label: string; required?: boolean }>) ?? [];
+ const requiredFields = fields.filter((f) => f.required);
+ const allRequiredFilled = requiredFields.every((f) => {
+ const v = formData[f.name];
+ if (f.type === 'boolean') return true;
+ return v !== undefined && v !== null && String(v).trim() !== '';
+ });
+ const formContent = (
+
+ );
+ return (
+ <>
+
+ setFormPopupOpen(false)}
+ size="medium"
+ footerContent={
+
+ }
+ >
+ {formContent}
+
+ >
+ );
+ }
+ case 'input.approval':
+ return (
+
+
{config.title}
+ {config.description &&
{config.description}
}
+
+
+
+
+
+ );
+ case 'input.comment':
+ return (
+
+
+ );
+ case 'input.selection': {
+ const options = (config.options as Array<{ value: string; label: string }>) ?? [];
+ const multiple = config.multiple as boolean;
+ return (
+
+ {options.map((o) => (
+
+ ))}
+
+
+ );
+ }
+ case 'input.confirmation':
+ return (
+
+
{(config.question as string) ?? 'Bestätigen?'}
+
+
+
+
+
+ );
+ case 'input.upload':
+ return (
+
+
Upload-Komponente – noch nicht implementiert
+
+
+ );
+ case 'input.review':
+ return (
+
+
Review – Content anzeigen + Feedback
+
+ );
+ default:
+ return (
+
+
Unbekannter Task-Typ: {nodeType}
+
+
+ );
+ }
+ };
+
+ const nodeTypeLabel: Record = {
+ 'input.form': 'Formular',
+ 'input.approval': 'Genehmigung',
+ 'input.upload': 'Upload',
+ 'input.comment': 'Kommentar',
+ 'input.review': 'Prüfung',
+ 'input.selection': 'Auswahl',
+ 'input.confirmation': 'Bestätigung',
+ };
+
+ return (
+
+
{nodeTypeLabel[nodeType] ?? nodeType}
+ {renderInput()}
+
+ );
+};
diff --git a/src/types/mandate.ts b/src/types/mandate.ts
index 9b38cb9..d8653d4 100644
--- a/src/types/mandate.ts
+++ b/src/types/mandate.ts
@@ -261,6 +261,15 @@ export const FEATURE_REGISTRY: Record = {
{ code: 'logs', label: { de: 'Protokolle', en: 'Logs' }, path: 'logs' },
]
},
+ automation2: {
+ code: 'automation2',
+ label: { de: 'Automation 2', en: 'Automation 2' },
+ icon: 'sitemap',
+ views: [
+ { code: 'editor', label: { de: 'Editor', en: 'Editor' }, path: 'editor' },
+ { code: 'workflows-tasks', label: { de: 'Workflows & Tasks', en: 'Workflows & Tasks' }, path: 'workflows-tasks' },
+ ]
+ },
neutralization: {
code: 'neutralization',
label: { de: 'Neutralisierung', en: 'Neutralization', fr: 'Neutralisation' },