+
+
+
setName(e.target.value)}
+ className="px-2 py-1 rounded border border-neutral-300 dark:border-neutral-600 bg-white dark:bg-neutral-900 text-neutral-900 dark:text-neutral-100"
+ placeholder={t('workflows.editor.namePlaceholder', 'Workflow name') as string}
+ />
+
+
+
+
+
+
+
+
+ {PALETTE.map((type) => (
+
+ ))}
+
+
+
+
+ setSelectedId(n.id)} fitView
+ >
+
+
+
+
+
+
+ {selectedNode && (
+
+
+
{(selectedNode.data as any).nodeType} · {selectedNode.id}
+
+
+
+
+ )}
+
+
+ );
+};
diff --git a/frontend/src/pages/admin/workflows/WorkflowsListPage.tsx b/frontend/src/pages/admin/workflows/WorkflowsListPage.tsx
new file mode 100644
index 00000000..c62def0a
--- /dev/null
+++ b/frontend/src/pages/admin/workflows/WorkflowsListPage.tsx
@@ -0,0 +1,135 @@
+/**
+ * Admin → Workflows list. Shows every automation flow with its trigger and
+ * enabled state, a quick enable toggle, edit (canvas) and delete. "New
+ * workflow" mints a minimal trigger→action graph and opens the editor. A
+ * pending-approvals shortcut sits in the header.
+ */
+import React from 'react';
+import { useTranslation } from 'react-i18next';
+import { useNavigate, Link } from 'react-router-dom';
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { toast } from 'react-toastify';
+import { Plus, Workflow as WorkflowIcon, Inbox, Trash2, Pencil } from 'lucide-react';
+import { Button, Card, Loading } from '../../../components/common';
+import { workflowsService, type WorkflowSummary, type WorkflowSavePayload } from '../../../services/workflows.service';
+
+const NEW_WORKFLOW: WorkflowSavePayload = {
+ name: 'New workflow',
+ trigger_type: 'invoice.sent',
+ enabled: false,
+ nodes: [
+ { node_key: 'trigger', type: 'trigger', pos_x: 240, pos_y: 40 },
+ { node_key: 'step1', type: 'action', config: { action: 'noop' }, pos_x: 240, pos_y: 200 },
+ ],
+ edges: [{ from_node: 'trigger', to_node: 'step1' }],
+};
+
+export const WorkflowsListPage: React.FC = () => {
+ const { t } = useTranslation();
+ const navigate = useNavigate();
+ const qc = useQueryClient();
+
+ const { data: workflows, isLoading } = useQuery({
+ queryKey: ['workflows'],
+ queryFn: () => workflowsService.list(),
+ });
+
+ const createMutation = useMutation({
+ mutationFn: () => workflowsService.create(NEW_WORKFLOW),
+ onSuccess: (res) => {
+ qc.invalidateQueries({ queryKey: ['workflows'] });
+ navigate(`/admin/workflows/${res.id}`);
+ },
+ onError: (err: any) => toast.error(err?.response?.data?.error || (t('workflows.toast.createFailed', 'Could not create workflow') as string)),
+ });
+
+ const toggleMutation = useMutation({
+ mutationFn: ({ id, enabled }: { id: number; enabled: boolean }) => workflowsService.setEnabled(id, enabled),
+ onSuccess: () => qc.invalidateQueries({ queryKey: ['workflows'] }),
+ onError: (err: any) => toast.error(err?.response?.data?.error || (t('common.error', 'Something went wrong') as string)),
+ });
+
+ const deleteMutation = useMutation({
+ mutationFn: (id: number) => workflowsService.remove(id),
+ onSuccess: () => {
+ qc.invalidateQueries({ queryKey: ['workflows'] });
+ toast.success(t('workflows.toast.deleted', 'Workflow deleted') as string);
+ },
+ onError: (err: any) => toast.error(err?.response?.data?.error || (t('workflows.toast.deleteFailed', 'Could not delete workflow') as string)),
+ });
+
+ const isEnabled = (w: WorkflowSummary) => w.enabled === true || w.enabled === 1;
+ const isBuiltin = (w: WorkflowSummary) => w.is_builtin === true || w.is_builtin === 1;
+
+ return (
+