Files
pdf_oralia_vibe/frontend/src/pages/AnalyticsPage.vue
Bertrand Benjamin 78b9bbf129 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>
2026-08-22 18:29:40 +02:00

152 lines
4.5 KiB
Vue

<template>
<div class="page">
<div class="page-content">
<!-- Header -->
<div class="page-header">
<div>
<h1 class="page-title">Dépenses</h1>
<p class="page-subtitle">
Visualisez et filtrez les dépenses de vos immeubles
</p>
</div>
<div v-if="lastUpdate" class="text-xs text-gray-500">
Derniere mise a jour: {{ lastUpdate }}
</div>
</div>
<!-- Filters -->
<FilterPanel :filtres-initiaux="currentFilters" @filter-change="onFiltersChange" />
<!-- KPI Cards -->
<KpiCards :summary="summary" />
<!-- Les deux repartitions cote a cote : meme total, deux decoupages. -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<CategoryChart :data="summary.by_category || []" />
<TagChart :data="summary.by_tag || []" />
</div>
<!-- L'evolution mensuelle prend toute la largeur : un axe de temps
serre est le premier a devenir illisible. -->
<MonthlyChart :data="summary.by_month || []" />
<!-- Top Fournisseurs -->
<TopFournisseursChart :data="summary.by_fournisseur || []" />
<!-- Depenses Table -->
<DepensesTable
:depenses="depenses"
:is-loading="isLoadingDepenses"
/>
</div>
</div>
</template>
<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'
import TagChart from '../components/analytics/TagChart.vue'
import MonthlyChart from '../components/analytics/MonthlyChart.vue'
import TopFournisseursChart from '../components/analytics/TopFournisseursChart.vue'
import DepensesTable from '../components/analytics/DepensesTable.vue'
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,
total_debit: 0,
total_credit: 0,
total_tva: 0,
total_locatif: 0,
total_deductible: 0,
by_category: [],
by_tag: [],
by_month: [],
by_fournisseur: []
})
const depenses = ref([])
const isLoadingDepenses = ref(false)
const lastUpdate = ref(null)
// Debounce timer
let debounceTimer = null
function onFiltersChange(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(() => {
loadData()
}, 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()
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()
}
async function loadData() {
const queryString = buildQueryParams()
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.value)
console.log('Summary URL:', summaryUrl)
isLoadingDepenses.value = true
try {
const [summaryRes, depensesRes] = await Promise.all([
fetch(summaryUrl),
fetch(depensesUrl)
])
if (summaryRes.ok) {
summary.value = await summaryRes.json()
console.log('Summary loaded:', summary.value.total_count, 'depenses')
}
if (depensesRes.ok) {
depenses.value = await depensesRes.json()
}
lastUpdate.value = new Date().toLocaleTimeString('fr-FR')
} catch (err) {
console.error('Failed to load analytics data:', err)
} finally {
isLoadingDepenses.value = false
}
}
onMounted(() => {
loadData()
})
</script>