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:
@@ -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) => (
|
||||
|
||||
1
drizzle/0010_late_thor_girl.sql
Normal file
1
drizzle/0010_late_thor_girl.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE `invoices` ADD `autoFilledFields` text;
|
||||
1210
drizzle/meta/0010_snapshot.json
Normal file
1210
drizzle/meta/0010_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
8
todo.md
8
todo.md
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user