feat: ramène à la vue d'où l'on vient une fois le document enregistré

Éditer une ligne depuis Dépenses ou Recettes ouvrait le document, puis
relâchait sur la liste des documents : il fallait revenir à la page
d'analyse et refaire ses filtres à la main pour corriger la ligne suivante.

Les deux tables passent maintenant à l'éditeur la vue d'où l'on part,
filtres compris, et il y ramène à ses trois sorties : enregistrement
terminé, abandon, et document introuvable. Sans cette destination — depuis
la liste des documents, ou un lien direct — le repli reste la liste.

La destination arrive par l'URL, donc fabricable par n'importe qui : seule
une page de l'application est suivie (utils/retour.js). Une URL absolue, un
chemin protocol-relatif que le navigateur lirait comme un domaine, ou des
contre-slashs retombent sur le repli.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-22 18:27:13 +02:00
parent 78b9bbf129
commit faab7173bd
5 changed files with 102 additions and 5 deletions

View File

@@ -84,7 +84,7 @@
</td>
<td class="text-center">
<router-link
:to="{ path: '/documents/' + dep.document_id + '/edit', query: { highlight: 'operation', fournisseur: dep.fournisseur, description: dep.description } }"
:to="lienEdition(dep)"
class="text-gray-500 hover:text-blue-400 transition-colors"
title="Editer le document source"
>
@@ -142,6 +142,9 @@
<script setup>
import { ref, computed, watch } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const props = defineProps({
depenses: {
@@ -169,6 +172,20 @@ const paginatedDepenses = computed(() =>
props.depenses.slice(startIndex.value, endIndex.value)
)
/** L'editeur recoit la vue d'ou l'on part, filtres compris : une fois
l'enregistrement termine, il y ramene au lieu de la liste des documents. */
function lienEdition(depense) {
return {
path: `/documents/${depense.document_id}/edit`,
query: {
highlight: 'operation',
fournisseur: depense.fournisseur,
description: depense.description,
retour: route.fullPath
}
}
}
function formatDate(dateStr) {
if (!dateStr) return '-'
const d = new Date(dateStr)

View File

@@ -77,7 +77,7 @@
</td>
<td class="text-center">
<router-link
:to="{ path: '/documents/' + revenu.document_id + '/edit', query: { highlight: 'locataire', lot_numero: revenu.lot_numero, locataire_nom: revenu.locataire_nom } }"
:to="lienEdition(revenu)"
class="text-gray-500 hover:text-blue-400 transition-colors"
title="Editer le document source"
>
@@ -140,6 +140,23 @@
<script setup>
import { ref, computed } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
/** L'editeur recoit la vue d'ou l'on part, filtres compris : une fois
l'enregistrement termine, il y ramene au lieu de la liste des documents. */
function lienEdition(revenu) {
return {
path: `/documents/${revenu.document_id}/edit`,
query: {
highlight: 'locataire',
lot_numero: revenu.lot_numero,
locataire_nom: revenu.locataire_nom,
retour: route.fullPath
}
}
}
const props = defineProps({
revenus: {

View File

@@ -151,6 +151,7 @@ import JsonViewer from '../components/JsonViewer.vue'
import TaggingStep from '../components/TaggingStep.vue'
import ReExtractionDiff from '../components/ReExtractionDiff.vue'
import { computeExtractionDiff } from '../utils/diffExtraction.js'
import { cheminRetour } from '../utils/retour.js'
const router = useRouter()
const route = useRoute()
@@ -167,6 +168,10 @@ const reExtractError = ref(null)
const diff = ref(null)
const previousData = ref(null)
// D'ou l'on vient, et donc ou repartir une fois l'edition terminee : la table
// d'analyse avec ses filtres si elle l'a dit, la liste des documents sinon.
const destinationRetour = computed(() => cheminRetour(route.query.retour, '/documents'))
const highlightInfo = computed(() => {
const q = route.query
if (q.highlight === 'locataire') return { type: 'locataire', lot_numero: q.lot_numero, locataire_nom: q.locataire_nom }
@@ -196,7 +201,7 @@ async function loadDocument() {
} catch (err) {
console.error('Error loading document:', err)
alert('Erreur lors du chargement du document: ' + err.message)
router.push('/documents')
router.push(destinationRetour.value)
} finally {
isLoading.value = false
}
@@ -214,7 +219,7 @@ function cancel() {
const confirmed = confirm('Abandonner les modifications ?')
if (!confirmed) return
}
router.push('/documents')
router.push(destinationRetour.value)
}
function goToTagging() {
@@ -284,7 +289,7 @@ async function handleSave(depensesTags, shouldOverwrite) {
if (result.success) {
// Succès - redirection vers la liste des documents
router.push('/documents')
router.push(destinationRetour.value)
} else {
throw new Error(result.message || 'Échec de la sauvegarde')
}

View File

@@ -0,0 +1,22 @@
// Une table d'analyse envoie vers l'editeur de document la page ou revenir une
// fois l'enregistrement termine, filtres compris. Cette destination arrive par
// l'URL : elle est donc fabricable par n'importe qui, et n'est suivie que si
// elle designe une page de l'application.
/**
* La destination de retour portee par une query, ou `repli` si elle manque ou
* ne designe pas un chemin interne.
*
* Sont refuses tout ce qui peut sortir du site : une URL absolue
* (`https://ailleurs`), un chemin protocol-relatif (`//ailleurs`) que le
* navigateur traite comme un domaine, et les contre-slashs dont certains
* navigateurs font des slashs.
*/
export function cheminRetour(brut, repli = '/documents') {
const valeur = Array.isArray(brut) ? brut[0] : brut
if (typeof valeur !== 'string' || valeur === '') return repli
if (!valeur.startsWith('/')) return repli
if (valeur.startsWith('//')) return repli
if (valeur.includes('\\')) return repli
return valeur
}