+ 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 · {{ loyer.hors_courbe.length }}
- 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.
+
+ 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.
+
+ {{ loyer.hors_courbe_masquees }}
+ {{ loyer.hors_courbe_masquees > 1 ? 'autres sont antérieures' : 'autre est antérieure' }}
+ à la période choisie.
+
+
+
+ {{ loyer.hors_courbe_masquees }}
+ {{ loyer.hors_courbe_masquees > 1 ? 'régularisations antérieures' : 'régularisation antérieure' }}
+ à la période choisie : élargir la période
+ {{ loyer.hors_courbe_masquees > 1 ? 'les ramène' : 'la ramène' }} ici.
+
@@ -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
+
+
+
+
+ {{ analyse.periode.lignes_masquees }}
+ {{ analyse.periode.lignes_masquees > 1 ? 'lignes antérieures sont exclues' : 'ligne antérieure est exclue' }}
+ des chiffres, de la chronologie et des intervenants ; la courbe du loyer
+ suit la même période.
+
+
+
+ Aucune ligne du lot ne tombe hors de cette fenêtre : les chiffres sont
+ ceux de tout son historique.
+
+
+
+
+
+
+ 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