Checkpoint: Améliorations majeures de la gestion des règles d'automatisme
Trois nouvelles fonctionnalités implémentées : ✅ **Assistant de création guidée** - Deux modes de création : "Assistant de création" (guidé) et "Mode avancé" (complet) - Interface simplifiée pour les utilisateurs non techniques - Boutons séparés pour choisir le mode de création ✅ **Test de règles avant activation** - Bouton "Tester la règle" dans le formulaire de création/édition - Simulation sur toutes les factures existantes sans modification - Affichage des résultats : * Nombre total de factures * Nombre de factures affectées * Aperçu des 10 premières factures avec les modifications prévues - Permet de valider l'impact avant d'activer la règle ✅ **Duplication de règles** - Bouton "Dupliquer" (icône Copy bleue) dans la liste des règles - Création automatique d'une copie avec le suffixe "(copie)" - Règle dupliquée désactivée par défaut pour éviter les conflits - Facilite la création de variantes de règles similaires Architecture technique : - server/routers.ts : Ajout des routes `test` et `duplicate` dans automationRules - server/routers.ts : Ajout des interfaces Condition et Actions - server/routers.ts : Implémentation de la logique d'évaluation des conditions pour le test - client/src/pages/AutomationRules.tsx : Ajout des mutations testMutation et duplicateMutation - client/src/pages/AutomationRules.tsx : Ajout des états wizardMode, wizardStep, testResultsOpen, testResults - client/src/pages/AutomationRules.tsx : Modification des boutons de création (Assistant vs Mode avancé) - client/src/pages/AutomationRules.tsx : Ajout du bouton "Tester la règle" dans le DialogFooter - client/src/pages/AutomationRules.tsx : Ajout du Dialog des résultats de test avec statistiques et aperçu - client/src/pages/AutomationRules.tsx : Ajout du bouton "Dupliquer" dans le tableau des règles Bénéfices utilisateur : - **Sécurité** : Test avant activation pour éviter les erreurs de configuration - **Productivité** : Duplication rapide pour créer des variantes - **Accessibilité** : Assistant guidé pour les utilisateurs non techniques - **Transparence** : Visualisation de l'impact réel avant application - **Confiance** : Validation des règles sur données réelles Ces améliorations transforment la gestion des automatismes en un outil puissant, sécurisé et accessible à tous les profils d'utilisateurs.
This commit is contained in:
@@ -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;
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user