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:
@@ -120,6 +120,11 @@
|
|||||||
import { ref, reactive, computed, onMounted } from 'vue'
|
import { ref, reactive, computed, onMounted } from 'vue'
|
||||||
import SelectionMultiple from '../SelectionMultiple.vue'
|
import SelectionMultiple from '../SelectionMultiple.vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
/** Etat de depart, tel que la page l'a lu dans l'URL. */
|
||||||
|
filtresInitiaux: { type: Object, default: null }
|
||||||
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['filter-change'])
|
const emit = defineEmits(['filter-change'])
|
||||||
|
|
||||||
/** Valeur de tag_id demandant les depenses sans tag (cf. SANS_TAG cote API). */
|
/** Valeur de tag_id demandant les depenses sans tag (cf. SANS_TAG cote API). */
|
||||||
@@ -140,7 +145,8 @@ const filters = reactive({
|
|||||||
tag_id: null,
|
tag_id: null,
|
||||||
fournisseurs: [],
|
fournisseurs: [],
|
||||||
date_debut: null,
|
date_debut: null,
|
||||||
date_fin: null
|
date_fin: null,
|
||||||
|
...(props.filtresInitiaux ?? {})
|
||||||
})
|
})
|
||||||
|
|
||||||
/** Ordre alphabetique - l'API les trie par montant, ce qui se lit bien dans un
|
/** Ordre alphabetique - l'API les trie par montant, ce qui se lit bien dans un
|
||||||
|
|||||||
@@ -101,32 +101,34 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
/** Etat de depart, tel que la page l'a lu dans l'URL. */
|
||||||
|
filtresInitiaux: { type: Object, default: null }
|
||||||
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['filter-change'])
|
const emit = defineEmits(['filter-change'])
|
||||||
|
|
||||||
const immeubles = ref([])
|
const immeubles = ref([])
|
||||||
|
|
||||||
const filters = ref({
|
const filters = ref({ ...filtresParDefaut(), ...(props.filtresInitiaux ?? {}) })
|
||||||
|
|
||||||
|
function filtresParDefaut() {
|
||||||
|
return {
|
||||||
immeuble_id: null,
|
immeuble_id: null,
|
||||||
type_ligne: null,
|
type_ligne: null,
|
||||||
date_debut: null,
|
date_debut: null,
|
||||||
date_fin: null,
|
date_fin: null,
|
||||||
impayes_only: false,
|
impayes_only: false,
|
||||||
months: 12
|
months: 12
|
||||||
})
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function emitFilters() {
|
function emitFilters() {
|
||||||
emit('filter-change', { ...filters.value })
|
emit('filter-change', { ...filters.value })
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetFilters() {
|
function resetFilters() {
|
||||||
filters.value = {
|
filters.value = filtresParDefaut()
|
||||||
immeuble_id: null,
|
|
||||||
type_ligne: null,
|
|
||||||
date_debut: null,
|
|
||||||
date_fin: null,
|
|
||||||
impayes_only: false,
|
|
||||||
months: 12
|
|
||||||
}
|
|
||||||
emitFilters()
|
emitFilters()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Filters -->
|
<!-- Filters -->
|
||||||
<FilterPanel @filter-change="onFiltersChange" />
|
<FilterPanel :filtres-initiaux="currentFilters" @filter-change="onFiltersChange" />
|
||||||
|
|
||||||
<!-- KPI Cards -->
|
<!-- KPI Cards -->
|
||||||
<KpiCards :summary="summary" />
|
<KpiCards :summary="summary" />
|
||||||
@@ -46,6 +46,8 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
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 FilterPanel from '../components/analytics/FilterPanel.vue'
|
||||||
import KpiCards from '../components/analytics/KpiCards.vue'
|
import KpiCards from '../components/analytics/KpiCards.vue'
|
||||||
import CategoryChart from '../components/analytics/CategoryChart.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 TopFournisseursChart from '../components/analytics/TopFournisseursChart.vue'
|
||||||
import DepensesTable from '../components/analytics/DepensesTable.vue'
|
import DepensesTable from '../components/analytics/DepensesTable.vue'
|
||||||
|
|
||||||
// Current filters state
|
const route = useRoute()
|
||||||
let currentFilters = {
|
const router = useRouter()
|
||||||
immeuble_id: null,
|
|
||||||
lot_id: null,
|
// Les filtres viennent de l'URL : ouvrir un lien filtre, recharger la page ou
|
||||||
categorie: null,
|
// revenir de l'editeur de document rend exactement la meme vue.
|
||||||
tag_id: null,
|
const currentFilters = ref(lireFiltres(route.query, SCHEMA_DEPENSES))
|
||||||
fournisseurs: [],
|
|
||||||
date_debut: null,
|
|
||||||
date_fin: null
|
|
||||||
}
|
|
||||||
|
|
||||||
const summary = ref({
|
const summary = ref({
|
||||||
total_count: 0,
|
total_count: 0,
|
||||||
@@ -86,7 +84,11 @@ const lastUpdate = ref(null)
|
|||||||
let debounceTimer = null
|
let debounceTimer = null
|
||||||
|
|
||||||
function onFiltersChange(newFilters) {
|
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
|
// Debounce
|
||||||
if (debounceTimer) clearTimeout(debounceTimer)
|
if (debounceTimer) clearTimeout(debounceTimer)
|
||||||
@@ -95,17 +97,17 @@ function onFiltersChange(newFilters) {
|
|||||||
}, 300)
|
}, 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() {
|
function buildQueryParams() {
|
||||||
const params = new URLSearchParams()
|
const params = new URLSearchParams()
|
||||||
|
const query = ecrireFiltres(currentFilters.value, SCHEMA_DEPENSES)
|
||||||
|
|
||||||
if (currentFilters.immeuble_id !== null) params.append('immeuble_id', currentFilters.immeuble_id)
|
for (const [cle, valeur] of Object.entries(query)) {
|
||||||
if (currentFilters.lot_id !== null) params.append('lot_id', currentFilters.lot_id)
|
for (const unitaire of Array.isArray(valeur) ? valeur : [valeur]) {
|
||||||
if (currentFilters.categorie !== null) params.append('categorie', currentFilters.categorie)
|
params.append(cle, unitaire)
|
||||||
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)
|
|
||||||
|
|
||||||
return params.toString()
|
return params.toString()
|
||||||
}
|
}
|
||||||
@@ -115,7 +117,7 @@ async function loadData() {
|
|||||||
const summaryUrl = queryString ? `/api/analytics/depenses/summary?${queryString}` : '/api/analytics/depenses/summary'
|
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'
|
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)
|
console.log('Summary URL:', summaryUrl)
|
||||||
|
|
||||||
isLoadingDepenses.value = true
|
isLoadingDepenses.value = true
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Filters -->
|
<!-- Filters -->
|
||||||
<RevenusFilterPanel @filter-change="onFiltersChange" />
|
<RevenusFilterPanel :filtres-initiaux="currentFilters" @filter-change="onFiltersChange" />
|
||||||
|
|
||||||
<!-- KPI Cards -->
|
<!-- KPI Cards -->
|
||||||
<RevenusKpiCards :kpis="summary.kpis" />
|
<RevenusKpiCards :kpis="summary.kpis" />
|
||||||
@@ -52,6 +52,8 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { SCHEMA_REVENUS, ecrireFiltres, lireFiltres } from '../utils/filtresUrl.js'
|
||||||
import RevenusFilterPanel from '../components/revenus/RevenusFilterPanel.vue'
|
import RevenusFilterPanel from '../components/revenus/RevenusFilterPanel.vue'
|
||||||
import RevenusKpiCards from '../components/revenus/RevenusKpiCards.vue'
|
import RevenusKpiCards from '../components/revenus/RevenusKpiCards.vue'
|
||||||
import RevenusMonthlyChart from '../components/revenus/RevenusMonthlyChart.vue'
|
import RevenusMonthlyChart from '../components/revenus/RevenusMonthlyChart.vue'
|
||||||
@@ -59,15 +61,12 @@ import RevenusImmeubleChart from '../components/revenus/RevenusImmeubleChart.vue
|
|||||||
import TopImpayesList from '../components/revenus/TopImpayesList.vue'
|
import TopImpayesList from '../components/revenus/TopImpayesList.vue'
|
||||||
import RevenusTable from '../components/revenus/RevenusTable.vue'
|
import RevenusTable from '../components/revenus/RevenusTable.vue'
|
||||||
|
|
||||||
// Current filters state
|
const route = useRoute()
|
||||||
let currentFilters = {
|
const router = useRouter()
|
||||||
immeuble_id: null,
|
|
||||||
type_ligne: null,
|
// Les filtres vivent dans l'URL : la vue se recharge, se partage, et l'editeur
|
||||||
date_debut: null,
|
// de document sait y ramener une fois l'enregistrement termine.
|
||||||
date_fin: null,
|
const currentFilters = ref(lireFiltres(route.query, SCHEMA_REVENUS))
|
||||||
impayes_only: false,
|
|
||||||
months: 12
|
|
||||||
}
|
|
||||||
|
|
||||||
const summary = ref({
|
const summary = ref({
|
||||||
kpis: {
|
kpis: {
|
||||||
@@ -95,7 +94,11 @@ const lastUpdate = ref(null)
|
|||||||
let debounceTimer = null
|
let debounceTimer = null
|
||||||
|
|
||||||
function onFiltersChange(newFilters) {
|
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_REVENUS) })
|
||||||
|
|
||||||
// Debounce
|
// Debounce
|
||||||
if (debounceTimer) clearTimeout(debounceTimer)
|
if (debounceTimer) clearTimeout(debounceTimer)
|
||||||
@@ -106,21 +109,23 @@ function onFiltersChange(newFilters) {
|
|||||||
|
|
||||||
function buildSummaryParams() {
|
function buildSummaryParams() {
|
||||||
const params = new URLSearchParams()
|
const params = new URLSearchParams()
|
||||||
|
const filtres = currentFilters.value
|
||||||
|
|
||||||
if (currentFilters.months) params.append('months', currentFilters.months)
|
if (filtres.months) params.append('months', filtres.months)
|
||||||
if (currentFilters.immeuble_id !== null) params.append('immeuble_id', currentFilters.immeuble_id)
|
if (filtres.immeuble_id !== null) params.append('immeuble_id', filtres.immeuble_id)
|
||||||
|
|
||||||
return params.toString()
|
return params.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildDetailsParams() {
|
function buildDetailsParams() {
|
||||||
const params = new URLSearchParams()
|
const params = new URLSearchParams()
|
||||||
|
const filtres = currentFilters.value
|
||||||
|
|
||||||
if (currentFilters.immeuble_id !== null) params.append('immeuble_id', currentFilters.immeuble_id)
|
if (filtres.immeuble_id !== null) params.append('immeuble_id', filtres.immeuble_id)
|
||||||
if (currentFilters.type_ligne !== null) params.append('type_ligne', currentFilters.type_ligne)
|
if (filtres.type_ligne !== null) params.append('type_ligne', filtres.type_ligne)
|
||||||
if (currentFilters.date_debut) params.append('date_debut', currentFilters.date_debut)
|
if (filtres.date_debut) params.append('date_debut', filtres.date_debut)
|
||||||
if (currentFilters.date_fin) params.append('date_fin', currentFilters.date_fin)
|
if (filtres.date_fin) params.append('date_fin', filtres.date_fin)
|
||||||
if (currentFilters.impayes_only) params.append('impayes_only', 'true')
|
if (filtres.impayes_only) params.append('impayes_only', 'true')
|
||||||
params.append('limit', '500')
|
params.append('limit', '500')
|
||||||
|
|
||||||
return params.toString()
|
return params.toString()
|
||||||
@@ -136,7 +141,7 @@ async function loadData() {
|
|||||||
const summaryUrl = summaryParams ? `/api/revenus/summary?${summaryParams}` : '/api/revenus/summary'
|
const summaryUrl = summaryParams ? `/api/revenus/summary?${summaryParams}` : '/api/revenus/summary'
|
||||||
const detailsUrl = detailsParams ? `/api/revenus/details?${detailsParams}` : '/api/revenus/details'
|
const detailsUrl = detailsParams ? `/api/revenus/details?${detailsParams}` : '/api/revenus/details'
|
||||||
|
|
||||||
console.log('Loading revenus data with filters:', currentFilters)
|
console.log('Loading revenus data with filters:', currentFilters.value)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [summaryRes, detailsRes] = await Promise.all([
|
const [summaryRes, detailsRes] = await Promise.all([
|
||||||
|
|||||||
143
frontend/src/utils/filtresUrl.js
Normal file
143
frontend/src/utils/filtresUrl.js
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
// Les filtres d'une page d'analyse vivent dans son URL : une vue filtree se
|
||||||
|
// recharge, se partage et se remet en signet, et le retour depuis l'editeur de
|
||||||
|
// document n'a plus qu'une chaine a transporter.
|
||||||
|
//
|
||||||
|
// Chaque champ declare comment il se lit depuis la query et comment il s'y
|
||||||
|
// ecrit. Ce qui vaut sa valeur par defaut n'est pas ecrit : l'URL ne porte que
|
||||||
|
// ce qui a ete choisi, et reste lisible.
|
||||||
|
|
||||||
|
/** Un entier (identifiant). `tag_id=0` est une valeur, pas une absence. */
|
||||||
|
export function entier({ param } = {}) {
|
||||||
|
return {
|
||||||
|
param,
|
||||||
|
defaut: null,
|
||||||
|
lire(brut) {
|
||||||
|
const valeur = Number(premier(brut))
|
||||||
|
return Number.isInteger(valeur) ? valeur : null
|
||||||
|
},
|
||||||
|
ecrire(valeur) {
|
||||||
|
return valeur === null || valeur === undefined ? undefined : String(valeur)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Un texte libre choisi dans une liste fermee (categorie, type de ligne). */
|
||||||
|
export function texte({ param } = {}) {
|
||||||
|
return {
|
||||||
|
param,
|
||||||
|
defaut: null,
|
||||||
|
lire(brut) {
|
||||||
|
const valeur = premier(brut)
|
||||||
|
return valeur ? valeur : null
|
||||||
|
},
|
||||||
|
ecrire(valeur) {
|
||||||
|
return valeur ? valeur : undefined
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Une date ISO. Un format inattendu est ignore plutot qu'affiche de travers. */
|
||||||
|
export function date({ param } = {}) {
|
||||||
|
return {
|
||||||
|
param,
|
||||||
|
defaut: null,
|
||||||
|
lire(brut) {
|
||||||
|
const valeur = premier(brut)
|
||||||
|
return valeur && /^\d{4}-\d{2}-\d{2}$/.test(valeur) ? valeur : null
|
||||||
|
},
|
||||||
|
ecrire(valeur) {
|
||||||
|
return valeur ? valeur : undefined
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Un booleen a un seul sens : seul `true` s'ecrit, l'absence vaut faux. */
|
||||||
|
export function booleen({ param } = {}) {
|
||||||
|
return {
|
||||||
|
param,
|
||||||
|
defaut: false,
|
||||||
|
lire(brut) {
|
||||||
|
return premier(brut) === 'true'
|
||||||
|
},
|
||||||
|
ecrire(valeur) {
|
||||||
|
return valeur ? 'true' : undefined
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Un entier borne avec une valeur par defaut (nombre de mois d'historique). */
|
||||||
|
export function entierBorne({ defaut, min, max, param } = {}) {
|
||||||
|
return {
|
||||||
|
param,
|
||||||
|
defaut,
|
||||||
|
lire(brut) {
|
||||||
|
const valeur = Number(premier(brut))
|
||||||
|
if (!Number.isInteger(valeur) || valeur < min || valeur > max) return defaut
|
||||||
|
return valeur
|
||||||
|
},
|
||||||
|
ecrire(valeur) {
|
||||||
|
return valeur === undefined || valeur === defaut ? undefined : String(valeur)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Une liste de textes, un parametre repete par valeur retenue. */
|
||||||
|
export function liste({ param } = {}) {
|
||||||
|
return {
|
||||||
|
param,
|
||||||
|
defaut: [],
|
||||||
|
lire(brut) {
|
||||||
|
if (brut === undefined || brut === null) return []
|
||||||
|
const valeurs = Array.isArray(brut) ? brut : [brut]
|
||||||
|
return valeurs.filter((valeur) => typeof valeur === 'string' && valeur !== '')
|
||||||
|
},
|
||||||
|
ecrire(valeur) {
|
||||||
|
return valeur && valeur.length ? [...valeur] : undefined
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Les filtres d'une page, reconstruits depuis la query d'une route. */
|
||||||
|
export function lireFiltres(query, schema) {
|
||||||
|
const filtres = {}
|
||||||
|
for (const [nom, champ] of Object.entries(schema)) {
|
||||||
|
filtres[nom] = champ.lire((query ?? {})[champ.param ?? nom])
|
||||||
|
}
|
||||||
|
return filtres
|
||||||
|
}
|
||||||
|
|
||||||
|
/** La query correspondant a des filtres : seuls les choix explicites y figurent. */
|
||||||
|
export function ecrireFiltres(filtres, schema) {
|
||||||
|
const query = {}
|
||||||
|
for (const [nom, champ] of Object.entries(schema)) {
|
||||||
|
const ecrit = champ.ecrire(filtres[nom])
|
||||||
|
if (ecrit !== undefined) query[champ.param ?? nom] = ecrit
|
||||||
|
}
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
|
||||||
|
/** vue-router rend une chaine ou un tableau selon le nombre d'occurrences. */
|
||||||
|
function premier(brut) {
|
||||||
|
return Array.isArray(brut) ? brut[0] : brut
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Filtres de la page Depenses. `fournisseur` se repete, comme cote API. */
|
||||||
|
export const SCHEMA_DEPENSES = {
|
||||||
|
immeuble_id: entier(),
|
||||||
|
lot_id: entier(),
|
||||||
|
categorie: texte(),
|
||||||
|
tag_id: entier(),
|
||||||
|
fournisseurs: liste({ param: 'fournisseur' }),
|
||||||
|
date_debut: date(),
|
||||||
|
date_fin: date(),
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Filtres de la page Recettes. */
|
||||||
|
export const SCHEMA_REVENUS = {
|
||||||
|
immeuble_id: entier(),
|
||||||
|
type_ligne: texte(),
|
||||||
|
date_debut: date(),
|
||||||
|
date_fin: date(),
|
||||||
|
impayes_only: booleen(),
|
||||||
|
months: entierBorne({ defaut: 12, min: 3, max: 24 }),
|
||||||
|
}
|
||||||
99
frontend/tests/filtresUrl.test.js
Normal file
99
frontend/tests/filtresUrl.test.js
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
// Les filtres d'une page d'analyse transitent par son URL. Deux exigences :
|
||||||
|
// ce qu'on relit doit etre ce qu'on a choisi, et une query fabriquee a la main
|
||||||
|
// ne doit jamais produire une vue incoherente.
|
||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
|
import {
|
||||||
|
SCHEMA_DEPENSES,
|
||||||
|
SCHEMA_REVENUS,
|
||||||
|
ecrireFiltres,
|
||||||
|
lireFiltres,
|
||||||
|
} from '../src/utils/filtresUrl.js'
|
||||||
|
|
||||||
|
describe('filtres de la page Depenses', () => {
|
||||||
|
it('rend une URL vide quand rien n est filtre', () => {
|
||||||
|
const filtres = lireFiltres({}, SCHEMA_DEPENSES)
|
||||||
|
|
||||||
|
expect(filtres).toEqual({
|
||||||
|
immeuble_id: null,
|
||||||
|
lot_id: null,
|
||||||
|
categorie: null,
|
||||||
|
tag_id: null,
|
||||||
|
fournisseurs: [],
|
||||||
|
date_debut: null,
|
||||||
|
date_fin: null,
|
||||||
|
})
|
||||||
|
expect(ecrireFiltres(filtres, SCHEMA_DEPENSES)).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('retrouve a l identique les filtres qu il a ecrits', () => {
|
||||||
|
const choisis = {
|
||||||
|
immeuble_id: 3,
|
||||||
|
lot_id: 12,
|
||||||
|
categorie: 'TRAVAUX',
|
||||||
|
tag_id: 7,
|
||||||
|
fournisseurs: ['PPR', 'MAILLET'],
|
||||||
|
date_debut: '2026-01-01',
|
||||||
|
date_fin: '2026-06-30',
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = ecrireFiltres(choisis, SCHEMA_DEPENSES)
|
||||||
|
|
||||||
|
expect(query.fournisseur).toEqual(['PPR', 'MAILLET'])
|
||||||
|
expect(lireFiltres(query, SCHEMA_DEPENSES)).toEqual(choisis)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('garde « sans tag » qui vaut zero, la ou l absence de filtre vaut null', () => {
|
||||||
|
// Le piege du filtre tag : 0 est un choix, pas une case vide.
|
||||||
|
expect(lireFiltres({ tag_id: '0' }, SCHEMA_DEPENSES).tag_id).toBe(0)
|
||||||
|
expect(ecrireFiltres({ tag_id: 0 }, SCHEMA_DEPENSES)).toEqual({ tag_id: '0' })
|
||||||
|
expect(ecrireFiltres({ tag_id: null }, SCHEMA_DEPENSES)).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('accepte un fournisseur seul, que vue-router rend comme une chaine', () => {
|
||||||
|
expect(lireFiltres({ fournisseur: 'PPR' }, SCHEMA_DEPENSES).fournisseurs).toEqual(['PPR'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('ignore une valeur qui n a pas de sens plutot que d afficher de travers', () => {
|
||||||
|
const filtres = lireFiltres(
|
||||||
|
{ immeuble_id: 'abc', tag_id: '3.5', date_debut: '01/02/2026', fournisseur: '' },
|
||||||
|
SCHEMA_DEPENSES
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(filtres.immeuble_id).toBeNull()
|
||||||
|
expect(filtres.tag_id).toBeNull()
|
||||||
|
expect(filtres.date_debut).toBeNull()
|
||||||
|
expect(filtres.fournisseurs).toEqual([])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('filtres de la page Recettes', () => {
|
||||||
|
it('n ecrit pas l historique quand il vaut sa valeur par defaut', () => {
|
||||||
|
expect(ecrireFiltres(lireFiltres({}, SCHEMA_REVENUS), SCHEMA_REVENUS)).toEqual({})
|
||||||
|
expect(lireFiltres({}, SCHEMA_REVENUS).months).toBe(12)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('retrouve a l identique les filtres qu il a ecrits', () => {
|
||||||
|
const choisis = {
|
||||||
|
immeuble_id: 2,
|
||||||
|
type_ligne: 'loyer',
|
||||||
|
date_debut: '2026-01-01',
|
||||||
|
date_fin: null,
|
||||||
|
impayes_only: true,
|
||||||
|
months: 24,
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(lireFiltres(ecrireFiltres(choisis, SCHEMA_REVENUS), SCHEMA_REVENUS)).toEqual(choisis)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('ramene un historique hors bornes a sa valeur par defaut', () => {
|
||||||
|
expect(lireFiltres({ months: '999' }, SCHEMA_REVENUS).months).toBe(12)
|
||||||
|
expect(lireFiltres({ months: '0' }, SCHEMA_REVENUS).months).toBe(12)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('ne retient « impayes uniquement » que sur un oui explicite', () => {
|
||||||
|
expect(lireFiltres({ impayes_only: 'true' }, SCHEMA_REVENUS).impayes_only).toBe(true)
|
||||||
|
expect(lireFiltres({ impayes_only: 'oui' }, SCHEMA_REVENUS).impayes_only).toBe(false)
|
||||||
|
expect(ecrireFiltres({ impayes_only: false }, SCHEMA_REVENUS)).toEqual({})
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user