Files
leocrm/frontend/src/pages/Workflows.tsx
T
Agent Zero a3a5a10514 Phase 1: Workflows UI, Dedup/Merge UI, Import/Export UI, Print/PDF
- Workflows UI: full page with definitions/instances tabs, step editor, instance detail with approve/reject
- Dedup/Merge UI: duplicate detection, side-by-side comparison, field-level merge dialog, merge history
- Import/Export UI: import wizard (dry-run preview), export panel (CSV/XLSX), backend export route added
- Print/PDF: PrintButton component, print.css, integrated in Contacts/Calendar/Reports/ContactDetail
- Backend: GET /api/v1/export endpoint, export_companies_csv() service function
- Routes: /workflows, /contacts/dedup, /import-export registered
- Menu items: Workflows, Import/Export, Duplikate added to automation plugin manifest
- IMPLEMENTATION_PLAN.md: audit-corrected plan for all 14 remaining features
2026-07-26 02:35:44 +02:00

279 lines
9.6 KiB
TypeScript

import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
useWorkflows,
useDeleteWorkflow,
useUpdateWorkflow,
} from '@/api/workflows';
import type { Workflow, WorkflowInstance } from '@/api/workflows';
import { Button } from '@/components/ui/Button';
import { Card } from '@/components/ui/Card';
import { Badge } from '@/components/ui/Badge';
import { Skeleton } from '@/components/ui/Skeleton';
import { EmptyState } from '@/components/ui/EmptyState';
import { ConfirmDialog } from '@/components/ui/ConfirmDialog';
import { useToast } from '@/components/ui/Toast';
import { WorkflowEditor } from '@/components/workflows/WorkflowEditor';
import { WorkflowInstanceList } from '@/components/workflows/WorkflowInstanceList';
import { WorkflowInstanceDetail } from '@/components/workflows/WorkflowInstanceDetail';
import {
Plus,
Settings2,
Trash2,
Zap,
AlertCircle,
Workflow as WorkflowIcon,
ListOrdered,
} from 'lucide-react';
type Tab = 'definitions' | 'instances';
function activeBadgeVariant(isActive: boolean): 'success' | 'secondary' {
return isActive ? 'success' : 'secondary';
}
export function WorkflowsPage() {
const { t } = useTranslation();
const toast = useToast();
const { data, isLoading, isError, refetch } = useWorkflows(1, 50);
const deleteMutation = useDeleteWorkflow();
const updateMutation = useUpdateWorkflow();
const [activeTab, setActiveTab] = useState<Tab>('definitions');
const [showEditor, setShowEditor] = useState(false);
const [editingWorkflow, setEditingWorkflow] = useState<Workflow | null>(null);
const [confirmDelete, setConfirmDelete] = useState<Workflow | null>(null);
const [selectedInstanceId, setSelectedInstanceId] = useState<string | null>(null);
const workflows = data?.items ?? [];
const handleToggleActive = async (workflow: Workflow) => {
try {
await updateMutation.mutateAsync({
id: workflow.id,
data: { is_active: !workflow.is_active },
});
toast.success(
workflow.is_active ? 'Workflow deaktiviert' : 'Workflow aktiviert'
);
} catch (err: any) {
toast.error(err.message || 'Fehler beim Umschalten');
}
};
const handleDelete = async () => {
if (!confirmDelete) return;
try {
await deleteMutation.mutateAsync(confirmDelete.id);
toast.success('Workflow geloescht');
setConfirmDelete(null);
} catch (err: any) {
toast.error(err.message || 'Fehler beim Loeschen');
}
};
const openCreate = () => {
setEditingWorkflow(null);
setShowEditor(true);
};
const openEdit = (workflow: Workflow) => {
setEditingWorkflow(workflow);
setShowEditor(true);
};
return (
<div className="max-w-7xl mx-auto p-6" data-testid="workflows-page">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<div>
<h1 className="text-2xl font-bold text-secondary-900">
{t('workflows.title', 'Workflows')}
</h1>
<p className="text-sm text-secondary-500 mt-1">
Definieren und verwalten Sie automatisierte Workflows
</p>
</div>
{activeTab === 'definitions' && (
<Button onClick={openCreate} icon={<Plus className="h-4 w-4" />}>
Neu
</Button>
)}
</div>
{/* Tabs */}
<div className="flex items-center gap-1 mb-6 border-b border-secondary-200">
<button
onClick={() => setActiveTab('definitions')}
className={
activeTab === 'definitions'
? 'px-4 py-2 text-sm font-medium text-primary-600 border-b-2 border-primary-600 -mb-px'
: 'px-4 py-2 text-sm font-medium text-secondary-500 hover:text-secondary-700'
}
>
<span className="flex items-center gap-2">
<WorkflowIcon className="h-4 w-4" />
Definitionen
</span>
</button>
<button
onClick={() => setActiveTab('instances')}
className={
activeTab === 'instances'
? 'px-4 py-2 text-sm font-medium text-primary-600 border-b-2 border-primary-600 -mb-px'
: 'px-4 py-2 text-sm font-medium text-secondary-500 hover:text-secondary-700'
}
>
<span className="flex items-center gap-2">
<ListOrdered className="h-4 w-4" />
Instanzen
</span>
</button>
</div>
{/* Definitions Tab */}
{activeTab === 'definitions' && (
<>
{isLoading && (
<div className="space-y-4">
{[1, 2, 3].map((i) => (
<Skeleton key={i} className="h-24 w-full" />
))}
</div>
)}
{isError && (
<Card className="p-6">
<div className="flex items-center gap-3 text-danger-600">
<AlertCircle className="h-5 w-5" />
<span>Fehler beim Laden der Workflows</span>
<Button size="sm" variant="secondary" onClick={() => refetch()}>
Erneut versuchen
</Button>
</div>
</Card>
)}
{!isLoading && !isError && workflows.length === 0 && (
<EmptyState
title="Keine Workflows"
description="Erstellen Sie Ihren ersten Workflow, um automatisierte Prozesse zu definieren."
icon={<WorkflowIcon className="h-8 w-8" />}
action={
<Button onClick={openCreate} icon={<Plus className="h-4 w-4" />}>
Workflow erstellen
</Button>
}
/>
)}
{!isLoading && !isError && workflows.length > 0 && (
<div className="space-y-4">
{workflows.map((wf) => (
<Card key={wf.id} className="p-4">
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-3 mb-1">
<h3 className="text-lg font-semibold text-secondary-900">
{wf.name}
</h3>
<Badge variant={activeBadgeVariant(wf.is_active)}>
{wf.is_active ? 'Aktiv' : 'Inaktiv'}
</Badge>
{wf.trigger_event && (
<div className="flex items-center gap-1 text-xs text-secondary-400">
<Zap className="h-3 w-3" />
<span>{wf.trigger_event}</span>
</div>
)}
</div>
{wf.description && (
<p className="text-sm text-secondary-500 mb-2">
{wf.description}
</p>
)}
<div className="flex items-center gap-4 text-xs text-secondary-400">
<span>{wf.steps?.length || 0} Schritte</span>
{wf.created_at && (
<span>
Erstellt: {new Date(wf.created_at).toLocaleDateString()}
</span>
)}
</div>
</div>
<div className="flex items-center gap-2 ml-4">
<label className="flex items-center gap-2 text-sm cursor-pointer">
<input
type="checkbox"
checked={wf.is_active}
onChange={() => handleToggleActive(wf)}
className="rounded border-secondary-300 text-primary-600 focus:ring-primary-500"
/>
<span className="sr-only">Aktiv</span>
</label>
<Button
size="sm"
variant="ghost"
onClick={() => openEdit(wf)}
title="Bearbeiten"
>
<Settings2 className="h-4 w-4" />
</Button>
<Button
size="sm"
variant="ghost"
onClick={() => setConfirmDelete(wf)}
title="Loeschen"
>
<Trash2 className="h-4 w-4 text-danger-500" />
</Button>
</div>
</div>
</Card>
))}
</div>
)}
</>
)}
{/* Instances Tab */}
{activeTab === 'instances' && (
<WorkflowInstanceList
onSelectInstance={(inst: WorkflowInstance) =>
setSelectedInstanceId(inst.id)
}
/>
)}
{/* Workflow Editor Modal */}
<WorkflowEditor
open={showEditor}
workflow={editingWorkflow}
onClose={() => {
setShowEditor(false);
setEditingWorkflow(null);
}}
/>
{/* Instance Detail Modal */}
{selectedInstanceId && (
<WorkflowInstanceDetail
instanceId={selectedInstanceId}
onClose={() => setSelectedInstanceId(null)}
/>
)}
{/* Delete Confirmation */}
<ConfirmDialog
open={!!confirmDelete}
onCancel={() => setConfirmDelete(null)}
onConfirm={handleDelete}
title="Workflow loeschen"
message={`Moechten Sie den Workflow "${confirmDelete?.name}" wirklich loeschen?`}
confirmLabel="Loeschen"
variant="danger"
/>
</div>
);
}