Nouvelle fonctionnalité visuelle : ✅ Valeurs remplies automatiquement par les règles : affichées en VERT ✅ Valeurs saisies manuellement : affichées en BLEU ✅ Traçage intelligent de l'origine des valeurs (automatique ou manuelle) ✅ Mise à jour automatique de la couleur lors d'une modification manuelle Architecture technique : - drizzle/schema.ts : Ajout du champ autoFilledFields (JSON) pour tracer l'origine - server/automationEngine.ts : Enregistrement des champs remplis automatiquement - server/routers.ts : Ajout de autoFilledFields dans le schéma d'update - client/src/pages/InvoicesBAP.tsx : Fonction getFieldColor() pour déterminer la couleur - Migration 0010_late_thor_girl.sql appliquée avec succès Fonctionnement : 1. **Import de facture** : Les règles d'automatisme s'appliquent et marquent les champs remplis dans autoFilledFields 2. **Affichage** : La fonction getFieldColor() analyse autoFilledFields pour déterminer la couleur : - Champ dans autoFilledFields → VERT (rempli automatiquement) - Champ avec valeur mais pas dans autoFilledFields → BLEU (rempli manuellement) - Champ vide → Pas de couleur 3. **Modification manuelle** : Lors d'une édition, le champ est retiré de autoFilledFields et passe en BLEU Bénéfices utilisateur : - Visibilité immédiate sur l'origine des données - Confiance accrue dans les automatismes (vert = validé par règle) - Identification rapide des saisies manuelles nécessitant vérification - Traçabilité complète du remplissage des champs métier Cette fonctionnalité améliore considérablement la transparence du système d'automatisation et facilite le contrôle qualité des données.
147 lines
4.1 KiB
TypeScript
147 lines
4.1 KiB
TypeScript
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<Partial<Invoice>> {
|
|
const rules = await getAutomationRulesByUser(userId);
|
|
const updates: Partial<Invoice> = {};
|
|
const autoFilledFieldsList: string[] = [];
|
|
|
|
// 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";
|
|
autoFilledFieldsList.push("typeAchat");
|
|
}
|
|
if (actions.serviceConcerne && !updates.serviceConcerne) {
|
|
updates.serviceConcerne = actions.serviceConcerne;
|
|
autoFilledFieldsList.push("serviceConcerne");
|
|
}
|
|
if (actions.ventilationComptable && !updates.ventilationComptable) {
|
|
updates.ventilationComptable = actions.ventilationComptable;
|
|
autoFilledFieldsList.push("ventilationComptable");
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error(`[AutomationEngine] Error processing rule ${rule.id}:`, error);
|
|
// Continue with next rule on error
|
|
}
|
|
}
|
|
|
|
// Add the list of auto-filled fields to the updates
|
|
if (autoFilledFieldsList.length > 0) {
|
|
updates.autoFilledFields = JSON.stringify(autoFilledFieldsList);
|
|
}
|
|
|
|
return updates;
|
|
}
|
|
|
|
/**
|
|
* Apply automation rules to multiple invoices
|
|
*/
|
|
export async function applyAutomationRulesToBatch(
|
|
userId: number,
|
|
invoices: Invoice[]
|
|
): Promise<Map<number, Partial<Invoice>>> {
|
|
const results = new Map<number, Partial<Invoice>>();
|
|
|
|
for (const invoice of invoices) {
|
|
const updates = await applyAutomationRules(userId, invoice);
|
|
if (Object.keys(updates).length > 0) {
|
|
results.set(invoice.id, updates);
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}
|