Checkpoint: Coloration des valeurs dans Factures BAP (automatique vs manuelle)

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.
This commit is contained in:
Manus
2026-02-11 12:39:22 -05:00
parent 72c3f503a3
commit 716b259fe4
8 changed files with 1281 additions and 6 deletions

View File

@@ -37,6 +37,23 @@ import * as XLSX from 'xlsx';
import { toast } from "sonner";
import { useLocation } from "wouter";
// Helper function to determine field color based on origin
const getFieldColor = (invoice: any, fieldName: string): string => {
try {
const autoFilledFields = invoice.autoFilledFields ? JSON.parse(invoice.autoFilledFields) : [];
if (autoFilledFields.includes(fieldName)) {
return "text-green-600 font-semibold"; // Auto-filled by rules
}
// If field has a value but is not auto-filled, it's manual
if (invoice[fieldName]) {
return "text-blue-600 font-semibold"; // Manually filled
}
} catch (error) {
console.error("Error parsing autoFilledFields:", error);
}
return ""; // No color for empty fields
};
export default function InvoicesBAP() {
const [, setLocation] = useLocation();
const [searchQuery, setSearchQuery] = useState("");
@@ -456,13 +473,19 @@ export default function InvoicesBAP() {
setAddDialogOpen(true);
e.target.value = invoice.serviceConcerne || "";
} else {
// Remove field from autoFilledFields when manually edited
const autoFilledFields = invoice.autoFilledFields ? JSON.parse(invoice.autoFilledFields) : [];
const updatedAutoFields = autoFilledFields.filter((f: string) => f !== "serviceConcerne");
updateFieldMutation.mutate({
id: invoice.id,
data: { serviceConcerne: e.target.value || undefined },
data: {
serviceConcerne: e.target.value || undefined,
autoFilledFields: updatedAutoFields.length > 0 ? JSON.stringify(updatedAutoFields) : null
},
});
}
}}
className="w-full px-2 py-1 border rounded text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
className={`w-full px-2 py-1 border rounded text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 ${getFieldColor(invoice, "serviceConcerne")}`}
>
<option value="">-</option>
{departments?.map((dept) => (
@@ -475,12 +498,18 @@ export default function InvoicesBAP() {
<select
value={invoice.typeAchat || ""}
onChange={(e) => {
// Remove field from autoFilledFields when manually edited
const autoFilledFields = invoice.autoFilledFields ? JSON.parse(invoice.autoFilledFields) : [];
const updatedAutoFields = autoFilledFields.filter((f: string) => f !== "typeAchat");
updateFieldMutation.mutate({
id: invoice.id,
data: { typeAchat: e.target.value as "CAPEX" | "OPEX" | undefined },
data: {
typeAchat: e.target.value as "CAPEX" | "OPEX" | undefined,
autoFilledFields: updatedAutoFields.length > 0 ? JSON.stringify(updatedAutoFields) : null
},
});
}}
className="w-full px-2 py-1 border rounded text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
className={`w-full px-2 py-1 border rounded text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 ${getFieldColor(invoice, "typeAchat")}`}
>
<option value="">-</option>
<option value="CAPEX">CAPEX</option>
@@ -497,13 +526,19 @@ export default function InvoicesBAP() {
setAddDialogOpen(true);
e.target.value = invoice.ventilationComptable || "";
} else {
// Remove field from autoFilledFields when manually edited
const autoFilledFields = invoice.autoFilledFields ? JSON.parse(invoice.autoFilledFields) : [];
const updatedAutoFields = autoFilledFields.filter((f: string) => f !== "ventilationComptable");
updateFieldMutation.mutate({
id: invoice.id,
data: { ventilationComptable: e.target.value || undefined },
data: {
ventilationComptable: e.target.value || undefined,
autoFilledFields: updatedAutoFields.length > 0 ? JSON.stringify(updatedAutoFields) : null
},
});
}
}}
className="w-full px-2 py-1 border rounded text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
className={`w-full px-2 py-1 border rounded text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 ${getFieldColor(invoice, "ventilationComptable")}`}
>
<option value="">-</option>
{allocations?.map((alloc) => (

View File

@@ -0,0 +1 @@
ALTER TABLE `invoices` ADD `autoFilledFields` text;

File diff suppressed because it is too large Load Diff

View File

@@ -71,6 +71,13 @@
"when": 1770831052283,
"tag": "0009_regular_warhawk",
"breakpoints": true
},
{
"idx": 10,
"version": "5",
"when": 1770831408967,
"tag": "0010_late_thor_girl",
"breakpoints": true
}
]
}

View File

@@ -89,6 +89,10 @@ export const invoices = mysqlTable("invoices", {
typeAchat: mysqlEnum("typeAchat", ["CAPEX", "OPEX"]), // Type d'achat
ventilationComptable: varchar("ventilationComptable", { length: 100 }), // Ventilation comptable (TOUS, PA, HEP, etc.)
// Automation tracking - JSON array of field names that were auto-filled by automation rules
// Example: ["typeAchat", "serviceConcerne", "ventilationComptable"]
autoFilledFields: text("autoFilledFields"), // JSON string array
// Extracted text from PDF
extractedText: text("extractedText"), // Full text extracted from the invoice PDF

View File

@@ -77,6 +77,7 @@ export async function applyAutomationRules(
): 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) {
@@ -100,12 +101,15 @@ export async function applyAutomationRules(
// 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) {
@@ -114,6 +118,11 @@ export async function applyAutomationRules(
}
}
// Add the list of auto-filled fields to the updates
if (autoFilledFieldsList.length > 0) {
updates.autoFilledFields = JSON.stringify(autoFilledFieldsList);
}
return updates;
}

View File

@@ -330,6 +330,7 @@ export const appRouter = router({
typeAchat: z.enum(["CAPEX", "OPEX"]).optional(),
ventilationComptable: z.string().optional(),
isSubscription: z.number().min(0).max(1).optional(),
autoFilledFields: z.string().nullable().optional(),
}),
}))
.mutation(async ({ input, ctx }) => {

View File

@@ -303,3 +303,11 @@
- [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
## Coloration des valeurs dans Factures BAP
- [x] Ajouter les champs de traçage (autoFilledFields) dans la table invoices
- [x] Pousser la migration avec pnpm db:push
- [x] Mettre à jour automationEngine.ts pour marquer les champs remplis automatiquement
- [x] Modifier InvoicesBAP.tsx pour afficher les valeurs en vert (auto) et bleu (manuel)
- [x] Mettre à jour la logique d'édition pour marquer les valeurs comme manuelles
- [x] Tester l'affichage avec des règles d'automatisme