diff --git a/frontend/src/components/ExtractionDiffDetails.vue b/frontend/src/components/ExtractionDiffDetails.vue new file mode 100644 index 0000000..e319a13 --- /dev/null +++ b/frontend/src/components/ExtractionDiffDetails.vue @@ -0,0 +1,77 @@ + + + diff --git a/frontend/src/components/ReExtractionDiff.vue b/frontend/src/components/ReExtractionDiff.vue index 22e0c2c..ee3a451 100644 --- a/frontend/src/components/ReExtractionDiff.vue +++ b/frontend/src/components/ReExtractionDiff.vue @@ -47,65 +47,15 @@ -
- -
-
Métadonnées
-
-
- {{ c.label }} - {{ c.before }} - - {{ c.after }} -
-
-
- - -
-
Locataires
-
-
-
- - {{ kindLabel(loc.kind) }} - - {{ loc.title }} -
-
- {{ f.label }} - {{ f.before }} - - {{ f.after }} -
-
-
-
- - -
-
Opérations
-
-
-
- - {{ kindLabel(op.kind) }} - - opération #{{ op.index + 1 }} -
-
-
{{ op.before }}
-
{{ op.after }}
-
-
-
-
+
+
diff --git a/frontend/src/pages/DocumentsPage.vue b/frontend/src/pages/DocumentsPage.vue index cb824ec..8c4190b 100644 --- a/frontend/src/pages/DocumentsPage.vue +++ b/frontend/src/pages/DocumentsPage.vue @@ -10,6 +10,20 @@ {{ documents.length }} document{{ documents.length > 1 ? 's' : '' }} en base

+ +
+ + + + + + Re-extraire la base + +
diff --git a/frontend/src/pages/EditDocumentPage.vue b/frontend/src/pages/EditDocumentPage.vue index f7667b2..1c85d45 100644 --- a/frontend/src/pages/EditDocumentPage.vue +++ b/frontend/src/pages/EditDocumentPage.vue @@ -262,16 +262,17 @@ async function handleSave(depensesTags, shouldOverwrite) { saveError.value = null try { - const response = await fetch('/api/save', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' + // Mise à jour ciblée par ID : si la nouvelle extraction corrige la référence + // ou la date, c'est bien ce document qui est mis à jour, pas un doublon. + const response = await fetch(`/api/documents/${documentId}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' }, body: JSON.stringify({ source_file: extractedData.value.source_file, data: extractedData.value.data, - depenses_tags: depensesTags, - overwrite: true // Toujours true en mode édition + depenses_tags: depensesTags }) }) diff --git a/frontend/src/pages/ReExtractionPage.vue b/frontend/src/pages/ReExtractionPage.vue new file mode 100644 index 0000000..8d54453 --- /dev/null +++ b/frontend/src/pages/ReExtractionPage.vue @@ -0,0 +1,578 @@ + + + diff --git a/frontend/src/router.js b/frontend/src/router.js index 073c018..45ed659 100644 --- a/frontend/src/router.js +++ b/frontend/src/router.js @@ -5,6 +5,7 @@ import AnalyticsPage from './pages/AnalyticsPage.vue' import RevenusPage from './pages/RevenusPage.vue' import DocumentsPage from './pages/DocumentsPage.vue' import EditDocumentPage from './pages/EditDocumentPage.vue' +import ReExtractionPage from './pages/ReExtractionPage.vue' import IAPage from './pages/IAPage.vue' import ConfigPage from './pages/ConfigPage.vue' @@ -48,6 +49,11 @@ const routes = [ path: '/documents/:id/edit', name: 'edit-document', component: EditDocumentPage + }, + { + path: '/re-extraction', + name: 're-extraction', + component: ReExtractionPage } ] diff --git a/frontend/src/utils/diffExtraction.js b/frontend/src/utils/diffExtraction.js index 61d3713..baab06b 100644 --- a/frontend/src/utils/diffExtraction.js +++ b/frontend/src/utils/diffExtraction.js @@ -110,7 +110,9 @@ function locataireTitle(loc) { return `${lot} — ${nom}` } -function opSig(op) { +// Signature d'une opération : sert au diff (appariement par contenu) comme au +// report des tags d'une extraction à l'autre. +export function opSig(op) { if (!op) return '∅' const m = op.montants || {} return ( diff --git a/frontend/src/utils/tagRemap.js b/frontend/src/utils/tagRemap.js new file mode 100644 index 0000000..51e6178 --- /dev/null +++ b/frontend/src/utils/tagRemap.js @@ -0,0 +1,95 @@ +// Report des tags de dépenses d'une extraction sur la suivante. +// +// Les tags sont stockés par *index* d'opération. Une ré-extraction peut ajouter, +// retirer ou réordonner des opérations : reporter les tags par index décalerait +// alors silencieusement les catégories. On les réapparie donc par signature +// d'opération (fournisseur, description, montants), et ce qui ne se réapparie +// pas est signalé plutôt que deviné. + +import { opSig } from './diffExtraction.js' + +/** + * Réapparie les tags de l'ancienne extraction sur la nouvelle. + * + * @param {Array} previousOps opérations de l'extraction en base + * @param {Array} tags tags actuels, [{ index, tag_id, tag_nom }] + * @param {Array} newOps opérations de la nouvelle extraction + * @returns {{ tags: Object, reported: number, missing: number[] }} + * `tags` associe l'index d'une nouvelle opération à { tag_id, tag_nom, origin }, + * `missing` liste les index des nouvelles opérations restées sans tag. + */ +export function remapTags(previousOps, tags, newOps) { + const previous = previousOps || [] + const next = newOps || [] + + // Signature -> file des tags disponibles (plusieurs opérations peuvent + // partager la même signature, on les consomme dans l'ordre). + const available = new Map() + for (const t of tags || []) { + const op = previous[t.index] + if (!op) continue + const sig = opSig(op) + if (!available.has(sig)) available.set(sig, []) + available.get(sig).push(t) + } + + const remapped = {} + const missing = [] + next.forEach((op, i) => { + const queue = available.get(opSig(op)) + const t = queue && queue.length ? queue.shift() : null + if (t) { + remapped[i] = { tag_id: t.tag_id, tag_nom: t.tag_nom, origin: 'reporte' } + } else { + missing.push(i) + } + }) + + return { tags: remapped, reported: Object.keys(remapped).length, missing } +} + +/** + * Complète les opérations sans tag par une prédiction du backend. + * + * Modifie `remapped` en place et retourne le nombre de tags prédits. Une panne + * de prédiction n'est pas bloquante : les opérations restent simplement à taguer. + */ +export async function fillPredictions(remapped, newOps, missing) { + if (!missing.length) return 0 + + try { + const response = await fetch('/api/predict-tags', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ depenses: missing.map((i) => newOps[i]) }), + }) + if (!response.ok) return 0 + + const { predictions } = await response.json() + let predicted = 0 + for (const p of predictions || []) { + if (p.tag_id == null) continue + const opIndex = missing[p.index] + if (opIndex == null) continue + remapped[opIndex] = { + tag_id: p.tag_id, + tag_nom: p.tag_name || null, + origin: 'predit', + confidence: p.confidence, + reason: p.reason, + } + predicted++ + } + return predicted + } catch (err) { + console.error('Prédiction de tags indisponible:', err) + return 0 + } +} + +// Format attendu par l'API de sauvegarde : [{ index, tag_id }]. +export function toDepensesTags(remapped) { + return Object.entries(remapped) + .filter(([, t]) => t && t.tag_id != null) + .map(([index, t]) => ({ index: Number(index), tag_id: t.tag_id })) +}