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:
@@ -115,6 +115,7 @@
|
|||||||
:key="idx"
|
:key="idx"
|
||||||
:locataire="loc"
|
:locataire="loc"
|
||||||
:index="idx"
|
:index="idx"
|
||||||
|
:highlighted="highlightedLocataireIndex === idx"
|
||||||
@update:locataire="updateLocataire(idx, $event)"
|
@update:locataire="updateLocataire(idx, $event)"
|
||||||
@remove="removeLocataire(idx)"
|
@remove="removeLocataire(idx)"
|
||||||
/>
|
/>
|
||||||
@@ -145,6 +146,7 @@
|
|||||||
:key="idx"
|
:key="idx"
|
||||||
:categorie="group.categorie"
|
:categorie="group.categorie"
|
||||||
:operations="group.operations"
|
:operations="group.operations"
|
||||||
|
:highlightIndex="group.categorie === highlightOperationCategorie ? highlightOperationIndex : -1"
|
||||||
@update:operations="updateOperationsGroup(group.categorie, $event)"
|
@update:operations="updateOperationsGroup(group.categorie, $event)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -166,7 +168,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, computed } from 'vue'
|
import { ref, reactive, computed, watch } from 'vue'
|
||||||
import DataSection from './DataSection.vue'
|
import DataSection from './DataSection.vue'
|
||||||
import DataCard from './DataCard.vue'
|
import DataCard from './DataCard.vue'
|
||||||
import DataRow from './DataRow.vue'
|
import DataRow from './DataRow.vue'
|
||||||
@@ -181,6 +183,10 @@ const props = defineProps({
|
|||||||
isLoading: {
|
isLoading: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
|
},
|
||||||
|
highlight: {
|
||||||
|
type: Object,
|
||||||
|
default: null
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -214,6 +220,55 @@ const expandedSections = reactive({
|
|||||||
operations: false
|
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) {
|
function toggleSection(section) {
|
||||||
expandedSections[section] = !expandedSections[section]
|
expandedSections[section] = !expandedSections[section]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<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 -->
|
<!-- Header -->
|
||||||
<div class="w-full flex items-center justify-between px-3 py-2 bg-white hover:bg-gray-50 transition-colors">
|
<div class="w-full flex items-center justify-between px-3 py-2 bg-white hover:bg-gray-50 transition-colors">
|
||||||
<button
|
<button
|
||||||
@@ -200,7 +200,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed, watch, nextTick } from 'vue'
|
||||||
import EditableField from './EditableField.vue'
|
import EditableField from './EditableField.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -211,12 +211,31 @@ const props = defineProps({
|
|||||||
index: {
|
index: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 0
|
default: 0
|
||||||
|
},
|
||||||
|
highlighted: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['update:locataire', 'remove'])
|
const emit = defineEmits(['update:locataire', 'remove'])
|
||||||
|
|
||||||
const expanded = ref(false)
|
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 totalClass = computed(() => {
|
||||||
const total = props.locataire.totaux?.total || 0
|
const total = props.locataire.totaux?.total || 0
|
||||||
|
|||||||
@@ -43,7 +43,9 @@
|
|||||||
<div
|
<div
|
||||||
v-for="(op, idx) in operations"
|
v-for="(op, idx) in operations"
|
||||||
:key="idx"
|
: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 items-start justify-between gap-2">
|
||||||
<div class="flex-1 min-w-0 space-y-1">
|
<div class="flex-1 min-w-0 space-y-1">
|
||||||
@@ -184,7 +186,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed, watch, nextTick } from 'vue'
|
||||||
import EditableField from './EditableField.vue'
|
import EditableField from './EditableField.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -195,12 +197,34 @@ const props = defineProps({
|
|||||||
operations: {
|
operations: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true
|
required: true
|
||||||
|
},
|
||||||
|
highlightIndex: {
|
||||||
|
type: Number,
|
||||||
|
default: -1
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['update:operations'])
|
const emit = defineEmits(['update:operations'])
|
||||||
|
|
||||||
const expanded = ref(false)
|
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(() => {
|
const totalDebit = computed(() => {
|
||||||
return props.operations.reduce((sum, op) => sum + (op.montants?.debit || 0), 0)
|
return props.operations.reduce((sum, op) => sum + (op.montants?.debit || 0), 0)
|
||||||
|
|||||||
@@ -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-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">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 text-right text-xs font-medium text-gray-400 uppercase">Credit</th>
|
||||||
|
<th class="px-3 py-2 w-10"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y divide-gray-800">
|
<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">
|
<td class="px-3 py-2 text-right text-green-400 font-mono whitespace-nowrap">
|
||||||
{{ dep.credit > 0 ? formatCurrency(dep.credit) : '-' }}
|
{{ dep.credit > 0 ? formatCurrency(dep.credit) : '-' }}
|
||||||
</td>
|
</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>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -38,6 +38,7 @@
|
|||||||
<th class="px-4 py-3 text-right">Total</th>
|
<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">Regle</th>
|
||||||
<th class="px-4 py-3 text-right">Impaye</th>
|
<th class="px-4 py-3 text-right">Impaye</th>
|
||||||
|
<th class="px-4 py-3 w-10"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y divide-gray-800">
|
<tbody class="divide-y divide-gray-800">
|
||||||
@@ -77,6 +78,17 @@
|
|||||||
{{ formatAmount(revenu.impayes) }}
|
{{ formatAmount(revenu.impayes) }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</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>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -82,6 +82,7 @@
|
|||||||
:data="extractedData"
|
:data="extractedData"
|
||||||
@update:data="extractedData = $event"
|
@update:data="extractedData = $event"
|
||||||
:is-loading="false"
|
:is-loading="false"
|
||||||
|
:highlight="highlightInfo"
|
||||||
class="flex-1"
|
class="flex-1"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -110,7 +111,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
import PdfPreview from '../components/PdfPreview.vue'
|
import PdfPreview from '../components/PdfPreview.vue'
|
||||||
import JsonViewer from '../components/JsonViewer.vue'
|
import JsonViewer from '../components/JsonViewer.vue'
|
||||||
@@ -127,6 +128,13 @@ const showTagging = ref(false)
|
|||||||
const isSaving = ref(false)
|
const isSaving = ref(false)
|
||||||
const saveError = ref(null)
|
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 () => {
|
onMounted(async () => {
|
||||||
await loadDocument()
|
await loadDocument()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ class RevenuDetailResponse(BaseModel):
|
|||||||
"""Detail d'un revenu."""
|
"""Detail d'un revenu."""
|
||||||
|
|
||||||
id: int
|
id: int
|
||||||
|
document_id: int
|
||||||
document_date: str
|
document_date: str
|
||||||
document_reference: str | None
|
document_reference: str | None
|
||||||
immeuble_code: str
|
immeuble_code: str
|
||||||
@@ -442,6 +443,7 @@ async def get_revenus_details(
|
|||||||
results.append(
|
results.append(
|
||||||
RevenuDetailResponse(
|
RevenuDetailResponse(
|
||||||
id=rev.id,
|
id=rev.id,
|
||||||
|
document_id=rev.document_id,
|
||||||
document_date=str(row.document_date),
|
document_date=str(row.document_date),
|
||||||
document_reference=row.document_reference,
|
document_reference=row.document_reference,
|
||||||
immeuble_code=row.immeuble_code,
|
immeuble_code=row.immeuble_code,
|
||||||
|
|||||||
Reference in New Issue
Block a user