Refuser une modification, c'est reenvoyer la valeur actuelle du document. Mais
l'enregistrement ramene les donnees a une forme canonique (aujourd'hui les
numeros de lot) : sur ces champs, le refus n'est pas conserve. L'interface le
laissait croire.
Plutot que de traiter le cas des lots en dur, le serveur devient seul juge de ce
qu'il stocke et l'annonce : `canonicalize_extraction` regroupe les reecritures
faites a la persistance, `save_document` et l'apercu passent par elle, et
/api/documents/{id}/re-extract renvoie `previous_canonical` a cote de
`previous_data`. Le front compare ces deux jeux et signale, sous chaque case
decochee, la valeur que l'enregistrement reecrira — sans connaitre aucune regle.
Une transformation ajoutee plus tard remontera donc toute seule dans l'interface.
Toutes les differences restent visibles et toutes les cases actives : voir ce
qui change et pouvoir se prononcer prime, quitte a etre prevenu que ce champ-la
reviendra reecrit.
Supprime au passage frontend/src/utils/lots.js, qui dupliquait la regle de
normalisation cote client.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
134 lines
5.1 KiB
JavaScript
134 lines
5.1 KiB
JavaScript
// Fusion sélective de deux extractions.
|
|
//
|
|
// Le balayage de ré-extraction n'impose pas de tout prendre ou tout laisser :
|
|
// l'utilisateur retient les changements un par un. Le document enregistré est
|
|
// donc construit ici, en partant des données actuelles et en n'y appliquant que
|
|
// les changements retenus — éventuellement avec une valeur saisie à la main.
|
|
//
|
|
// `selection` associe l'identifiant d'un changement (cf. diffExtraction.js) à
|
|
// { accepted, value } ; une entrée absente vaut « retenu », pour que le cas
|
|
// courant (tout accepter) ne demande aucune initialisation.
|
|
//
|
|
// Limite connue : le backend normalise les numéros de lot de tout enregistrement
|
|
// (save_document → normalize_extraction_lots). Refuser une réécriture de forme
|
|
// « 0001 » → « 01 » produit bien « 0001 » ici, mais la base portera « 01 ».
|
|
// Choix assumé : mieux vaut voir toutes les différences et pouvoir se prononcer,
|
|
// même si ce champ-là revient normalisé.
|
|
|
|
function clone(value) {
|
|
return value == null ? value : JSON.parse(JSON.stringify(value))
|
|
}
|
|
|
|
function isAccepted(selection, id) {
|
|
const state = selection?.[id]
|
|
return state ? state.accepted !== false : true
|
|
}
|
|
|
|
// Valeur à écrire pour un changement retenu : celle saisie à la main si elle
|
|
// existe, sinon celle proposée par la nouvelle extraction.
|
|
function chosenValue(selection, change) {
|
|
const state = selection?.[change.id]
|
|
if (state && state.value !== undefined) return state.value
|
|
return change.afterValue
|
|
}
|
|
|
|
function setPath(obj, path, value) {
|
|
const keys = path.split('.')
|
|
let target = obj
|
|
for (const key of keys.slice(0, -1)) {
|
|
if (target[key] == null || typeof target[key] !== 'object') target[key] = {}
|
|
target = target[key]
|
|
}
|
|
target[keys.at(-1)] = value
|
|
}
|
|
|
|
/**
|
|
* Construit les données à enregistrer.
|
|
*
|
|
* @param {Object} previous données actuellement en base
|
|
* @param {Object} next données ré-extraites
|
|
* @param {Object} diff sortie de computeExtractionDiff(previous, next)
|
|
* @param {Object} selection état des cases, par identifiant de changement
|
|
*/
|
|
export function mergeExtraction(previous, next, diff, selection = {}) {
|
|
const merged = clone(previous) || {}
|
|
const nextData = next || {}
|
|
|
|
// --- Métadonnées : un champ retenu écrase le champ correspondant.
|
|
if (!merged.metadata) merged.metadata = {}
|
|
for (const change of diff.metadata) {
|
|
if (isAccepted(selection, change.id)) {
|
|
setPath(merged.metadata, change.path, chosenValue(selection, change))
|
|
}
|
|
}
|
|
|
|
// --- Locataires : alignés par index, comme dans le diff.
|
|
const previousLoc = previous?.situation_locataires || []
|
|
const nextLoc = nextData.situation_locataires || []
|
|
const byIndex = new Map(diff.locataires.map((loc) => [loc.index, loc]))
|
|
const locataires = []
|
|
for (let i = 0; i < Math.max(previousLoc.length, nextLoc.length); i++) {
|
|
const entry = byIndex.get(i)
|
|
|
|
// Locataire inchangé : l'ancienne et la nouvelle version sont équivalentes.
|
|
if (!entry) {
|
|
const kept = i < previousLoc.length ? previousLoc[i] : nextLoc[i]
|
|
if (kept) locataires.push(clone(kept))
|
|
continue
|
|
}
|
|
|
|
if (entry.kind === 'added') {
|
|
if (isAccepted(selection, entry.id)) locataires.push(clone(nextLoc[i]))
|
|
continue
|
|
}
|
|
if (entry.kind === 'removed') {
|
|
// Refuser la suppression, c'est conserver le locataire actuel.
|
|
if (!isAccepted(selection, entry.id)) locataires.push(clone(previousLoc[i]))
|
|
continue
|
|
}
|
|
|
|
const kept = clone(previousLoc[i])
|
|
for (const change of entry.changes) {
|
|
if (!isAccepted(selection, change.id)) continue
|
|
if (change.kind === 'montants') {
|
|
// Les chiffres d'un locataire viennent en bloc du même parser.
|
|
kept.totaux = clone(nextLoc[i]?.totaux)
|
|
kept.lignes = clone(nextLoc[i]?.lignes)
|
|
} else {
|
|
setPath(kept, change.path, chosenValue(selection, change))
|
|
}
|
|
}
|
|
locataires.push(kept)
|
|
}
|
|
merged.situation_locataires = locataires
|
|
|
|
// --- Opérations : on part de la nouvelle liste, dont on retire les ajouts
|
|
// refusés, puis on y réinsère les suppressions refusées à leur place d'origine.
|
|
const previousOps = previous?.recapitulatif_operations || []
|
|
const refusedAdded = new Set(
|
|
diff.operations
|
|
.filter((op) => op.kind === 'added' && !isAccepted(selection, op.id))
|
|
.map((op) => op.index)
|
|
)
|
|
const operations = []
|
|
;(nextData.recapitulatif_operations || []).forEach((op, i) => {
|
|
if (!refusedAdded.has(i)) operations.push(clone(op))
|
|
})
|
|
const restored = diff.operations
|
|
.filter((op) => op.kind === 'removed' && !isAccepted(selection, op.id))
|
|
.sort((x, y) => x.oldIndex - y.oldIndex)
|
|
for (const op of restored) {
|
|
operations.splice(Math.min(op.oldIndex, operations.length), 0, clone(previousOps[op.oldIndex]))
|
|
}
|
|
merged.recapitulatif_operations = operations
|
|
|
|
return merged
|
|
}
|
|
|
|
// Nombre de changements retenus sur le total, pour l'affichage.
|
|
export function countAccepted(changeIds, selection = {}) {
|
|
let accepted = 0
|
|
for (const id of changeIds) if (isAccepted(selection, id)) accepted++
|
|
return { accepted, total: changeIds.length }
|
|
}
|