import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useAutomations, useCreateAutomation, useUpdateAutomation, useDeleteAutomation, useExecuteAutomation, useDryRunAutomation, useAutomationRuns, useAutomationVersions, useRestoreAutomationVersion, } from '@/api/automation'; import { Button } from '@/components/ui/Button'; import { Card } from '@/components/ui/Card'; import { Badge } from '@/components/ui/Badge'; import { Modal } from '@/components/ui/Modal'; import { Select } from '@/components/ui/Select'; import { EmptyState } from '@/components/ui/EmptyState'; import { Skeleton } from '@/components/ui/Skeleton'; import { ConfirmDialog } from '@/components/ui/ConfirmDialog'; import { useToast } from '@/components/ui/Toast'; import type { AutomationDefinition, AutomationCondition, AutomationAction } from '@/types/automation'; import { Plus, Play, RotateCcw, History, GitBranch, Trash2, Settings2, Workflow, Clock, Zap, AlertCircle, CheckCircle2, XCircle, Loader2, } from 'lucide-react'; const triggerTypeOptions = [ { value: 'event', label: 'Event' }, { value: 'schedule', label: 'Schedule' }, { value: 'manual', label: 'Manual' }, ]; const actionTypeOptions = [ { value: 'api_call', label: 'API Call' }, { value: 'notification', label: 'Notification' }, { value: 'workflow_start', label: 'Workflow Start' }, ]; const conditionOperatorOptions = [ { value: 'equals', label: 'Equals' }, { value: 'not_equals', label: 'Not Equals' }, { value: 'contains', label: 'Contains' }, { value: 'greater_than', label: 'Greater Than' }, { value: 'less_than', label: 'Less Than' }, ]; function statusBadgeVariant(status: string): 'success' | 'warning' | 'danger' | 'secondary' { switch (status) { case 'active': return 'success'; case 'inactive': return 'warning'; default: return 'secondary'; } } function runStatusBadgeVariant(status: string): 'success' | 'warning' | 'danger' | 'info' { switch (status) { case 'completed': return 'success'; case 'running': return 'info'; case 'failed': return 'danger'; case 'cancelled': return 'warning'; default: return 'warning'; } } function triggerIcon(type: string) { switch (type) { case 'event': return ; case 'schedule': return ; default: return ; } } interface AutomationFormData { name: string; description: string; trigger_type: 'event' | 'schedule' | 'manual'; trigger_config: Record; conditions: AutomationCondition[]; actions: AutomationAction[]; active: boolean; dry_run: boolean; } const emptyForm: AutomationFormData = { name: '', description: '', trigger_type: 'manual', trigger_config: {}, conditions: [], actions: [], active: true, dry_run: false, }; function AutomationForm({ initial, onSave, onCancel, isSaving, }: { initial?: AutomationDefinition; onSave: (data: Partial) => void; onCancel: () => void; isSaving: boolean; }) { const { t } = useTranslation(); const [form, setForm] = useState(() => { if (initial) { return { name: initial.name, description: initial.description || '', trigger_type: initial.trigger_type, trigger_config: (initial.trigger_config as Record) || {}, conditions: initial.conditions || [], actions: initial.actions || [], active: initial.active, dry_run: initial.dry_run, }; } return { ...emptyForm }; }); const updateField = (key: K, value: AutomationFormData[K]) => { setForm((prev) => ({ ...prev, [key]: value })); }; const addCondition = () => { setForm((prev) => ({ ...prev, conditions: [...prev.conditions, { field: '', operator: 'equals', value: '' }], })); }; const updateCondition = (index: number, field: keyof AutomationCondition, value: string) => { setForm((prev) => { const conditions = [...prev.conditions]; conditions[index] = { ...conditions[index], [field]: value }; return { ...prev, conditions }; }); }; const removeCondition = (index: number) => { setForm((prev) => ({ ...prev, conditions: prev.conditions.filter((_, i) => i !== index), })); }; const addAction = () => { setForm((prev) => ({ ...prev, actions: [...prev.actions, { type: 'api_call', config: {} }], })); }; const updateAction = (index: number, field: 'type' | 'config', value: string | Record) => { setForm((prev) => { const actions = [...prev.actions]; if (field === 'type') { actions[index] = { ...actions[index], type: value as 'api_call' | 'notification' | 'workflow_start' }; } else { actions[index] = { ...actions[index], config: value as Record }; } return { ...prev, actions }; }); }; const removeAction = (index: number) => { setForm((prev) => ({ ...prev, actions: prev.actions.filter((_, i) => i !== index), })); }; const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); onSave({ name: form.name, description: form.description || undefined, trigger_type: form.trigger_type, trigger_config: form.trigger_config, conditions: form.conditions, actions: form.actions, active: form.active, dry_run: form.dry_run, }); }; return ( {/* Name & Description */} {t('automation.name')} * updateField('name', e.target.value)} required className="w-full rounded-lg border border-secondary-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary-500" placeholder="My Automation" /> {t('automation.description')} updateField('description', e.target.value)} rows={2} className="w-full rounded-lg border border-secondary-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary-500" placeholder="Optional description" /> {/* Trigger Type */} {t('automation.triggerType')} updateField('trigger_type', e.target.value as 'event' | 'schedule' | 'manual')} /> {/* Trigger Config */} {form.trigger_type === 'event' && ( {t('automation.eventName')} updateField('trigger_config', { ...form.trigger_config, event_name: e.target.value })} className="w-full rounded-lg border border-secondary-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary-500" placeholder="contact.created" /> )} {form.trigger_type === 'schedule' && ( {t('automation.cronExpression')} updateField('trigger_config', { ...form.trigger_config, cron: e.target.value })} className="w-full rounded-lg border border-secondary-300 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary-500" placeholder="0 9 * * *" /> Cron expression (e.g. 0 9 * * * for daily at 9am) )} {/* Conditions */} {t('automation.conditions')} + {t('common.add')} {form.conditions.length === 0 && ( {t('automation.noConditions')} )} {form.conditions.map((cond, i) => ( updateCondition(i, 'field', e.target.value)} placeholder="Field" className="flex-1 rounded-lg border border-secondary-300 px-3 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-primary-500" /> updateCondition(i, 'operator', e.target.value)} className="w-32" /> updateCondition(i, 'value', e.target.value)} placeholder="Value" className="flex-1 rounded-lg border border-secondary-300 px-3 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-primary-500" /> removeCondition(i)} className="text-danger-500 hover:text-danger-700 p-1" aria-label="Remove condition" > ))} {/* Actions */} {t('automation.actions')} + {t('common.add')} {form.actions.length === 0 && ( {t('automation.noActions')} )} {form.actions.map((action, i) => ( updateAction(i, 'type', e.target.value)} className="w-40" /> { try { updateAction(i, 'config', JSON.parse(e.target.value)); } catch { // ignore invalid JSON while typing } }} placeholder='{"url": "..."}' className="flex-1 rounded-lg border border-secondary-300 px-3 py-1.5 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-primary-500" /> removeAction(i)} className="text-danger-500 hover:text-danger-700 p-1" aria-label="Remove action" > ))} {/* Toggles */} updateField('active', e.target.checked)} className="rounded border-secondary-300 text-primary-600 focus:ring-primary-500" /> {t('automation.active')} updateField('dry_run', e.target.checked)} className="rounded border-secondary-300 text-primary-600 focus:ring-primary-500" /> {t('automation.dryRun')} {/* Buttons */} {t('common.cancel')} {initial ? t('common.save') : t('common.create')} ); } function RunHistoryModal({ automationId, open, onClose, }: { automationId: string; open: boolean; onClose: () => void; }) { const { t } = useTranslation(); const { data: runs, isLoading } = useAutomationRuns(automationId); return ( {isLoading ? ( {[1, 2, 3].map((i) => ( ))} ) : !runs || runs.length === 0 ? ( } /> ) : ( {runs.map((run) => ( {run.status} {new Date(run.started_at).toLocaleString()} {run.error && ( {run.error} )} ))} )} ); } function VersionHistoryModal({ automationId, open, onClose, }: { automationId: string; open: boolean; onClose: () => void; }) { const { t } = useTranslation(); const toast = useToast(); const { data: versions, isLoading } = useAutomationVersions(automationId); const restoreMutation = useRestoreAutomationVersion(); const handleRestore = async (versionId: string) => { try { await restoreMutation.mutateAsync({ id: automationId, versionId }); toast.success(t('automation.versionRestored')); } catch (err: any) { toast.error(err.message || t('common.error')); } }; return ( {isLoading ? ( {[1, 2, 3].map((i) => ( ))} ) : !versions || versions.length === 0 ? ( } /> ) : ( {versions.map((ver) => ( v{ver.version} {new Date(ver.created_at).toLocaleString()} handleRestore(ver.id)} isLoading={restoreMutation.isPending} > {t('common.restore')} ))} )} ); } export function AutomationDashboardPage() { const { t } = useTranslation(); const toast = useToast(); const { data: automations, isLoading, isError, refetch } = useAutomations(); const createMutation = useCreateAutomation(); const updateMutation = useUpdateAutomation(); const deleteMutation = useDeleteAutomation(); const executeMutation = useExecuteAutomation(); const dryRunMutation = useDryRunAutomation(); const [showForm, setShowForm] = useState(false); const [editingAutomation, setEditingAutomation] = useState(undefined); const [confirmDelete, setConfirmDelete] = useState(null); const [runHistoryId, setRunHistoryId] = useState(null); const [versionHistoryId, setVersionHistoryId] = useState(null); const handleSave = async (data: Partial) => { try { if (editingAutomation) { await updateMutation.mutateAsync({ id: editingAutomation.id, data }); toast.success(t('automation.updated')); } else { await createMutation.mutateAsync(data); toast.success(t('automation.created')); } setShowForm(false); setEditingAutomation(undefined); } catch (err: any) { toast.error(err.message || t('common.error')); } }; const handleDelete = async () => { if (!confirmDelete) return; try { await deleteMutation.mutateAsync(confirmDelete.id); toast.success(t('automation.deleted')); setConfirmDelete(null); } catch (err: any) { toast.error(err.message || t('common.error')); } }; const handleExecute = async (id: string) => { try { await executeMutation.mutateAsync(id); toast.success(t('automation.executed')); } catch (err: any) { toast.error(err.message || t('common.error')); } }; const handleDryRun = async (id: string) => { try { await dryRunMutation.mutateAsync(id); toast.success(t('automation.dryRunSuccess')); } catch (err: any) { toast.error(err.message || t('common.error')); } }; const openEdit = (automation: AutomationDefinition) => { setEditingAutomation(automation); setShowForm(true); }; const openCreate = () => { setEditingAutomation(undefined); setShowForm(true); }; const isSaving = createMutation.isPending || updateMutation.isPending; return ( {/* Header */} {t('automation.title')} {t('automation.subtitle')} }> {t('automation.create')} {/* Loading */} {isLoading && ( {[1, 2, 3].map((i) => ( ))} )} {/* Error */} {isError && ( {t('common.errorLoading')} refetch()}> {t('common.retry')} )} {/* Empty State */} {!isLoading && !isError && (!automations || automations.length === 0) && ( } action={ }> {t('automation.createFirst')} } /> )} {/* Automation List */} {!isLoading && !isError && automations && automations.length > 0 && ( {automations.map((automation) => ( {automation.name} {automation.active ? t('automation.active') : t('automation.inactive')} {triggerIcon(automation.trigger_type)} {automation.trigger_type} {automation.description && ( {automation.description} )} {automation.conditions?.length || 0} conditions {automation.actions?.length || 0} actions {automation.dry_run && ( {t('automation.dryRun')} )} handleExecute(automation.id)} isLoading={executeMutation.isPending} title={t('automation.execute')} > handleDryRun(automation.id)} isLoading={dryRunMutation.isPending} title={t('automation.dryRun')} > setRunHistoryId(automation.id)} title={t('automation.runHistory')} > setVersionHistoryId(automation.id)} title={t('automation.versionHistory')} > openEdit(automation)} title={t('common.edit')} > setConfirmDelete(automation)} title={t('common.delete')} > ))} )} {/* Create/Edit Modal */} { setShowForm(false); setEditingAutomation(undefined); }} title={editingAutomation ? t('automation.edit') : t('automation.create')} size="lg" > { setShowForm(false); setEditingAutomation(undefined); }} isSaving={isSaving} /> {/* Run History Modal */} {runHistoryId && ( setRunHistoryId(null)} /> )} {/* Version History Modal */} {versionHistoryId && ( setVersionHistoryId(null)} /> )} {/* Delete Confirmation */} setConfirmDelete(null)} onConfirm={handleDelete} title={t('automation.deleteConfirmTitle')} message={t('automation.deleteConfirmMessage', { name: confirmDelete?.name })} confirmLabel={t('common.delete')} variant="danger" /> ); }
Cron expression (e.g. 0 9 * * * for daily at 9am)
{t('automation.noConditions')}
{t('automation.noActions')}
{t('automation.subtitle')}
{automation.description}