feat: fait vivre les filtres d'analyse dans l'URL

Les filtres des pages Dépenses et Recettes n'existaient que dans l'état du
composant : recharger la page les perdait, et une vue filtrée ne pouvait ni
se partager, ni se mettre en signet, ni servir de destination à un lien.

Chaque page déclare desormais comment ses filtres se lisent et s'écrivent
dans la query (utils/filtresUrl.js). Ce qui vaut sa valeur par défaut n'est
pas écrit : l'URL ne porte que ce qui a été choisi, et reste lisible. Ce qui
en arrive est validé — un identifiant qui n'est pas un entier ou une date au
mauvais format est ignoré plutôt qu'affiché de travers.

Le changement de filtre remplace l'entrée d'historique au lieu d'en empiler
une : le bouton Retour du navigateur quitte la page, il ne défait pas les
filtres un par un.

Côté Dépenses, la requête API dérive de cette même écriture : les paramètres
sont les mêmes des deux côtés, la vue et la requête ne peuvent plus diverger.

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

View File

@@ -16,7 +16,7 @@
</div>
<!-- Filters -->
<FilterPanel @filter-change="onFiltersChange" />
<FilterPanel :filtres-initiaux="currentFilters" @filter-change="onFiltersChange" />
<!-- KPI Cards -->
<KpiCards :summary="summary" />
@@ -46,6 +46,8 @@
<script setup>
import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { SCHEMA_DEPENSES, ecrireFiltres, lireFiltres } from '../utils/filtresUrl.js'
import FilterPanel from '../components/analytics/FilterPanel.vue'
import KpiCards from '../components/analytics/KpiCards.vue'
import CategoryChart from '../components/analytics/CategoryChart.vue'
@@ -54,16 +56,12 @@ import MonthlyChart from '../components/analytics/MonthlyChart.vue'
import TopFournisseursChart from '../components/analytics/TopFournisseursChart.vue'
import DepensesTable from '../components/analytics/DepensesTable.vue'
// Current filters state
let currentFilters = {
immeuble_id: null,
lot_id: null,
categorie: null,
tag_id: null,
fournisseurs: [],
date_debut: null,
date_fin: null
}
const route = useRoute()
const router = useRouter()
// Les filtres viennent de l'URL : ouvrir un lien filtre, recharger la page ou
// revenir de l'editeur de document rend exactement la meme vue.
const currentFilters = ref(lireFiltres(route.query, SCHEMA_DEPENSES))
const summary = ref({
total_count: 0,
@@ -86,8 +84,12 @@ const lastUpdate = ref(null)
let debounceTimer = null
function onFiltersChange(newFilters) {
currentFilters = { ...newFilters }
currentFilters.value = { ...newFilters }
// `replace` et non `push` : le bouton Retour du navigateur doit quitter la
// page, pas defaire les filtres un par un.
router.replace({ query: ecrireFiltres(currentFilters.value, SCHEMA_DEPENSES) })
// Debounce
if (debounceTimer) clearTimeout(debounceTimer)
debounceTimer = setTimeout(() => {
@@ -95,18 +97,18 @@ function onFiltersChange(newFilters) {
}, 300)
}
/** L'API attend les memes parametres que l'URL de la page : une seule ecriture
des filtres, donc aucun moyen que la vue et la requete divergent. */
function buildQueryParams() {
const params = new URLSearchParams()
if (currentFilters.immeuble_id !== null) params.append('immeuble_id', currentFilters.immeuble_id)
if (currentFilters.lot_id !== null) params.append('lot_id', currentFilters.lot_id)
if (currentFilters.categorie !== null) params.append('categorie', currentFilters.categorie)
if (currentFilters.tag_id !== null) params.append('tag_id', currentFilters.tag_id)
// Un parametre `fournisseur` par fournisseur retenu : l'API en fait un OR.
for (const nom of currentFilters.fournisseurs || []) params.append('fournisseur', nom)
if (currentFilters.date_debut) params.append('date_debut', currentFilters.date_debut)
if (currentFilters.date_fin) params.append('date_fin', currentFilters.date_fin)
const query = ecrireFiltres(currentFilters.value, SCHEMA_DEPENSES)
for (const [cle, valeur] of Object.entries(query)) {
for (const unitaire of Array.isArray(valeur) ? valeur : [valeur]) {
params.append(cle, unitaire)
}
}
return params.toString()
}
@@ -115,7 +117,7 @@ async function loadData() {
const summaryUrl = queryString ? `/api/analytics/depenses/summary?${queryString}` : '/api/analytics/depenses/summary'
const depensesUrl = queryString ? `/api/analytics/depenses?${queryString}&limit=500` : '/api/analytics/depenses?limit=500'
console.log('Loading data with filters:', currentFilters)
console.log('Loading data with filters:', currentFilters.value)
console.log('Summary URL:', summaryUrl)
isLoadingDepenses.value = true