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 { toast } from "sonner";
|
||||||
import { useLocation } from "wouter";
|
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() {
|
export default function InvoicesBAP() {
|
||||||
const [, setLocation] = useLocation();
|
const [, setLocation] = useLocation();
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
@@ -456,13 +473,19 @@ export default function InvoicesBAP() {
|
|||||||
setAddDialogOpen(true);
|
setAddDialogOpen(true);
|
||||||
e.target.value = invoice.serviceConcerne || "";
|
e.target.value = invoice.serviceConcerne || "";
|
||||||
} else {
|
} 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({
|
updateFieldMutation.mutate({
|
||||||
id: invoice.id,
|
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>
|
<option value="">-</option>
|
||||||
{departments?.map((dept) => (
|
{departments?.map((dept) => (
|
||||||
@@ -475,12 +498,18 @@ export default function InvoicesBAP() {
|
|||||||
<select
|
<select
|
||||||
value={invoice.typeAchat || ""}
|
value={invoice.typeAchat || ""}
|
||||||
onChange={(e) => {
|
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({
|
updateFieldMutation.mutate({
|
||||||
id: invoice.id,
|
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="">-</option>
|
||||||
<option value="CAPEX">CAPEX</option>
|
<option value="CAPEX">CAPEX</option>
|
||||||
@@ -497,13 +526,19 @@ export default function InvoicesBAP() {
|
|||||||
setAddDialogOpen(true);
|
setAddDialogOpen(true);
|
||||||
e.target.value = invoice.ventilationComptable || "";
|
e.target.value = invoice.ventilationComptable || "";
|
||||||
} else {
|
} 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({
|
updateFieldMutation.mutate({
|
||||||
id: invoice.id,
|
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>
|
<option value="">-</option>
|
||||||
{allocations?.map((alloc) => (
|
{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,
|
"when": 1770831052283,
|
||||||
"tag": "0009_regular_warhawk",
|
"tag": "0009_regular_warhawk",
|
||||||
"breakpoints": true
|
"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
|
typeAchat: mysqlEnum("typeAchat", ["CAPEX", "OPEX"]), // Type d'achat
|
||||||
ventilationComptable: varchar("ventilationComptable", { length: 100 }), // Ventilation comptable (TOUS, PA, HEP, etc.)
|
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
|
// Extracted text from PDF
|
||||||
extractedText: text("extractedText"), // Full text extracted from the invoice PDF
|
extractedText: text("extractedText"), // Full text extracted from the invoice PDF
|
||||||
|
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ export async function applyAutomationRules(
|
|||||||
): Promise<Partial<Invoice>> {
|
): Promise<Partial<Invoice>> {
|
||||||
const rules = await getAutomationRulesByUser(userId);
|
const rules = await getAutomationRulesByUser(userId);
|
||||||
const updates: Partial<Invoice> = {};
|
const updates: Partial<Invoice> = {};
|
||||||
|
const autoFilledFieldsList: string[] = [];
|
||||||
|
|
||||||
// Process rules in priority order (already sorted by priority in getAutomationRulesByUser)
|
// Process rules in priority order (already sorted by priority in getAutomationRulesByUser)
|
||||||
for (const rule of rules) {
|
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)
|
// Apply actions (only if the field is not already set by a higher priority rule)
|
||||||
if (actions.typeAchat && !updates.typeAchat) {
|
if (actions.typeAchat && !updates.typeAchat) {
|
||||||
updates.typeAchat = actions.typeAchat as "CAPEX" | "OPEX";
|
updates.typeAchat = actions.typeAchat as "CAPEX" | "OPEX";
|
||||||
|
autoFilledFieldsList.push("typeAchat");
|
||||||
}
|
}
|
||||||
if (actions.serviceConcerne && !updates.serviceConcerne) {
|
if (actions.serviceConcerne && !updates.serviceConcerne) {
|
||||||
updates.serviceConcerne = actions.serviceConcerne;
|
updates.serviceConcerne = actions.serviceConcerne;
|
||||||
|
autoFilledFieldsList.push("serviceConcerne");
|
||||||
}
|
}
|
||||||
if (actions.ventilationComptable && !updates.ventilationComptable) {
|
if (actions.ventilationComptable && !updates.ventilationComptable) {
|
||||||
updates.ventilationComptable = actions.ventilationComptable;
|
updates.ventilationComptable = actions.ventilationComptable;
|
||||||
|
autoFilledFieldsList.push("ventilationComptable");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} 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;
|
return updates;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -330,6 +330,7 @@ export const appRouter = router({
|
|||||||
typeAchat: z.enum(["CAPEX", "OPEX"]).optional(),
|
typeAchat: z.enum(["CAPEX", "OPEX"]).optional(),
|
||||||
ventilationComptable: z.string().optional(),
|
ventilationComptable: z.string().optional(),
|
||||||
isSubscription: z.number().min(0).max(1).optional(),
|
isSubscription: z.number().min(0).max(1).optional(),
|
||||||
|
autoFilledFields: z.string().nullable().optional(),
|
||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
.mutation(async ({ input, ctx }) => {
|
.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] Créer la fonction d'évaluation des règles (applyAutomationRules)
|
||||||
- [x] Intégrer l'application des règles dans le processus d'import
|
- [x] Intégrer l'application des règles dans le processus d'import
|
||||||
- [x] Tester avec des règles réelles
|
- [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