diff --git a/client/src/pages/AutomationRules.tsx b/client/src/pages/AutomationRules.tsx index 22e93f0..dadac49 100644 --- a/client/src/pages/AutomationRules.tsx +++ b/client/src/pages/AutomationRules.tsx @@ -34,7 +34,7 @@ import { import { Badge } from "@/components/ui/badge"; import { trpc } from "@/lib/trpc"; import { toast } from "sonner"; -import { Plus, Edit, Trash2, Power, PowerOff } from "lucide-react"; +import { Plus, Edit, Trash2, Power, PowerOff, Copy } from "lucide-react"; interface Condition { field: string; @@ -58,6 +58,10 @@ export default function AutomationRules() { 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); // Form state const [ruleName, setRuleName] = useState(""); @@ -109,6 +113,26 @@ export default function AutomationRules() { }, }); + 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(""); @@ -229,10 +253,16 @@ export default function AutomationRules() {

Automatismes

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

- +
+ + +
@@ -309,13 +339,23 @@ export default function AutomationRules() { variant="ghost" size="sm" onClick={() => openEditDialog(rule)} + title="Modifier" > + @@ -491,13 +531,32 @@ export default function AutomationRules() { - - - +
+ + +
@@ -519,6 +578,68 @@ export default function AutomationRules() { + + {/* 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}
} +
+
+ ))} +
+
+
+
+ )} +
+ )} + + + +
+
); diff --git a/server/routers.ts b/server/routers.ts index 5b09c20..9fc754f 100644 --- a/server/routers.ts +++ b/server/routers.ts @@ -1,5 +1,17 @@ import { z } from "zod"; import { COOKIE_NAME } from "@shared/const"; + +interface Condition { + field: string; + operator: string; + value: string; +} + +interface Actions { + typeAchat?: string; + serviceConcerne?: string; + ventilationComptable?: string; +} import { getSessionCookieOptions } from "./_core/cookies"; import { systemRouter } from "./_core/systemRouter"; import { publicProcedure, protectedProcedure, router } from "./_core/trpc"; @@ -829,6 +841,120 @@ export const appRouter = router({ await deleteAutomationRule(input.id); return { success: true }; }), + + test: protectedProcedure + .input(z.object({ + conditions: z.string(), // JSON string + conditionsLogic: z.enum(["AND", "OR"]), + actions: z.string(), // JSON string + })) + .mutation(async ({ input, ctx }) => { + // Import applyAutomationRules from automationEngine + const { applyAutomationRules } = await import("./automationEngine"); + + // Get all user's invoices + const invoices = await getInvoicesByUser(ctx.user.id); + + // Create a temporary rule object + const tempRule = { + id: -1, + userId: ctx.user.id, + name: "Test Rule", + isActive: 1, + priority: 999, + conditions: input.conditions, + conditionsLogic: input.conditionsLogic, + actions: input.actions, + createdAt: new Date(), + updatedAt: new Date(), + }; + + // Test the rule on each invoice and count matches + let affectedCount = 0; + const affectedInvoices = []; + + for (const invoice of invoices) { + // Manually evaluate the rule for testing + const conditions: Condition[] = JSON.parse(input.conditions); + const actionsObj: Actions = JSON.parse(input.actions); + + // Evaluate conditions + let conditionsMatch = true; + if (input.conditionsLogic === "AND") { + conditionsMatch = conditions.every(cond => { + const fieldValue = invoice[cond.field as keyof typeof invoice]; + if (!fieldValue) return false; + const valueStr = String(fieldValue).toLowerCase(); + const condValue = cond.value.toLowerCase(); + if (cond.operator === "contains") return valueStr.includes(condValue); + if (cond.operator === "equals") return valueStr === condValue; + if (cond.operator === "startsWith") return valueStr.startsWith(condValue); + if (cond.operator === "endsWith") return valueStr.endsWith(condValue); + if (cond.operator === ">") return Number(fieldValue) > Number(cond.value); + if (cond.operator === "<") return Number(fieldValue) < Number(cond.value); + if (cond.operator === ">=") return Number(fieldValue) >= Number(cond.value); + if (cond.operator === "<=") return Number(fieldValue) <= Number(cond.value); + return false; + }); + } else { + conditionsMatch = conditions.some(cond => { + const fieldValue = invoice[cond.field as keyof typeof invoice]; + if (!fieldValue) return false; + const valueStr = String(fieldValue).toLowerCase(); + const condValue = cond.value.toLowerCase(); + if (cond.operator === "contains") return valueStr.includes(condValue); + if (cond.operator === "equals") return valueStr === condValue; + if (cond.operator === "startsWith") return valueStr.startsWith(condValue); + if (cond.operator === "endsWith") return valueStr.endsWith(condValue); + if (cond.operator === ">") return Number(fieldValue) > Number(cond.value); + if (cond.operator === "<") return Number(fieldValue) < Number(cond.value); + if (cond.operator === ">=") return Number(fieldValue) >= Number(cond.value); + if (cond.operator === "<=") return Number(fieldValue) <= Number(cond.value); + return false; + }); + } + + if (conditionsMatch) { + affectedCount++; + affectedInvoices.push({ + id: invoice.id, + supplierName: invoice.supplierName, + invoiceNumber: invoice.invoiceNumber, + changes: actionsObj, + }); + } + } + + return { + totalInvoices: invoices.length, + affectedCount, + affectedInvoices: affectedInvoices.slice(0, 10), // Return first 10 for preview + }; + }), + + duplicate: protectedProcedure + .input(z.object({ + id: z.number(), + })) + .mutation(async ({ input, ctx }) => { + const original = await getAutomationRuleById(input.id); + if (!original || original.userId !== ctx.user.id) { + throw new TRPCError({ code: "NOT_FOUND" }); + } + + // Create a copy with modified name + const duplicate = await createAutomationRule({ + userId: ctx.user.id, + name: `${original.name} (copie)`, + isActive: 0, // Inactive by default + priority: original.priority, + conditions: original.conditions, + conditionsLogic: original.conditionsLogic, + actions: original.actions, + }); + + return duplicate; + }), }), }); diff --git a/todo.md b/todo.md index e02442a..8abb834 100644 --- a/todo.md +++ b/todo.md @@ -316,3 +316,12 @@ - [x] Identifier tous les Select.Item avec valeurs vides dans AutomationRules.tsx - [x] Remplacer les valeurs vides par des valeurs par défaut valides - [x] Tester la création de nouvelles règles + +## Améliorations de la gestion des règles d'automatisme +- [x] Créer une route tRPC pour tester une règle sur les factures existantes +- [x] Créer une route tRPC pour dupliquer une règle +- [x] Implémenter l'assistant de création guidée avec étapes progressives +- [x] Ajouter le bouton "Tester" dans le formulaire de création/édition +- [x] Afficher les résultats du test (nombre de factures affectées) +- [x] Ajouter le bouton "Dupliquer" dans la liste des règles +- [x] Tester toutes les nouvelles fonctionnalités