Files
pdf_oralia_vibe/frontend/src/components/ReExtractionDiff.vue
Bertrand Benjamin c79ecc45ff feat: extraction locataires et opérations par cellules de tableau
Remplace les parseurs texte+regex (fragiles sur l'alignement en colonnes des
PDF Oralia mal construits) par une extraction géométrique : reconstruction des
lignes visuelles par regroupement vertical tolérant des mots, puis affectation
de chaque valeur à sa colonne via les filets du tableau (pdfplumber find_tables).

Corrections apportées :
- locataires : montant/libellé « divers » dans la bonne colonne (plus le total
  cumulé du lot), nom de locataire correct (le logo/en-tête hors filets est
  ignoré), lignes multi-période et pages recollées.
- opérations : fournisseur séparé de la description (TOTALENERGIES ≠ DIDIER
  NETTOYAGE, PPR ≠ BOUVARD), colonne Déductible remplie, Débit/Crédit distingués,
  fournisseur des honoraires reporté sur le bloc, fragments de description
  recollés (LATAPY, AUDOUIN).
- code lot : gère « S10 - », « S 17 - » (espace) et « S01 SOLDE » (sans tiret).

Branchés dans extractor.py avec repli sur les anciens parseurs si un tableau n'a
pas de filets détectables. Validés par réconciliation comptable : locataires
124/124 lots, opérations 7/7 PDF et 25/25 catégories, au centime.

Ajoute le bouton « Relancer l'extraction » dans l'écran d'édition : re-extrait
depuis le PDF stocké et met en évidence les différences avec la version
précédente (panneau récapitulatif + anneaux « modifié » sur les cartes). Le diff
des opérations s'aligne par contenu (robuste aux changements d'ordre/nombre).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 21:29:04 +02:00

133 lines
5.5 KiB
Vue

<template>
<div class="flex-shrink-0 border-b border-amber-300 bg-amber-50">
<!-- En-tête -->
<div class="flex items-center justify-between px-4 py-2">
<div class="flex items-center gap-2 text-sm">
<svg class="w-4 h-4 text-amber-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
<span v-if="diff.summary.total === 0" class="font-medium text-amber-800">
Nouvelle extraction aucune différence avec la version précédente
</span>
<span v-else class="font-medium text-amber-800">
Nouvelle extraction {{ diff.summary.total }} différence{{ diff.summary.total > 1 ? 's' : '' }}
<span class="text-amber-600 font-normal">
({{ diff.summary.metadata }} métadonnée{{ diff.summary.metadata > 1 ? 's' : '' }},
{{ diff.summary.locataires }} locataire{{ diff.summary.locataires > 1 ? 's' : '' }},
{{ diff.summary.operations }} opération{{ diff.summary.operations > 1 ? 's' : '' }})
</span>
</span>
</div>
<div class="flex items-center gap-2">
<button
@click="$emit('revert')"
class="px-2 py-1 text-xs text-amber-700 hover:text-amber-900 hover:bg-amber-100 rounded transition-colors"
title="Restaurer les données d'avant la re-extraction"
>
Revenir à la version précédente
</button>
<button
v-if="diff.summary.total > 0"
@click="open = !open"
class="px-2 py-1 text-xs text-amber-700 hover:text-amber-900 hover:bg-amber-100 rounded transition-colors"
>
{{ open ? 'Masquer le détail' : 'Voir le détail' }}
</button>
<button
@click="$emit('close')"
class="p-1 text-amber-500 hover:text-amber-700 hover:bg-amber-100 rounded transition-colors"
title="Fermer"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
<!-- Détail -->
<div v-if="open && diff.summary.total > 0" class="max-h-56 overflow-auto px-4 pb-3 space-y-3 text-xs">
<!-- Métadonnées -->
<div v-if="diff.metadata.length">
<div class="font-semibold text-amber-800 mb-1">Métadonnées</div>
<div class="space-y-0.5">
<div v-for="(c, i) in diff.metadata" :key="'m' + i" class="flex items-baseline gap-2">
<span class="text-gray-500 w-40 flex-shrink-0">{{ c.label }}</span>
<span class="text-red-600 line-through">{{ c.before }}</span>
<span class="text-gray-400"></span>
<span class="text-green-700 font-medium">{{ c.after }}</span>
</div>
</div>
</div>
<!-- Locataires -->
<div v-if="diff.locataires.length">
<div class="font-semibold text-amber-800 mb-1">Locataires</div>
<div class="space-y-1.5">
<div v-for="(loc, i) in diff.locataires" :key="'l' + i" class="bg-white/60 rounded p-1.5">
<div class="flex items-center gap-2 mb-0.5">
<span :class="kindClass(loc.kind)" class="text-[10px] uppercase font-semibold px-1 rounded">
{{ kindLabel(loc.kind) }}
</span>
<span class="font-medium text-gray-700">{{ loc.title }}</span>
</div>
<div v-for="(f, j) in loc.fields" :key="j" class="flex items-baseline gap-2 pl-2">
<span class="text-gray-500 w-24 flex-shrink-0">{{ f.label }}</span>
<span class="text-red-600 line-through break-all">{{ f.before }}</span>
<span class="text-gray-400"></span>
<span class="text-green-700 font-medium break-all">{{ f.after }}</span>
</div>
</div>
</div>
</div>
<!-- Opérations -->
<div v-if="diff.operations.length">
<div class="font-semibold text-amber-800 mb-1">Opérations</div>
<div class="space-y-1.5">
<div v-for="(op, i) in diff.operations" :key="'o' + i" class="bg-white/60 rounded p-1.5">
<div class="flex items-center gap-2 mb-0.5">
<span :class="kindClass(op.kind)" class="text-[10px] uppercase font-semibold px-1 rounded">
{{ kindLabel(op.kind) }}
</span>
<span v-if="op.index >= 0" class="text-gray-500">opération #{{ op.index + 1 }}</span>
</div>
<div class="pl-2">
<div class="text-red-600 line-through break-all">{{ op.before }}</div>
<div class="text-green-700 font-medium break-all">{{ op.after }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
defineProps({
diff: {
type: Object,
required: true,
},
})
defineEmits(['revert', 'close'])
const open = ref(true)
function kindLabel(kind) {
if (kind === 'added') return 'ajouté'
if (kind === 'removed') return 'supprimé'
return 'modifié'
}
function kindClass(kind) {
if (kind === 'added') return 'bg-green-100 text-green-700'
if (kind === 'removed') return 'bg-red-100 text-red-700'
return 'bg-amber-100 text-amber-700'
}
</script>