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