diff --git a/client/src/App.tsx b/client/src/App.tsx index 912130c..29bff32 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -16,6 +16,7 @@ import ImportSettings from "./pages/ImportSettings"; import History from "./pages/History"; import Users from "./pages/Users"; import ListsAdmin from "./pages/ListsAdmin"; +import AutomationRules from "./pages/AutomationRules"; function Router() { return ( @@ -32,6 +33,7 @@ function Router() { + diff --git a/client/src/components/DashboardLayout.tsx b/client/src/components/DashboardLayout.tsx index 8d85cd9..fbdb05f 100644 --- a/client/src/components/DashboardLayout.tsx +++ b/client/src/components/DashboardLayout.tsx @@ -21,7 +21,7 @@ import { } from "@/components/ui/sidebar"; import { getLoginUrl } from "@/const"; import { useIsMobile } from "@/hooks/useMobile"; -import { LayoutDashboard, LogOut, PanelLeft, Users, FileText, Upload, History, Settings, Download, List } from "lucide-react"; +import { LayoutDashboard, LogOut, PanelLeft, Users, FileText, Upload, History, Settings, Download, List, Zap } from "lucide-react"; import { CSSProperties, useEffect, useRef, useState } from "react"; import { useLocation } from "wouter"; import { DashboardLayoutSkeleton } from './DashboardLayoutSkeleton'; @@ -33,8 +33,9 @@ const menuItems = [ { icon: FileText, label: "Factures", path: "/invoices" }, { icon: FileText, label: "Factures BAP", path: "/invoices-bap" }, { icon: History, label: "Historique", path: "/history" }, - { icon: Settings, label: "Paramètres", path: "/settings" }, - { icon: Download, label: "Paramètres de réception", path: "/import-settings" }, + { icon: Settings, label: "Param\u00e8tres", path: "/settings" }, + { icon: Download, label: "Param\u00e8tres de r\u00e9ception", path: "/import-settings" }, + { icon: Zap, label: "Automatismes", path: "/automation-rules" }, { icon: List, label: "Administration des listes", path: "/lists-admin" }, { icon: Users, label: "Utilisateurs", path: "/users", adminOnly: true }, ]; diff --git a/client/src/pages/AutomationRules.tsx b/client/src/pages/AutomationRules.tsx new file mode 100644 index 0000000..7baa25e --- /dev/null +++ b/client/src/pages/AutomationRules.tsx @@ -0,0 +1,525 @@ +import { 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 { toast } from "sonner"; +import { Plus, Edit, Trash2, Power, PowerOff } from "lucide-react"; + +interface Condition { + field: string; + operator: string; + value: string; +} + +interface Actions { + typeAchat?: string; + serviceConcerne?: string; + ventilationComptable?: string; +} + +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); + + // 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({}); + + const createMutation = trpc.automationRules.create.useMutation({ + onSuccess: () => { + toast.success("Règle créée avec succès"); + utils.automationRules.list.invalidate(); + closeDialog(); + }, + onError: (error) => { + toast.error(error.message || "Erreur lors de la création"); + }, + }); + + const updateMutation = trpc.automationRules.update.useMutation({ + onSuccess: () => { + toast.success("Règle mise à jour"); + utils.automationRules.list.invalidate(); + 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 openCreateDialog = () => { + setEditingRule(null); + setRuleName(""); + setConditionsLogic("AND"); + setConditions([{ field: "supplierName", operator: "contains", value: "" }]); + setActions({}); + 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 { + setActions(JSON.parse(rule.actions)); + } catch { + setActions({}); + } + 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); + const actionsJSON = JSON.stringify(actions); + + 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: "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 à" }, + ]; + + return ( + + + + + Automatismes + Gérez les règles de remplissage automatique des champs + + + + Nouvelle règle + + + + + + Règles d'automatisme + + Les règles sont appliquées dans l'ordre de priorité lors de l'import des factures + + + + {isLoading ? ( + Chargement... + ) : rules.length === 0 ? ( + + Aucune règle d'automatisme configurée + + ) : ( + + + + Nom + Conditions + Actions + Statut + Actions + + + + {rules.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}`); + actionsDisplay = actionsArr.join(", "); + } catch {} + + return ( + + {rule.name} + {conditionsDisplay} + {actionsDisplay} + + {rule.isActive === 1 ? ( + Actif + ) : ( + Inactif + )} + + + + toggleActive(rule)} + title={rule.isActive === 1 ? "Désactiver" : "Activer"} + > + {rule.isActive === 1 ? ( + + ) : ( + + )} + + openEditDialog(rule)} + > + + + handleDelete(rule.id)} + > + + + + + + ); + })} + + + )} + + + + {/* Create/Edit Dialog */} + + + + {editingRule ? "Modifier la règle" : "Nouvelle règle"} + + Définissez les conditions (SI) et les actions (ALORS) pour le remplissage automatique + + + + + {/* Rule Name */} + + Nom de la règle + setRuleName(e.target.value)} + placeholder="Ex: Factures Microsoft CAPEX" + /> + + + {/* Conditions */} + + Conditions (SI) + + {conditions.map((condition, index) => ( + + updateCondition(index, "field", value)} + > + + + + + {fieldOptions.map((opt) => ( + + {opt.label} + + ))} + + + + updateCondition(index, "operator", value)} + > + + + + + {operatorOptions.map((opt) => ( + + {opt.label} + + ))} + + + + updateCondition(index, "value", e.target.value)} + placeholder="Valeur" + className="flex-1" + /> + + removeCondition(index)} + disabled={conditions.length === 1} + > + + + + ))} + + + + + + Ajouter une condition + + + setConditionsLogic(value)}> + + + + + ET (AND) + OU (OR) + + + + + + {/* Actions */} + + Actions (ALORS) + + + Type d'achat + setActions({ ...actions, typeAchat: value || undefined })} + > + + + + + Ne pas modifier + CAPEX + OPEX + + + + + + Service concerné + setActions({ ...actions, serviceConcerne: value || undefined })} + > + + + + + Ne pas modifier + {departments?.map((dept) => ( + + {dept.name} + + ))} + + + + + + Ventilation comptable + setActions({ ...actions, ventilationComptable: value || undefined })} + > + + + + + Ne pas modifier + {allocations?.map((alloc) => ( + + {alloc.name} + + ))} + + + + + + + + + + Annuler + + + {editingRule ? "Mettre à jour" : "Créer"} + + + + + + {/* Delete Confirmation Dialog */} + + + + Confirmer la suppression + + Êtes-vous sûr de vouloir supprimer cette règle ? Cette action est irréversible. + + + + Annuler + + Supprimer + + + + + + + ); +} diff --git a/drizzle/0009_regular_warhawk.sql b/drizzle/0009_regular_warhawk.sql new file mode 100644 index 0000000..ce2835e --- /dev/null +++ b/drizzle/0009_regular_warhawk.sql @@ -0,0 +1,13 @@ +CREATE TABLE `automationRules` ( + `id` int AUTO_INCREMENT NOT NULL, + `userId` int NOT NULL, + `name` varchar(255) NOT NULL, + `isActive` int NOT NULL DEFAULT 1, + `priority` int NOT NULL DEFAULT 0, + `conditions` text NOT NULL, + `conditionsLogic` enum('AND','OR') NOT NULL DEFAULT 'AND', + `actions` text NOT NULL, + `createdAt` timestamp NOT NULL DEFAULT (now()), + `updatedAt` timestamp NOT NULL DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT `automationRules_id` PRIMARY KEY(`id`) +); diff --git a/drizzle/meta/0009_snapshot.json b/drizzle/meta/0009_snapshot.json new file mode 100644 index 0000000..3730cd6 --- /dev/null +++ b/drizzle/meta/0009_snapshot.json @@ -0,0 +1,1203 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "42bb01b9-d61a-4184-b4fc-b464a710984f", + "prevId": "5d54cefa-c00a-4c84-8b4b-b963d176a366", + "tables": { + "accountingAllocationList": { + "name": "accountingAllocationList", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": { + "user_allocation_unique": { + "name": "user_allocation_unique", + "columns": [ + "userId", + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "accountingAllocationList_id": { + "name": "accountingAllocationList_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "automationRules": { + "name": "automationRules", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "isActive": { + "name": "isActive", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "priority": { + "name": "priority", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "conditions": { + "name": "conditions", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "conditionsLogic": { + "name": "conditionsLogic", + "type": "enum('AND','OR')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'AND'" + }, + "actions": { + "name": "actions", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "automationRules_id": { + "name": "automationRules_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "departmentList": { + "name": "departmentList", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": { + "user_department_unique": { + "name": "user_department_unique", + "columns": [ + "userId", + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "departmentList_id": { + "name": "departmentList_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "importLogs": { + "name": "importLogs", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sourceFileId": { + "name": "sourceFileId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fileName": { + "name": "fileName", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "totalInvoicesDetected": { + "name": "totalInvoicesDetected", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "invoicesImported": { + "name": "invoicesImported", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "duplicatesIgnored": { + "name": "duplicatesIgnored", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "errors": { + "name": "errors", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "duplicateDetails": { + "name": "duplicateDetails", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "errorDetails": { + "name": "errorDetails", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "importedAt": { + "name": "importedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "importLogs_id": { + "name": "importLogs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "importSettings": { + "name": "importSettings", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "manualImportEnabled": { + "name": "manualImportEnabled", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "autoImportEnabled": { + "name": "autoImportEnabled", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "autoImportSourcePath": { + "name": "autoImportSourcePath", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autoImportFrequency": { + "name": "autoImportFrequency", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 60 + }, + "emailImportEnabled": { + "name": "emailImportEnabled", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "emailImportAddress": { + "name": "emailImportAddress", + "type": "varchar(320)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "emailImportPassword": { + "name": "emailImportPassword", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "emailImportHost": { + "name": "emailImportHost", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "emailImportPort": { + "name": "emailImportPort", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 993 + }, + "emailImportFrequency": { + "name": "emailImportFrequency", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 30 + }, + "exportFolder": { + "name": "exportFolder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "importSettings_id": { + "name": "importSettings_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "importSettings_userId_unique": { + "name": "importSettings_userId_unique", + "columns": [ + "userId" + ] + } + }, + "checkConstraint": {} + }, + "invoices": { + "name": "invoices", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sourceFileId": { + "name": "sourceFileId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "invoiceIndexInFile": { + "name": "invoiceIndexInFile", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "fileName": { + "name": "fileName", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fileKey": { + "name": "fileKey", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fileUrl": { + "name": "fileUrl", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "supplierName": { + "name": "supplierName", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "invoiceNumber": { + "name": "invoiceNumber", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "invoiceDate": { + "name": "invoiceDate", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deliveryNoteNumber": { + "name": "deliveryNoteNumber", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "orderNumber": { + "name": "orderNumber", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totalAmount": { + "name": "totalAmount", + "type": "decimal(10,2)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pageRange": { + "name": "pageRange", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "qualityScore": { + "name": "qualityScore", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadataFileKey": { + "name": "metadataFileKey", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadataFileUrl": { + "name": "metadataFileUrl", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "enum('processing','completed','error')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'processing'" + }, + "errorMessage": { + "name": "errorMessage", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "exportStatus": { + "name": "exportStatus", + "type": "enum('not_exported','exported','export_error')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'not_exported'" + }, + "manuallyEdited": { + "name": "manuallyEdited", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "serviceConcerne": { + "name": "serviceConcerne", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "typeAchat": { + "name": "typeAchat", + "type": "enum('CAPEX','OPEX')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ventilationComptable": { + "name": "ventilationComptable", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "extractedText": { + "name": "extractedText", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "isSubscription": { + "name": "isSubscription", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "exportedAt": { + "name": "exportedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "exportMode": { + "name": "exportMode", + "type": "enum('manual','automatic')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": { + "supplier_invoice_date_unique": { + "name": "supplier_invoice_date_unique", + "columns": [ + "supplierName", + "invoiceNumber", + "invoiceDate" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "invoices_id": { + "name": "invoices_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "llmLogs": { + "name": "llmLogs", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sourceFileId": { + "name": "sourceFileId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "invoiceId": { + "name": "invoiceId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "operation": { + "name": "operation", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "promptSent": { + "name": "promptSent", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rawResponse": { + "name": "rawResponse", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cleanedResponse": { + "name": "cleanedResponse", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "success": { + "name": "success", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "errorMessage": { + "name": "errorMessage", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "processingTimeMs": { + "name": "processingTimeMs", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pageRange": { + "name": "pageRange", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "llmLogs_id": { + "name": "llmLogs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "sourceFiles": { + "name": "sourceFiles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fileName": { + "name": "fileName", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fileKey": { + "name": "fileKey", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fileUrl": { + "name": "fileUrl", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "totalInvoicesDetected": { + "name": "totalInvoicesDetected", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "processingStatus": { + "name": "processingStatus", + "type": "enum('processing','completed','error')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'processing'" + }, + "processingProgress": { + "name": "processingProgress", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "sourceFiles_id": { + "name": "sourceFiles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "userSettings": { + "name": "userSettings", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "llmModel": { + "name": "llmModel", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'mistral-large-latest'" + }, + "orderNumberFormat": { + "name": "orderNumberFormat", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "invoiceNumberKeywords": { + "name": "invoiceNumberKeywords", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deliveryNoteKeywords": { + "name": "deliveryNoteKeywords", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "orderNumberKeywords": { + "name": "orderNumberKeywords", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "supplierKeywords": { + "name": "supplierKeywords", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totalAmountKeywords": { + "name": "totalAmountKeywords", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "subscriptionKeywords": { + "name": "subscriptionKeywords", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sftpHost": { + "name": "sftpHost", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sftpPort": { + "name": "sftpPort", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 22 + }, + "sftpUsername": { + "name": "sftpUsername", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sftpPassword": { + "name": "sftpPassword", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sftpRemotePath": { + "name": "sftpRemotePath", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'/'" + }, + "sftpAutoExport": { + "name": "sftpAutoExport", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "llmLogsRetentionMonths": { + "name": "llmLogsRetentionMonths", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 3 + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "userSettings_id": { + "name": "userSettings_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "userSettings_userId_unique": { + "name": "userSettings_userId_unique", + "columns": [ + "userId" + ] + } + }, + "checkConstraint": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "openId": { + "name": "openId", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "azureAdId": { + "name": "azureAdId", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "varchar(320)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "passwordHash": { + "name": "passwordHash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "loginMethod": { + "name": "loginMethod", + "type": "enum('manus','local','azure-ad')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "enum('user','admin')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'user'" + }, + "isActive": { + "name": "isActive", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + }, + "lastSignedIn": { + "name": "lastSignedIn", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "users_id": { + "name": "users_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "users_openId_unique": { + "name": "users_openId_unique", + "columns": [ + "openId" + ] + }, + "users_azureAdId_unique": { + "name": "users_azureAdId_unique", + "columns": [ + "azureAdId" + ] + }, + "users_email_unique": { + "name": "users_email_unique", + "columns": [ + "email" + ] + } + }, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 2a8ae59..6380b99 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -64,6 +64,13 @@ "when": 1770830684846, "tag": "0008_orange_prism", "breakpoints": true + }, + { + "idx": 9, + "version": "5", + "when": 1770831052283, + "tag": "0009_regular_warhawk", + "breakpoints": true } ] } \ No newline at end of file diff --git a/drizzle/schema.ts b/drizzle/schema.ts index 45e5075..b6f0e98 100644 --- a/drizzle/schema.ts +++ b/drizzle/schema.ts @@ -253,3 +253,29 @@ export const accountingAllocationList = mysqlTable("accountingAllocationList", { export type AccountingAllocation = typeof accountingAllocationList.$inferSelect; export type InsertAccountingAllocation = typeof accountingAllocationList.$inferInsert; + +/** + * Automation rules table for automatic field filling based on conditions + */ +export const automationRules = mysqlTable("automationRules", { + id: int("id").autoincrement().primaryKey(), + userId: int("userId").notNull(), // Each user has their own rules + name: varchar("name", { length: 255 }).notNull(), // Rule name for identification + isActive: int("isActive").default(1).notNull(), // 0 = disabled, 1 = enabled + priority: int("priority").default(0).notNull(), // Execution order (lower = higher priority) + + // Conditions (IF) - JSON array of condition objects + // Example: [{"field": "supplierName", "operator": "contains", "value": "Microsoft"}, {"field": "totalAmount", "operator": ">", "value": "1000"}] + conditions: text("conditions").notNull(), // JSON string + conditionsLogic: mysqlEnum("conditionsLogic", ["AND", "OR"]).default("AND").notNull(), // How to combine conditions + + // Actions (THEN) - JSON object with field assignments + // Example: {"typeAchat": "CAPEX", "serviceConcerne": "DSI", "ventilationComptable": "TOUS"} + actions: text("actions").notNull(), // JSON string + + createdAt: timestamp("createdAt").defaultNow().notNull(), + updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(), +}); + +export type AutomationRule = typeof automationRules.$inferSelect; +export type InsertAutomationRule = typeof automationRules.$inferInsert; diff --git a/server/automationEngine.ts b/server/automationEngine.ts new file mode 100644 index 0000000..56c2f97 --- /dev/null +++ b/server/automationEngine.ts @@ -0,0 +1,137 @@ +import { getAutomationRulesByUser } from "./db"; +import { Invoice } from "../drizzle/schema"; + +interface Condition { + field: string; + operator: string; + value: string; +} + +interface Actions { + typeAchat?: string; + serviceConcerne?: string; + ventilationComptable?: string; +} + +/** + * Evaluate a single condition against an invoice + */ +function evaluateCondition(invoice: Invoice, condition: Condition): boolean { + const fieldValue = invoice[condition.field as keyof Invoice]; + + if (fieldValue === null || fieldValue === undefined) { + return false; + } + + const valueStr = String(fieldValue).toLowerCase(); + const conditionValue = condition.value.toLowerCase(); + + switch (condition.operator) { + case "contains": + return valueStr.includes(conditionValue); + case "equals": + return valueStr === conditionValue; + case "startsWith": + return valueStr.startsWith(conditionValue); + case "endsWith": + return valueStr.endsWith(conditionValue); + case ">": + return Number(fieldValue) > Number(condition.value); + case "<": + return Number(fieldValue) < Number(condition.value); + case ">=": + return Number(fieldValue) >= Number(condition.value); + case "<=": + return Number(fieldValue) <= Number(condition.value); + default: + return false; + } +} + +/** + * Evaluate all conditions for a rule based on the logic (AND/OR) + */ +function evaluateConditions( + invoice: Invoice, + conditions: Condition[], + logic: "AND" | "OR" +): boolean { + if (conditions.length === 0) { + return false; + } + + if (logic === "AND") { + return conditions.every((condition) => evaluateCondition(invoice, condition)); + } else { + return conditions.some((condition) => evaluateCondition(invoice, condition)); + } +} + +/** + * Apply automation rules to an invoice and return the fields to update + * Returns an object with the fields that should be updated based on matching rules + */ +export async function applyAutomationRules( + userId: number, + invoice: Invoice +): Promise> { + const rules = await getAutomationRulesByUser(userId); + const updates: Partial = {}; + + // Process rules in priority order (already sorted by priority in getAutomationRulesByUser) + for (const rule of rules) { + // Skip inactive rules + if (rule.isActive !== 1) { + continue; + } + + try { + const conditions: Condition[] = JSON.parse(rule.conditions); + const actions: Actions = JSON.parse(rule.actions); + + // Evaluate conditions + const conditionsMatch = evaluateConditions( + invoice, + conditions, + rule.conditionsLogic + ); + + if (conditionsMatch) { + // Apply actions (only if the field is not already set by a higher priority rule) + if (actions.typeAchat && !updates.typeAchat) { + updates.typeAchat = actions.typeAchat as "CAPEX" | "OPEX"; + } + if (actions.serviceConcerne && !updates.serviceConcerne) { + updates.serviceConcerne = actions.serviceConcerne; + } + if (actions.ventilationComptable && !updates.ventilationComptable) { + updates.ventilationComptable = actions.ventilationComptable; + } + } + } catch (error) { + console.error(`[AutomationEngine] Error processing rule ${rule.id}:`, error); + // Continue with next rule on error + } + } + + return updates; +} + +/** + * Apply automation rules to multiple invoices + */ +export async function applyAutomationRulesToBatch( + userId: number, + invoices: Invoice[] +): Promise>> { + const results = new Map>(); + + for (const invoice of invoices) { + const updates = await applyAutomationRules(userId, invoice); + if (Object.keys(updates).length > 0) { + results.set(invoice.id, updates); + } + } + + return results; +} diff --git a/server/db.ts b/server/db.ts index a76877d..efd07e2 100644 --- a/server/db.ts +++ b/server/db.ts @@ -26,7 +26,10 @@ import { Department, accountingAllocationList, InsertAccountingAllocation, - AccountingAllocation + AccountingAllocation, + automationRules, + InsertAutomationRule, + AutomationRule } from "../drizzle/schema"; import { ENV } from './_core/env'; @@ -529,6 +532,44 @@ export async function deleteAccountingAllocation(id: number): Promise { await db.delete(accountingAllocationList).where(eq(accountingAllocationList.id, id)); } +// ============= AUTOMATION RULES OPERATIONS ============= + +export async function getAutomationRulesByUser(userId: number): Promise { + const db = await getDb(); + if (!db) return []; + return await db.select().from(automationRules).where(eq(automationRules.userId, userId)).orderBy(automationRules.priority); +} + +export async function getAutomationRuleById(id: number): Promise { + const db = await getDb(); + if (!db) return null; + const result = await db.select().from(automationRules).where(eq(automationRules.id, id)).limit(1); + return result[0] || null; +} + +export async function createAutomationRule(data: InsertAutomationRule): Promise { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + const result = await db.insert(automationRules).values(data); + const insertedId = result[0].insertId; + const newRule = await getAutomationRuleById(insertedId); + return newRule!; +} + +export async function updateAutomationRule(id: number, data: Partial): Promise { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + await db.update(automationRules).set(data).where(eq(automationRules.id, id)); + const updated = await getAutomationRuleById(id); + return updated!; +} + +export async function deleteAutomationRule(id: number): Promise { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + await db.delete(automationRules).where(eq(automationRules.id, id)); +} + // ============= INITIALIZE DEFAULT VALUES ============= export async function initializeDefaultLists(userId: number): Promise { diff --git a/server/routers.ts b/server/routers.ts index 63fe684..1c4af5c 100644 --- a/server/routers.ts +++ b/server/routers.ts @@ -36,6 +36,11 @@ import { createAccountingAllocation, deleteAccountingAllocation, initializeDefaultLists, + getAutomationRulesByUser, + getAutomationRuleById, + createAutomationRule, + updateAutomationRule, + deleteAutomationRule, } from "./db"; import { loginLocal, hashPassword, getAzureAuthUrl, isAzureAdConfigured, generateToken } from "./auth"; import { extractInvoicesWithMistral, generateMetadataJSON } from "./invoiceExtractor"; @@ -220,7 +225,7 @@ export const appRouter = router({ ); // Create invoice record - await createInvoice({ + const newInvoice = await createInvoice({ userId, sourceFileId: sourceFile.id, invoiceIndexInFile: i + 1, @@ -242,6 +247,20 @@ export const appRouter = router({ status: "completed", }); + // Apply automation rules to the newly created invoice + try { + const { applyAutomationRules } = await import("./automationEngine"); + const automationUpdates = await applyAutomationRules(userId, newInvoice); + + // If automation rules suggest updates, apply them + if (Object.keys(automationUpdates).length > 0) { + await updateInvoice(newInvoice.id, automationUpdates); + } + } catch (autoError) { + console.error("[Automation] Error applying rules:", autoError); + // Don't fail the import if automation fails + } + importedCount++; } catch (error: any) { errorsCount++; @@ -753,6 +772,63 @@ export const appRouter = router({ return { success: true }; }), }), + + // ============= AUTOMATION RULES ROUTES ============= + automationRules: router({ + list: protectedProcedure.query(async ({ ctx }) => { + return await getAutomationRulesByUser(ctx.user.id); + }), + + getById: protectedProcedure + .input(z.object({ + id: z.number(), + })) + .query(async ({ input }) => { + return await getAutomationRuleById(input.id); + }), + + create: protectedProcedure + .input(z.object({ + name: z.string().min(1), + isActive: z.number().min(0).max(1).optional(), + priority: z.number().optional(), + conditions: z.string(), // JSON string + conditionsLogic: z.enum(["AND", "OR"]).optional(), + actions: z.string(), // JSON string + })) + .mutation(async ({ input, ctx }) => { + const rule = await createAutomationRule({ + userId: ctx.user.id, + ...input, + }); + return rule; + }), + + update: protectedProcedure + .input(z.object({ + id: z.number(), + name: z.string().min(1).optional(), + isActive: z.number().min(0).max(1).optional(), + priority: z.number().optional(), + conditions: z.string().optional(), + conditionsLogic: z.enum(["AND", "OR"]).optional(), + actions: z.string().optional(), + })) + .mutation(async ({ input }) => { + const { id, ...data } = input; + const updated = await updateAutomationRule(id, data); + return updated; + }), + + delete: protectedProcedure + .input(z.object({ + id: z.number(), + })) + .mutation(async ({ input }) => { + await deleteAutomationRule(input.id); + return { success: true }; + }), + }), }); export type AppRouter = typeof appRouter; diff --git a/todo.md b/todo.md index 35b154e..71d0aea 100644 --- a/todo.md +++ b/todo.md @@ -289,3 +289,17 @@ - [x] Pousser la migration avec pnpm db:push - [x] Ajouter le champ dans l'interface ImportSettings.tsx - [x] Tester la sauvegarde et récupération du paramètre + +## Système de gestion des automatismes +- [x] Créer la table automationRules dans le schéma pour stocker les règles +- [x] Définir la structure JSON pour les conditions (SI) et actions (ALORS) +- [x] Pousser la migration avec pnpm db:push +- [x] Créer les fonctions CRUD dans db.ts pour les règles +- [x] Créer les routes tRPC pour gérer les règles (list, create, update, delete, reorder) +- [x] Créer la page AutomationRules.tsx pour l'interface de gestion +- [x] Ajouter le menu "Automatismes" dans DashboardLayout +- [x] Implémenter le formulaire de création/édition de règles +- [x] Implémenter l'affichage de la liste des règles avec ordre de priorité +- [x] Créer la fonction d'évaluation des règles (applyAutomationRules) +- [x] Intégrer l'application des règles dans le processus d'import +- [x] Tester avec des règles réelles
Gérez les règles de remplissage automatique des champs