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:
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 }),
|
||||
}
|
||||
Reference in New Issue
Block a user