ultisuite-client/components/gmail/settings/automation/automation-rules-panel.tsx
2026-05-25 13:52:40 +02:00

195 lines
6.9 KiB
TypeScript

'use client'
import { useMemo, useState } from 'react'
import { Pencil, Plus, Trash2, FunctionSquare, Workflow } from 'lucide-react'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import {
useMailRules,
useCreateMailRule,
useUpdateMailRule,
useDeleteMailRule,
} from '@/lib/api/hooks/use-mail-automation-queries'
import { useAuthReady } from '@/lib/api/use-auth-ready'
import { SettingsSyncBanner } from '@/components/gmail/settings/settings-sync-banner'
import { RuleWorkflowEditor } from './rule-workflow-editor'
import { RuleSimulatorPanel } from './rule-simulator-panel'
import type { ApiRule } from '@/lib/api/types'
import type { RuleEditorState } from '@/lib/mail-automation/types'
import {
createDefaultRuleEditorState,
workflowToApiPayload,
} from '@/lib/mail-automation/defaults'
import { parseWorkflowFromRule } from '@/lib/mail-automation/workflow-flow'
import { AutomationSuggestionsProvider } from './automation-suggest-input'
export function AutomationRulesPanel() {
const { ready, authenticated } = useAuthReady()
const { data: rules = [], isFetching, isError, refetch, isPending } = useMailRules()
const createRule = useCreateMailRule()
const updateRule = useUpdateMailRule()
const deleteRule = useDeleteMailRule()
const [editorOpen, setEditorOpen] = useState(false)
const [editingId, setEditingId] = useState<string | null>(null)
const [editorState, setEditorState] = useState<RuleEditorState>(() =>
createDefaultRuleEditorState('rule')
)
const showInitialLoad = ready && authenticated && isPending && rules.length === 0
const rulesOnly = useMemo(() => rules.filter((r) => (r.rule_kind ?? 'rule') === 'rule'), [rules])
const functionsOnly = useMemo(() => rules.filter((r) => r.rule_kind === 'function'), [rules])
function openNew(kind: 'rule' | 'function') {
setEditingId(null)
setEditorState(createDefaultRuleEditorState(kind))
setEditorOpen(true)
}
function openEdit(rule: ApiRule) {
const kind = rule.rule_kind ?? 'rule'
const workflow = parseWorkflowFromRule(rule.workflow, kind)
setEditingId(rule.id)
setEditorState({
name: rule.name,
priority: rule.priority,
is_active: rule.is_active,
rule_kind: kind,
account_id: rule.account_id,
workflow:
workflow ??
createDefaultRuleEditorState(kind).workflow,
})
setEditorOpen(true)
}
async function handleSave() {
const payload = workflowToApiPayload(editorState)
if (editingId) {
await updateRule.mutateAsync({ ruleId: editingId, ...payload })
} else {
await createRule.mutateAsync(payload)
}
setEditorOpen(false)
}
return (
<div className="space-y-4">
<SettingsSyncBanner isFetching={isFetching} isError={isError} onRetry={() => refetch()} />
<div className="flex flex-wrap gap-2">
<Button type="button" size="sm" onClick={() => openNew('rule')}>
<Plus className="mr-1 size-3.5" />
Nouvelle règle
</Button>
<Button type="button" size="sm" variant="outline" onClick={() => openNew('function')}>
<FunctionSquare className="mr-1 size-3.5" />
Nouvelle fonction
</Button>
</div>
{showInitialLoad ? null : rules.length === 0 ? (
<p className="text-sm text-muted-foreground">
Aucune règle. Créez une règle graphique avec déclencheurs, conditions et actions.
</p>
) : (
<>
<RuleList title="Règles actives" icon={Workflow} items={rulesOnly} onEdit={openEdit} onDelete={(id) => deleteRule.mutate(id)} />
{functionsOnly.length > 0 ? (
<RuleList title="Fonctions réutilisables" icon={FunctionSquare} items={functionsOnly} onEdit={openEdit} onDelete={(id) => deleteRule.mutate(id)} />
) : null}
</>
)}
<Dialog open={editorOpen} onOpenChange={setEditorOpen}>
<DialogContent className="flex max-h-[95vh] max-w-[95vw] flex-col gap-0 overflow-hidden p-0 sm:max-w-6xl">
<DialogHeader className="border-b border-border px-4 py-3">
<DialogTitle className="text-base">
{editingId ? 'Modifier' : 'Créer'}{' '}
{editorState.rule_kind === 'function' ? 'une fonction' : 'une règle'}
</DialogTitle>
</DialogHeader>
<div className="flex-1 overflow-y-auto p-4">
<AutomationSuggestionsProvider>
<RuleWorkflowEditor
key={editingId ?? `new-${editorState.rule_kind}`}
state={editorState}
allRules={rules}
onChange={setEditorState}
/>
<div className="mt-4">
<RuleSimulatorPanel state={editorState} ruleId={editingId ?? undefined} />
</div>
</AutomationSuggestionsProvider>
</div>
<div className="flex justify-end gap-2 border-t border-border px-4 py-3">
<Button type="button" variant="outline" onClick={() => setEditorOpen(false)}>
Annuler
</Button>
<Button
type="button"
disabled={!editorState.name.trim() || createRule.isPending || updateRule.isPending}
onClick={handleSave}
>
Enregistrer
</Button>
</div>
</DialogContent>
</Dialog>
</div>
)
}
function RuleList({
title,
icon: Icon,
items,
onEdit,
onDelete,
}: {
title: string
icon: typeof Workflow
items: ApiRule[]
onEdit: (rule: ApiRule) => void
onDelete: (id: string) => void
}) {
if (items.length === 0) return null
return (
<section className="space-y-2">
<div className="flex items-center gap-2 text-sm font-medium">
<Icon className="size-4 opacity-70" />
{title}
</div>
<ul className="divide-y divide-border rounded-lg border border-border">
{items.map((rule) => (
<li key={rule.id} className="flex items-start justify-between gap-2 px-3 py-3">
<div className="min-w-0">
<p className="text-sm font-medium">{rule.name}</p>
<p className="text-xs text-muted-foreground">
Priorité {rule.priority}
{rule.is_active === false ? ' · inactive' : ''}
{rule.match_count != null ? ` · ${rule.match_count} exécutions` : ''}
{rule.workflow ? ' · graphique' : ' · legacy'}
</p>
</div>
<div className="flex shrink-0 gap-1">
<Button type="button" variant="ghost" size="icon" onClick={() => onEdit(rule)}>
<Pencil className="size-4" />
</Button>
<Button type="button" variant="ghost" size="icon" onClick={() => onDelete(rule.id)}>
<Trash2 className="size-4" />
</Button>
</div>
</li>
))}
</ul>
</section>
)
}