feat: highlight source element when navigating from tables to edit page

Clicking the pencil icon in revenus/depenses tables now passes query
params to identify the source row. The edit page auto-expands the
matching section, scrolls to the element, and briefly highlights it
with a blue ring that fades after 2 seconds.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-13 06:16:29 +01:00
parent 04db85c688
commit 5c9a8c9b5e
7 changed files with 145 additions and 13 deletions

View File

@@ -110,11 +110,12 @@
@toggle="toggleSection('locataires')"
>
<div class="space-y-2">
<LocataireCard
v-for="(loc, idx) in data.data?.situation_locataires"
<LocataireCard
v-for="(loc, idx) in data.data?.situation_locataires"
:key="idx"
:locataire="loc"
:index="idx"
:highlighted="highlightedLocataireIndex === idx"
@update:locataire="updateLocataire(idx, $event)"
@remove="removeLocataire(idx)"
/>
@@ -140,11 +141,12 @@
@toggle="toggleSection('operations')"
>
<div class="space-y-2">
<OperationCard
v-for="(group, idx) in operationsGroupedByCategory"
<OperationCard
v-for="(group, idx) in operationsGroupedByCategory"
:key="idx"
:categorie="group.categorie"
:operations="group.operations"
:highlightIndex="group.categorie === highlightOperationCategorie ? highlightOperationIndex : -1"
@update:operations="updateOperationsGroup(group.categorie, $event)"
/>
@@ -166,7 +168,7 @@
</template>
<script setup>
import { ref, reactive, computed } from 'vue'
import { ref, reactive, computed, watch } from 'vue'
import DataSection from './DataSection.vue'
import DataCard from './DataCard.vue'
import DataRow from './DataRow.vue'
@@ -181,6 +183,10 @@ const props = defineProps({
isLoading: {
type: Boolean,
default: false
},
highlight: {
type: Object,
default: null
}
})
@@ -214,6 +220,55 @@ const expandedSections = reactive({
operations: false
})
// Highlight state
const highlightedLocataireIndex = ref(-1)
const highlightOperationCategorie = ref(null)
const highlightOperationIndex = ref(-1)
watch(
() => [props.data, props.highlight],
([data, highlight]) => {
if (!data || !highlight) return
if (highlight.type === 'locataire') {
const locataires = data.data?.situation_locataires
if (!locataires) return
const idx = locataires.findIndex(
loc => loc.lot?.numero === highlight.lot_numero && loc.locataire?.nom === highlight.locataire_nom
)
if (idx >= 0) {
expandedSections.locataires = true
highlightedLocataireIndex.value = idx
}
}
if (highlight.type === 'operation') {
const operations = data.data?.recapitulatif_operations
if (!operations) return
// Find the matching operation and determine its category + index within that category group
let targetCategorie = null
let indexInGroup = -1
const categoryCounters = {}
for (const op of operations) {
const cat = op.categorie || 'AUTRES'
if (!(cat in categoryCounters)) categoryCounters[cat] = 0
if (op.fournisseur === highlight.fournisseur && op.description === highlight.description) {
targetCategorie = cat
indexInGroup = categoryCounters[cat]
break
}
categoryCounters[cat]++
}
if (targetCategorie !== null) {
expandedSections.operations = true
highlightOperationCategorie.value = targetCategorie
highlightOperationIndex.value = indexInGroup
}
}
},
{ immediate: true }
)
function toggleSection(section) {
expandedSections[section] = !expandedSections[section]
}

View File

@@ -1,5 +1,5 @@
<template>
<div class="border border-gray-200 rounded-lg overflow-hidden">
<div ref="rootEl" class="border rounded-lg overflow-hidden transition-all duration-300" :class="isHighlighted ? 'border-blue-400 ring-2 ring-blue-400 bg-blue-50' : 'border-gray-200'">
<!-- Header -->
<div class="w-full flex items-center justify-between px-3 py-2 bg-white hover:bg-gray-50 transition-colors">
<button
@@ -200,7 +200,7 @@
</template>
<script setup>
import { ref, computed } from 'vue'
import { ref, computed, watch, nextTick } from 'vue'
import EditableField from './EditableField.vue'
const props = defineProps({
@@ -211,12 +211,31 @@ const props = defineProps({
index: {
type: Number,
default: 0
},
highlighted: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['update:locataire', 'remove'])
const expanded = ref(false)
const rootEl = ref(null)
const isHighlighted = ref(false)
watch(() => props.highlighted, (val) => {
if (val) {
expanded.value = true
isHighlighted.value = true
nextTick(() => {
rootEl.value?.scrollIntoView({ behavior: 'smooth', block: 'center' })
})
setTimeout(() => {
isHighlighted.value = false
}, 2000)
}
}, { immediate: true })
const totalClass = computed(() => {
const total = props.locataire.totaux?.total || 0

View File

@@ -40,10 +40,12 @@
<!-- Content -->
<div v-show="expanded" class="border-t border-gray-100 bg-gray-50">
<div class="divide-y divide-gray-100">
<div
v-for="(op, idx) in operations"
<div
v-for="(op, idx) in operations"
:key="idx"
class="px-3 py-2 bg-white text-xs"
:ref="el => { if (el) opRefs[idx] = el }"
class="px-3 py-2 bg-white text-xs transition-all duration-300"
:class="highlightedIdx === idx ? 'ring-2 ring-blue-400 bg-blue-50' : ''"
>
<div class="flex items-start justify-between gap-2">
<div class="flex-1 min-w-0 space-y-1">
@@ -184,7 +186,7 @@
</template>
<script setup>
import { ref, computed } from 'vue'
import { ref, computed, watch, nextTick } from 'vue'
import EditableField from './EditableField.vue'
const props = defineProps({
@@ -195,12 +197,34 @@ const props = defineProps({
operations: {
type: Array,
required: true
},
highlightIndex: {
type: Number,
default: -1
}
})
const emit = defineEmits(['update:operations'])
const expanded = ref(false)
const opRefs = ref({})
const highlightedIdx = ref(-1)
watch(() => props.highlightIndex, (val) => {
if (val >= 0) {
expanded.value = true
highlightedIdx.value = val
nextTick(() => {
const el = opRefs.value[val]
if (el) {
el.scrollIntoView({ behavior: 'smooth', block: 'center' })
}
})
setTimeout(() => {
highlightedIdx.value = -1
}, 2000)
}
}, { immediate: true })
const totalDebit = computed(() => {
return props.operations.reduce((sum, op) => sum + (op.montants?.debit || 0), 0)

View File

@@ -38,6 +38,7 @@
<th class="px-3 py-2 text-left text-xs font-medium text-gray-400 uppercase">Tag</th>
<th class="px-3 py-2 text-right text-xs font-medium text-gray-400 uppercase">Debit</th>
<th class="px-3 py-2 text-right text-xs font-medium text-gray-400 uppercase">Credit</th>
<th class="px-3 py-2 w-10"></th>
</tr>
</thead>
<tbody class="divide-y divide-gray-800">
@@ -82,6 +83,17 @@
<td class="px-3 py-2 text-right text-green-400 font-mono whitespace-nowrap">
{{ dep.credit > 0 ? formatCurrency(dep.credit) : '-' }}
</td>
<td class="px-3 py-2 text-center">
<router-link
:to="{ path: '/documents/' + dep.document_id + '/edit', query: { highlight: 'operation', fournisseur: dep.fournisseur, description: dep.description } }"
class="text-gray-500 hover:text-blue-400 transition-colors"
title="Editer le document source"
>
<svg class="w-4 h-4 inline-block" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
</svg>
</router-link>
</td>
</tr>
</tbody>
</table>

View File

@@ -38,6 +38,7 @@
<th class="px-4 py-3 text-right">Total</th>
<th class="px-4 py-3 text-right">Regle</th>
<th class="px-4 py-3 text-right">Impaye</th>
<th class="px-4 py-3 w-10"></th>
</tr>
</thead>
<tbody class="divide-y divide-gray-800">
@@ -77,6 +78,17 @@
{{ formatAmount(revenu.impayes) }}
</span>
</td>
<td class="px-4 py-3 text-center">
<router-link
:to="{ path: '/documents/' + revenu.document_id + '/edit', query: { highlight: 'locataire', lot_numero: revenu.lot_numero, locataire_nom: revenu.locataire_nom } }"
class="text-gray-500 hover:text-blue-400 transition-colors"
title="Editer le document source"
>
<svg class="w-4 h-4 inline-block" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
</svg>
</router-link>
</td>
</tr>
</tbody>
</table>

View File

@@ -77,11 +77,12 @@
<!-- Droite : Édition ou Tagging -->
<div class="w-1/2 flex flex-col bg-gray-50">
<!-- Vue 1 : JsonViewer (mode édition) -->
<JsonViewer
<JsonViewer
v-if="!showTagging && extractedData"
:data="extractedData"
@update:data="extractedData = $event"
:is-loading="false"
:highlight="highlightInfo"
class="flex-1"
/>
@@ -110,7 +111,7 @@
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ref, computed, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import PdfPreview from '../components/PdfPreview.vue'
import JsonViewer from '../components/JsonViewer.vue'
@@ -127,6 +128,13 @@ const showTagging = ref(false)
const isSaving = ref(false)
const saveError = ref(null)
const highlightInfo = computed(() => {
const q = route.query
if (q.highlight === 'locataire') return { type: 'locataire', lot_numero: q.lot_numero, locataire_nom: q.locataire_nom }
if (q.highlight === 'operation') return { type: 'operation', fournisseur: q.fournisseur, description: q.description }
return null
})
onMounted(async () => {
await loadDocument()
})

View File

@@ -99,6 +99,7 @@ class RevenuDetailResponse(BaseModel):
"""Detail d'un revenu."""
id: int
document_id: int
document_date: str
document_reference: str | None
immeuble_code: str
@@ -442,6 +443,7 @@ async def get_revenus_details(
results.append(
RevenuDetailResponse(
id=rev.id,
document_id=rev.document_id,
document_date=str(row.document_date),
document_reference=row.document_reference,
immeuble_code=row.immeuble_code,