// 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 } }