195 lines
6.9 KiB
TypeScript
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>
|
|
)
|
|
}
|