From e80f899498be55cb7e8981626fe8b8b0bb6ee2cb Mon Sep 17 00:00:00 2001 From: Manus Date: Sun, 12 Apr 2026 15:11:24 -0400 Subject: [PATCH] =?UTF-8?q?Checkpoint:=20Apprentissage=20d=C3=A9clench?= =?UTF-8?q?=C3=A9=20depuis=20les=20selects=20serviceConcerne/typeAchat/ven?= =?UTF-8?q?tilation=20dans=20la=20liste=20Factures=20BAP.=20Page=20Apprent?= =?UTF-8?q?issages=20IA=20refaite=20avec=20indicateur=20de=20confiance=20(?= =?UTF-8?q?Confirm=C3=A9/En=20observation)=20et=20seuil=20configurable.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/pages/InvoicesBAP.tsx | 38 +++- client/src/pages/LearningSettings.tsx | 263 ++++++++++++++++++-------- 2 files changed, 223 insertions(+), 78 deletions(-) diff --git a/client/src/pages/InvoicesBAP.tsx b/client/src/pages/InvoicesBAP.tsx index d5f719c..3e528dc 100644 --- a/client/src/pages/InvoicesBAP.tsx +++ b/client/src/pages/InvoicesBAP.tsx @@ -158,6 +158,8 @@ export default function InvoicesBAP() { }, }); + const upsertLearningMutation = trpc.learnings.upsert.useMutation(); + const reprocessMutation = trpc.invoices.reprocessSelected.useMutation({ onSuccess: (data) => { if (data.errors > 0) { @@ -624,10 +626,20 @@ export default function InvoicesBAP() { // Remove field from autoFilledFields when manually edited const autoFilledFields = invoice.autoFilledFields ? JSON.parse(invoice.autoFilledFields) : []; const updatedAutoFields = autoFilledFields.filter((f: string) => f !== "serviceConcerne"); + const newVal = e.target.value || undefined; + // Apprentissage : mémoriser la correction manuelle + if (invoice.supplierName && newVal !== (invoice.serviceConcerne || undefined)) { + upsertLearningMutation.mutate({ + supplierName: invoice.supplierName, + fieldName: 'serviceConcerne', + originalValue: invoice.serviceConcerne || undefined, + correctedValue: newVal || '', + }); + } updateFieldMutation.mutate({ id: invoice.id, data: { - serviceConcerne: e.target.value || undefined, + serviceConcerne: newVal, autoFilledFields: updatedAutoFields.length > 0 ? JSON.stringify(updatedAutoFields) : null }, }); @@ -649,10 +661,20 @@ export default function InvoicesBAP() { // Remove field from autoFilledFields when manually edited const autoFilledFields = invoice.autoFilledFields ? JSON.parse(invoice.autoFilledFields) : []; const updatedAutoFields = autoFilledFields.filter((f: string) => f !== "typeAchat"); + const newTypeAchat = e.target.value as "CAPEX" | "OPEX" | undefined; + // Apprentissage : mémoriser la correction manuelle + if (invoice.supplierName && newTypeAchat !== (invoice.typeAchat || undefined)) { + upsertLearningMutation.mutate({ + supplierName: invoice.supplierName, + fieldName: 'typeAchat', + originalValue: invoice.typeAchat || undefined, + correctedValue: newTypeAchat || '', + }); + } updateFieldMutation.mutate({ id: invoice.id, data: { - typeAchat: e.target.value as "CAPEX" | "OPEX" | undefined, + typeAchat: newTypeAchat, autoFilledFields: updatedAutoFields.length > 0 ? JSON.stringify(updatedAutoFields) : null }, }); @@ -677,10 +699,20 @@ export default function InvoicesBAP() { // Remove field from autoFilledFields when manually edited const autoFilledFields = invoice.autoFilledFields ? JSON.parse(invoice.autoFilledFields) : []; const updatedAutoFields = autoFilledFields.filter((f: string) => f !== "ventilationComptable"); + const newVentil = e.target.value || undefined; + // Apprentissage : mémoriser la correction manuelle + if (invoice.supplierName && newVentil !== (invoice.ventilationComptable || undefined)) { + upsertLearningMutation.mutate({ + supplierName: invoice.supplierName, + fieldName: 'ventilationComptable', + originalValue: invoice.ventilationComptable || undefined, + correctedValue: newVentil || '', + }); + } updateFieldMutation.mutate({ id: invoice.id, data: { - ventilationComptable: e.target.value || undefined, + ventilationComptable: newVentil, autoFilledFields: updatedAutoFields.length > 0 ? JSON.stringify(updatedAutoFields) : null }, }); diff --git a/client/src/pages/LearningSettings.tsx b/client/src/pages/LearningSettings.tsx index 9321b99..7d3f863 100644 --- a/client/src/pages/LearningSettings.tsx +++ b/client/src/pages/LearningSettings.tsx @@ -3,9 +3,11 @@ import DashboardLayout from "@/components/DashboardLayout"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; import { trpc } from "@/lib/trpc"; import { toast } from "sonner"; -import { Brain, Trash2, RefreshCw, AlertTriangle } from "lucide-react"; +import { Brain, Trash2, RefreshCw, CheckCircle2, Eye, Settings2 } from "lucide-react"; import { AlertDialog, AlertDialogAction, @@ -32,9 +34,14 @@ const FIELD_COLORS: Record = { isSubscription: "bg-orange-100 text-orange-800", }; +const DEFAULT_CONFIDENCE_THRESHOLD = 2; + export default function LearningSettings() { const utils = trpc.useUtils(); const { data: learnings, isLoading } = trpc.learnings.list.useQuery(); + const [confidenceThreshold, setConfidenceThreshold] = useState(DEFAULT_CONFIDENCE_THRESHOLD); + const [editingThreshold, setEditingThreshold] = useState(false); + const [thresholdInput, setThresholdInput] = useState(String(DEFAULT_CONFIDENCE_THRESHOLD)); const deleteMutation = trpc.learnings.delete.useMutation({ onSuccess: () => { @@ -62,6 +69,36 @@ export default function LearningSettings() { const supplierCount = Object.keys(grouped).length; const totalCount = learnings?.length || 0; + const confirmedCount = (learnings || []).filter(l => (l?.applyCount || 0) >= confidenceThreshold).length; + const observationCount = totalCount - confirmedCount; + + const getConfidenceBadge = (applyCount: number) => { + if (applyCount >= confidenceThreshold) { + return ( + + + Confirmé · {applyCount}× + + ); + } + return ( + + + Observation · {applyCount}× + + ); + }; + + const handleSaveThreshold = () => { + const val = parseInt(thresholdInput, 10); + if (isNaN(val) || val < 1) { + toast.error("Le seuil doit être un entier ≥ 1"); + return; + } + setConfidenceThreshold(val); + setEditingThreshold(false); + toast.success(`Seuil de confiance mis à jour : ${val} application(s)`); + }; return ( @@ -110,7 +147,7 @@ export default function LearningSettings() { {/* Stats */} -
+
{supplierCount}
@@ -123,8 +160,65 @@ export default function LearningSettings() {
Correction(s) mémorisée(s)
+ + +
{confirmedCount}
+
Confirmée(s) (≥ {confidenceThreshold}×)
+
+
+ + +
{observationCount}
+
En observation (< {confidenceThreshold}×)
+
+
+ {/* Seuil de confiance */} + + + + + Seuil de confiance + + + Un apprentissage est marqué Confirmé quand il a été appliqué au moins{" "} + {confidenceThreshold} fois. En dessous, il est En observation. + + + + {editingThreshold ? ( +
+ + setThresholdInput(e.target.value)} + className="w-24" + /> + + +
+ ) : ( +
+ + Seuil actuel : {confidenceThreshold} application(s) + + +
+ )} +
+
+ {/* Comment ça fonctionne */} @@ -136,13 +230,17 @@ export default function LearningSettings() {

Quand vous modifiez manuellement le type d'achat, le service concerné ou - la ventilation comptable d'une facture dans sa page de détail, le système mémorise cette + la ventilation comptable d'une facture (dans le détail ou dans la liste BAP), le système mémorise cette correction pour ce fournisseur.

Lors des prochains imports, si une facture du même fournisseur est détectée, les corrections mémorisées sont appliquées automatiquement après l'extraction IA et les règles d'automatisme.

+

+ Le compteur Appliqué N× indique combien de fois cette correction a été utilisée lors d'imports. + Une correction Confirmée a atteint le seuil de confiance configuré. +

@@ -165,81 +263,96 @@ export default function LearningSettings() { ) : (
- {Object.entries(grouped).map(([supplierKey, entries]) => ( - - -
- {supplierKey} - - {entries?.length} correction(s) - -
-
- -
- {entries?.map((learning) => ( -
-
- - {FIELD_LABELS[learning.fieldName] || learning.fieldName} + {Object.entries(grouped).map(([supplierKey, entries]) => { + const supplierConfirmed = (entries || []).filter(l => (l?.applyCount || 0) >= confidenceThreshold).length; + const supplierObservation = (entries?.length || 0) - supplierConfirmed; + return ( + + +
+ {supplierKey} +
+ {supplierConfirmed > 0 && ( + + {supplierConfirmed} confirmée(s) -
- {learning.originalValue && ( - <> - - {learning.originalValue} - - - - )} - - {learning.correctedValue || vide} - + )} + {supplierObservation > 0 && ( + + {supplierObservation} en observation + + )} +
+
+ + +
+ {entries?.map((learning) => ( +
= confidenceThreshold + ? "bg-emerald-50/40 border-emerald-200" + : "bg-amber-50/40 border-amber-200" + }`} + > +
+ + {FIELD_LABELS[learning.fieldName] || learning.fieldName} + +
+ {learning.originalValue && ( + <> + + {learning.originalValue} + + + + )} + + {learning.correctedValue || vide} + +
+
+
+ {getConfidenceBadge(learning.applyCount || 0)} + + + + + + + Supprimer cet apprentissage ? + + La correction « {FIELD_LABELS[learning.fieldName] || learning.fieldName} → {learning.correctedValue} » + pour le fournisseur « {supplierKey} » ne sera plus appliquée automatiquement. + + + + Annuler + deleteMutation.mutate({ id: learning.id })} + > + Supprimer + + + +
-
- - Appliqué {learning.applyCount}× - - - - - - - - Supprimer cet apprentissage ? - - La correction « {FIELD_LABELS[learning.fieldName] || learning.fieldName} → {learning.correctedValue} » - pour le fournisseur « {supplierKey} » ne sera plus appliquée automatiquement. - - - - Annuler - deleteMutation.mutate({ id: learning.id })} - > - Supprimer - - - - -
-
- ))} -
- -
- ))} + ))} +
+ + + ); + })}
)}