From 24420027a591348ca327198dc8a33b0a8327b152 Mon Sep 17 00:00:00 2001 From: Bertrand Benjamin Date: Sat, 22 Aug 2026 17:26:33 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20ouvre=20le=20choix=20de=20la=20p=C3=A9r?= =?UTF-8?q?iode=20sur=20la=20fiche=20d'un=20lot?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Un second sélecteur accompagne celui du lot : tout l'historique, trois mois, six mois, un an, deux ans, cinq ans. Le défaut reste l'historique entier — un filtre par défaut cacherait des opérations dès l'arrivée sur la page, sans que rien ne le signale. La période part dans l'URL, comme le lot : une fiche filtrée se partage et se recharge telle qu'on la lisait. Une valeur qui ne correspond à aucune durée proposée est ignorée et retirée de l'adresse. Le filtrage est demandé au serveur plutôt que rejoué ici : le taux de recouvrement et le restant dû sortent de requêtes SQL que le navigateur ne peut pas refaire, et les recalculer de son côté les ferait diverger de la chronologie affichée juste dessous. Ce que la fenêtre écarte reste dit, là où le lecteur pourrait le croire inexistant : - un bandeau donne les bornes retenues, précise qu'elles sont calées sur le dernier compte rendu du lot et non sur aujourd'hui, compte les lignes antérieures exclues et offre de tout réafficher ; - sous la courbe, le nombre de mois non tracés, et le rappel que le loyer en vigueur, sa date d'effet et la comparaison au parc restent lus sur tout l'historique — sans quoi « depuis mai 26 » au-dessus d'un graphe qui commence en mai passerait pour une contradiction ; - à côté des noms, le nombre d'occupants antérieurs à la période ; - le bloc des régularisations hors courbe s'affiche même quand la période n'en laisse aucune à lister, pour dire qu'il en existe. Co-Authored-By: Claude Opus 5 --- frontend/src/components/lot/LoyerLot.vue | 43 ++++++-- frontend/src/pages/LotPage.vue | 119 ++++++++++++++++++++--- 2 files changed, 140 insertions(+), 22 deletions(-) diff --git a/frontend/src/components/lot/LoyerLot.vue b/frontend/src/components/lot/LoyerLot.vue index b78365d..8aa0955 100644 --- a/frontend/src/components/lot/LoyerLot.vue +++ b/frontend/src/components/lot/LoyerLot.vue @@ -24,6 +24,17 @@
+ + +

+ Courbe limitée à la période choisie : + {{ loyer.mois_masques }} + {{ loyer.mois_masques > 1 ? 'mois antérieurs ne sont pas tracés' : 'mois antérieur n\'est pas tracé' }}. + Le loyer en vigueur, sa date d'effet et la comparaison au parc restent lus + sur tout l'historique. +

-
+ des montants disparaîtraient de la page sans que rien ne le signale. + Le bloc s'affiche aussi quand la période n'en laisse aucune, pour dire + qu'il en existe. --> +
+

- Hors de la courbe · {{ loyer.hors_courbe.length }} + Hors de la courbe

- Régularisations à cheval sur plusieurs mois sans en couvrir aucun entièrement. - Les rattacher à un mois inventerait un loyer que le compte rendu ne porte pas ; - elles figurent dans la chronologie ci-dessous. + +

- +
diff --git a/frontend/src/pages/LotPage.vue b/frontend/src/pages/LotPage.vue index 26e69fb..f0ff980 100644 --- a/frontend/src/pages/LotPage.vue +++ b/frontend/src/pages/LotPage.vue @@ -9,12 +9,20 @@

- +
+ + + +
@@ -58,9 +66,53 @@ :key="nom" class="badge badge-neutral ml-2" >{{ nom }} + + + + + {{ analyse.identite.locataires_masques }} + {{ analyse.identite.locataires_masques > 1 ? 'occupants antérieurs' : 'occupant antérieur' }} + à la période +
+ +
+

+ Du {{ formatDate(analyse.periode.debut) }} + au {{ formatDate(analyse.periode.fin) }} + + · calé sur le dernier compte rendu du lot, pas sur aujourd'hui + +

+

+ + +

+
+ + +

+ Aucun compte rendu ne porte de ligne sur ce lot : il n'y a pas de période à + borner, et la fiche reste entière. +

+
@@ -244,6 +296,24 @@ const chargement = ref(true) const depensesSeules = ref(false) const detailsOuverts = reactive({}) +// `null` en tête : la fiche s'ouvre sur tout l'historique, et c'est +// l'utilisateur qui décide de restreindre. Les durées montent jusqu'à cinq ans, +// au-delà desquels « tout » revient au même sur un parc suivi depuis 2024. +const PERIODES = [ + { mois: null, libelle: "Tout l'historique" }, + { mois: 3, libelle: '3 derniers mois' }, + { mois: 6, libelle: '6 derniers mois' }, + { mois: 12, libelle: 'Dernière année' }, + { mois: 24, libelle: '2 dernières années' }, + { mois: 60, libelle: '5 dernières années' } +] + +const moisChoisis = ref(null) + +// Une fenêtre n'est active que si le serveur a pu la caler : sur un lot sans +// aucune ligne, il n'y a pas de dernier compte rendu et donc pas de bornes. +const fenetreActive = computed(() => analyse.value?.periode?.debut != null) + const CHAMPS_IDENTITE = [ { cle: 'type_effectif', libelle: 'Type' }, { cle: 'surface', libelle: 'Surface', unite: ' m²' }, @@ -357,39 +427,58 @@ async function chargerLots() { lots.value = await response.json() } -async function chargerAnalyse(lotId) { +async function chargerAnalyse(lotId, mois) { analyse.value = null // Les dépliages appartiennent au lot affiché : gardés, une entreprise // présente sur deux lots (PPR par exemple) arriverait déjà ouverte. for (const fournisseur of Object.keys(detailsOuverts)) delete detailsOuverts[fournisseur] - const response = await fetch(`${API}/api/lots/${lotId}/analyse`) + // Le filtre est appliqué côté serveur : les chiffres (taux de recouvrement, + // restant dû) sortent de requêtes SQL que le navigateur ne peut pas rejouer, + // et les recalculer ici les ferait diverger de la chronologie. + const parametres = mois == null ? '' : `?mois=${mois}` + const response = await fetch(`${API}/api/lots/${lotId}/analyse${parametres}`) if (response.ok) analyse.value = await response.json() } -// L'URL porte le lot : une fiche se partage et se recharge sans repasser par -// le sélecteur. -watch(lotChoisi, (lotId) => { +// L'URL porte le lot et la période : une fiche se partage et se recharge telle +// qu'on la lisait, filtre compris. +watch([lotChoisi, moisChoisis], ([lotId, mois]) => { if (lotId == null) return - if (String(lotId) !== route.params.id) router.replace(`/lots/${lotId}`) - chargerAnalyse(lotId) + + const query = mois == null ? {} : { mois: String(mois) } + if (String(lotId) !== route.params.id || query.mois !== route.query.mois) { + router.replace({ path: `/lots/${lotId}`, query }) + } + chargerAnalyse(lotId, mois) }) +/** Période lue dans l'URL, ramenée à une des durées proposées. */ +function moisDeLUrl() { + const demande = Number(route.query.mois) + return PERIODES.some((periode) => periode.mois === demande) ? demande : null +} + // Passer d'un lot à l'autre reste la même route : Vue réutilise le composant // sans le remonter, et `onMounted` ne rejoue pas. Sans ce suivi, un clic dans // le nuage du parc changerait l'adresse en laissant la fiche du lot précédent // à l'écran — l'écart le plus trompeur qui soit entre l'URL et ce qu'on lit. +// Le retour arrière du navigateur passe par le même chemin, période comprise. watch( - () => route.params.id, - (id) => { + [() => route.params.id, () => route.query.mois], + ([id]) => { const demande = Number(id) if (Number.isFinite(demande) && demande !== lotChoisi.value) lotChoisi.value = demande + moisChoisis.value = moisDeLUrl() } ) onMounted(async () => { try { await chargerLots() + // Avant le lot : les deux changent dans le même tick, le watcher ne part + // qu'une fois et la première requête porte déjà la période de l'URL. + moisChoisis.value = moisDeLUrl() const demande = Number(route.params.id) lotChoisi.value = lots.value.some((lot) => lot.id === demande) ? demande
Période