a3a5a10514
- 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
279 lines
9.6 KiB
TypeScript
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>
|
|
);
|
|
}
|