import { useMemo, useState } from "react"; import DashboardLayout from "@/components/DashboardLayout"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Badge } from "@/components/ui/badge"; import { trpc } from "@/lib/trpc"; import { AUTOMATION_ACTION_FILTERS, type AutomationActionFilter, matchesAutomationActionFilter, } from "@shared/automationActions"; import { toast } from "sonner"; import { Plus, Edit, Trash2, Power, PowerOff, Copy } from "lucide-react"; interface Condition { field: string; operator: string; value: string; } interface Actions { typeAchat?: string; serviceConcerne?: string; ventilationComptable?: string; isSubscription?: 0 | 1; } export default function AutomationRules() { const { data: rules = [], isLoading } = trpc.automationRules.list.useQuery(); const { data: departments } = trpc.departments.getByUser.useQuery(); const { data: allocations } = trpc.accountingAllocations.getByUser.useQuery(); const utils = trpc.useUtils(); const [dialogOpen, setDialogOpen] = useState(false); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [ruleToDelete, setRuleToDelete] = useState(null); const [editingRule, setEditingRule] = useState(null); const [wizardMode, setWizardMode] = useState(false); const [wizardStep, setWizardStep] = useState(1); const [testResultsOpen, setTestResultsOpen] = useState(false); const [testResults, setTestResults] = useState(null); const [actionFilter, setActionFilter] = useState("all"); // Form state const [ruleName, setRuleName] = useState(""); const [conditionsLogic, setConditionsLogic] = useState<"AND" | "OR">("AND"); const [conditions, setConditions] = useState([{ field: "supplierName", operator: "contains", value: "" }]); const [actions, setActions] = useState({}); // Textes personnalisés quand "Autre" est sélectionné const [customTypeAchat, setCustomTypeAchat] = useState(""); const [customServiceConcerne, setCustomServiceConcerne] = useState(""); const [customVentilation, setCustomVentilation] = useState(""); // Helper : détecte si une valeur est "Autre" (insensible à la casse) const isAutre = (val?: string) => val?.toUpperCase() === "AUTRE"; // Helper : résout la valeur finale (si Autre + texte perso, retourne le texte perso) const resolveAction = (val?: string, custom?: string) => { if (!val || val === "__NONE__") return undefined; if (isAutre(val) && custom?.trim()) return custom.trim().toUpperCase(); if (isAutre(val)) return "AUTRE"; return val; }; const normalizeSubscriptionAction = (value: unknown): 0 | 1 | undefined => { if (value === 1 || value === "1" || value === "OUI") return 1; if (value === 0 || value === "0" || value === "NON") return 0; return undefined; }; const createMutation = trpc.automationRules.create.useMutation({ onSuccess: (rule) => { if (rule.isActive === 1) { toast.success("Règle créée et appliquée aux factures BAP existantes"); } else { toast.success("Règle créée avec succès"); } utils.automationRules.list.invalidate(); utils.invoices.list.invalidate(); // Refresh invoices list closeDialog(); }, onError: (error) => { toast.error(error.message || "Erreur lors de la création"); }, }); const updateMutation = trpc.automationRules.update.useMutation({ onSuccess: (rule) => { if (rule && rule.isActive === 1) { toast.success("Règle mise à jour et appliquée aux factures BAP existantes"); } else { toast.success("Règle mise à jour"); } utils.automationRules.list.invalidate(); utils.invoices.list.invalidate(); // Refresh invoices list closeDialog(); }, onError: (error) => { toast.error(error.message || "Erreur lors de la mise à jour"); }, }); const deleteMutation = trpc.automationRules.delete.useMutation({ onSuccess: () => { toast.success("Règle supprimée"); utils.automationRules.list.invalidate(); setDeleteDialogOpen(false); setRuleToDelete(null); }, onError: (error) => { toast.error(error.message || "Erreur lors de la suppression"); }, }); const toggleActiveMutation = trpc.automationRules.update.useMutation({ onSuccess: () => { toast.success("Statut mis à jour"); utils.automationRules.list.invalidate(); }, onError: (error) => { toast.error(error.message || "Erreur lors de la mise à jour"); }, }); const testMutation = trpc.automationRules.test.useMutation({ onSuccess: (data) => { setTestResults(data); setTestResultsOpen(true); }, onError: (error) => { toast.error(error.message || "Erreur lors du test"); }, }); const duplicateMutation = trpc.automationRules.duplicate.useMutation({ onSuccess: () => { toast.success("Règle dupliquée avec succès"); utils.automationRules.list.invalidate(); }, onError: (error) => { toast.error(error.message || "Erreur lors de la duplication"); }, }); const openCreateDialog = () => { setEditingRule(null); setRuleName(""); setConditionsLogic("AND"); setConditions([{ field: "supplierName", operator: "contains", value: "" }]); setActions({}); setCustomTypeAchat(""); setCustomServiceConcerne(""); setCustomVentilation(""); setDialogOpen(true); }; const openEditDialog = (rule: any) => { setEditingRule(rule); setRuleName(rule.name); setConditionsLogic(rule.conditionsLogic || "AND"); try { setConditions(JSON.parse(rule.conditions)); } catch { setConditions([{ field: "supplierName", operator: "contains", value: "" }]); } try { const parsedActions = JSON.parse(rule.actions); // Détecter si les valeurs stockées ne correspondent pas aux valeurs fixes // (ce qui signifie qu'un texte personnalisé avait été saisi) const knownTypeAchat = ["CAPEX", "OPEX", "AUTRE", undefined]; const deptNames = departments?.map(d => d.name) || []; const allocNames = allocations?.map(a => a.name) || []; if (parsedActions.typeAchat && !knownTypeAchat.includes(parsedActions.typeAchat)) { setCustomTypeAchat(parsedActions.typeAchat); parsedActions.typeAchat = "AUTRE"; } else { setCustomTypeAchat(""); } if (parsedActions.serviceConcerne && !deptNames.includes(parsedActions.serviceConcerne) && parsedActions.serviceConcerne !== "AUTRE") { setCustomServiceConcerne(parsedActions.serviceConcerne); parsedActions.serviceConcerne = "AUTRE"; } else { setCustomServiceConcerne(""); } if (parsedActions.ventilationComptable && !allocNames.includes(parsedActions.ventilationComptable) && parsedActions.ventilationComptable !== "AUTRE") { setCustomVentilation(parsedActions.ventilationComptable); parsedActions.ventilationComptable = "AUTRE"; } else { setCustomVentilation(""); } setActions({ ...parsedActions, isSubscription: normalizeSubscriptionAction(parsedActions.isSubscription) }); } catch { setActions({}); setCustomTypeAchat(""); setCustomServiceConcerne(""); setCustomVentilation(""); } setDialogOpen(true); }; const closeDialog = () => { setDialogOpen(false); setEditingRule(null); }; const handleSave = () => { if (!ruleName.trim()) { toast.error("Le nom de la règle est obligatoire"); return; } const conditionsJSON = JSON.stringify(conditions); // Résoudre les valeurs finales (remplacer AUTRE par le texte personnalisé si fourni) const resolvedActions: Actions = { typeAchat: resolveAction(actions.typeAchat, customTypeAchat), serviceConcerne: resolveAction(actions.serviceConcerne, customServiceConcerne), ventilationComptable: resolveAction(actions.ventilationComptable, customVentilation), isSubscription: actions.isSubscription, }; // Supprimer les clés undefined if (!resolvedActions.typeAchat) delete resolvedActions.typeAchat; if (!resolvedActions.serviceConcerne) delete resolvedActions.serviceConcerne; if (!resolvedActions.ventilationComptable) delete resolvedActions.ventilationComptable; if (resolvedActions.isSubscription === undefined) delete resolvedActions.isSubscription; const actionsJSON = JSON.stringify(resolvedActions); if (editingRule) { updateMutation.mutate({ id: editingRule.id, name: ruleName, conditionsLogic, conditions: conditionsJSON, actions: actionsJSON, }); } else { createMutation.mutate({ name: ruleName, conditionsLogic, conditions: conditionsJSON, actions: actionsJSON, isActive: 1, priority: rules.length, }); } }; const handleDelete = (id: number) => { setRuleToDelete(id); setDeleteDialogOpen(true); }; const confirmDelete = () => { if (ruleToDelete) { deleteMutation.mutate({ id: ruleToDelete }); } }; const toggleActive = (rule: any) => { toggleActiveMutation.mutate({ id: rule.id, isActive: rule.isActive === 1 ? 0 : 1, }); }; const addCondition = () => { setConditions([...conditions, { field: "supplierName", operator: "contains", value: "" }]); }; const removeCondition = (index: number) => { setConditions(conditions.filter((_, i) => i !== index)); }; const updateCondition = (index: number, key: keyof Condition, value: string) => { const newConditions = [...conditions]; newConditions[index][key] = value; setConditions(newConditions); }; const fieldOptions = [ { value: "supplierName", label: "Fournisseur" }, { value: "recipientName", label: "Destinataire" }, { value: "invoiceNumber", label: "N° Facture" }, { value: "totalAmount", label: "Montant" }, { value: "invoiceDate", label: "Date facture" }, { value: "orderNumber", label: "N° Commande" }, { value: "deliveryNoteNumber", label: "N° Bon de livraison" }, ]; const operatorOptions = [ { value: "contains", label: "contient" }, { value: "equals", label: "est égal à" }, { value: "startsWith", label: "commence par" }, { value: "endsWith", label: "finit par" }, { value: ">", label: "supérieur à" }, { value: "<", label: "inférieur à" }, { value: ">=", label: "supérieur ou égal à" }, { value: "<=", label: "inférieur ou égal à" }, ]; const filteredRules = useMemo( () => rules.filter((rule) => matchesAutomationActionFilter(rule.actions, actionFilter)), [rules, actionFilter], ); return (

Automatismes

Gérez les règles de remplissage automatique des champs

Règles d'automatisme Les règles sont appliquées dans l'ordre de priorité lors de l'import des factures
{filteredRules.length} règle{filteredRules.length > 1 ? "s" : ""} affichée{filteredRules.length > 1 ? "s" : ""}
{isLoading ? (
Chargement...
) : rules.length === 0 ? (
Aucune règle d'automatisme configurée
) : filteredRules.length === 0 ? (
Aucune règle ne correspond à cette action
) : ( Nom Conditions Actions Statut Actions {filteredRules.map((rule) => { let conditionsDisplay = ""; let actionsDisplay = ""; try { const conds = JSON.parse(rule.conditions); conditionsDisplay = conds.map((c: Condition) => `${fieldOptions.find(f => f.value === c.field)?.label || c.field} ${operatorOptions.find(o => o.value === c.operator)?.label || c.operator} "${c.value}"` ).join(` ${rule.conditionsLogic} `); } catch {} try { const acts = JSON.parse(rule.actions); const actionsArr = []; if (acts.typeAchat) actionsArr.push(`Type: ${acts.typeAchat}`); if (acts.serviceConcerne) actionsArr.push(`Service: ${acts.serviceConcerne}`); if (acts.ventilationComptable) actionsArr.push(`Ventilation: ${acts.ventilationComptable}`); if (acts.isSubscription !== undefined) actionsArr.push(`Abonnement: ${normalizeSubscriptionAction(acts.isSubscription) === 1 ? "Oui" : "Non"}`); actionsDisplay = actionsArr.join(", "); } catch {} return ( {rule.name} {conditionsDisplay} {actionsDisplay} {rule.isActive === 1 ? ( Actif ) : ( Inactif )}
); })}
)}
{/* Create/Edit Dialog */} {wizardMode && !editingRule ? (
Assistant de création Étape {wizardStep}/3
) : ( editingRule ? "Modifier la règle" : "Nouvelle règle" )}
{wizardMode && !editingRule ? ( wizardStep === 1 ? "Définissez les conditions pour déclencher la règle" : wizardStep === 2 ? "Choisissez les champs à remplir automatiquement" : "Vérifiez et validez votre règle" ) : ( "Définissez les conditions (SI) et les actions (ALORS) pour le remplissage automatique" )}
{wizardMode && !editingRule ? ( // WIZARD MODE - Étapes guidées
{wizardStep === 1 && ( // ÉTAPE 1: Nom et conditions simples
setRuleName(e.target.value)} placeholder="Ex: Factures Microsoft CAPEX" />

Quand appliquer cette règle ?

Définissez les conditions pour déclencher la règle

{ if (conditions.length === 0) { setConditions([{ field: "supplierName", operator: "contains", value: e.target.value }]); } else { updateCondition(0, "value", e.target.value); } }} placeholder="Ex: Microsoft, Amazon, etc." className="flex-1" />
)} {wizardStep === 2 && ( // ÉTAPE 2: Actions

Que voulez-vous remplir automatiquement ?

Sélectionnez les champs à compléter

{isAutre(actions.typeAchat) && ( setCustomTypeAchat(e.target.value)} placeholder="Texte à afficher sur le BAP (ex: INVESTISSEMENT)" className="mt-1 border-orange-300 focus:border-orange-500" /> )}
{isAutre(actions.serviceConcerne) && ( setCustomServiceConcerne(e.target.value)} placeholder="Texte à afficher sur le BAP (ex: LOGISTIQUE)" className="mt-1 border-orange-300 focus:border-orange-500" /> )}
{isAutre(actions.ventilationComptable) && ( setCustomVentilation(e.target.value)} placeholder="Texte à afficher sur le BAP (ex: SANITAIRE)" className="mt-1 border-orange-300 focus:border-orange-500" /> )}
)} {wizardStep === 3 && ( // ÉTAPE 3: Révision

Récapitulatif de la règle

Vérifiez les informations avant de créer

Nom
{ruleName || "(non défini)"}
Condition
Le fournisseur {conditions[0]?.operator === "contains" ? "contient" : conditions[0]?.operator === "equals" ? "est égal à" : "commence par"} "{conditions[0]?.value || "(vide)"}"
Actions
{actions.typeAchat &&
• Type d'achat : {actions.typeAchat}
} {actions.serviceConcerne &&
• Service : {actions.serviceConcerne}
} {actions.ventilationComptable &&
• Ventilation : {actions.ventilationComptable}
} {actions.isSubscription !== undefined &&
• Abonnement : {actions.isSubscription === 1 ? "Oui" : "Non"}
} {!actions.typeAchat && !actions.serviceConcerne && !actions.ventilationComptable && actions.isSubscription === undefined && (
Aucune action définie
)}
)}
) : ( // MODE AVANCÉ - Formulaire complet
{/* Rule Name */}
setRuleName(e.target.value)} placeholder="Ex: Factures Microsoft CAPEX" />
{/* Conditions */}
{conditions.map((condition, index) => (
updateCondition(index, "value", e.target.value)} placeholder="Valeur" className="flex-1" />
))}
{/* Actions */}

Définit si les factures correspondant à la règle sont des abonnements.

{isAutre(actions.typeAchat) && ( setCustomTypeAchat(e.target.value)} placeholder="Saisir le texte à afficher sur le BAP (ex: INVESTISSEMENT)" className="mt-1 border-orange-300 focus:border-orange-500" autoFocus /> )}
{isAutre(actions.serviceConcerne) && ( setCustomServiceConcerne(e.target.value)} placeholder="Saisir le texte à afficher sur le BAP (ex: LOGISTIQUE)" className="mt-1 border-orange-300 focus:border-orange-500" /> )}
{isAutre(actions.ventilationComptable) && ( setCustomVentilation(e.target.value)} placeholder="Saisir le texte à afficher sur le BAP (ex: SANITAIRE)" className="mt-1 border-orange-300 focus:border-orange-500" /> )}
)}
{wizardMode && !editingRule ? ( // Navigation wizard <> ) : ( // Navigation mode avancé <>
)}
{/* Delete Confirmation Dialog */} Confirmer la suppression Êtes-vous sûr de vouloir supprimer cette règle ? Cette action est irréversible. Annuler Supprimer {/* Test Results Dialog */} Résultats du test Simulation de l'application de la règle sur vos factures existantes {testResults && (
{testResults.totalInvoices}
Factures totales
{testResults.affectedCount}
Factures affectées
{testResults.affectedInvoices && testResults.affectedInvoices.length > 0 && (

Aperçu des factures affectées (10 premières) :

Fournisseur N° Facture Modifications {testResults.affectedInvoices.map((inv: any) => ( {inv.supplierName || "-"} {inv.invoiceNumber || "-"} {inv.changes.typeAchat &&
Type: {inv.changes.typeAchat}
} {inv.changes.serviceConcerne &&
Service: {inv.changes.serviceConcerne}
} {inv.changes.ventilationComptable &&
Ventilation: {inv.changes.ventilationComptable}
}
))}
)}
)}
); }