---
suivi: 1581
date: 2026-09-16
sujet: Supervision du patrimoine foncier
chantier: foncier / supervision
type: feature
statut: poussé
hash: c5f7052c
fichiers:
  - config/foncier.php
  - app/Services/Foncier/ParcelleSupervisionQuery.php
  - app/Http/Controllers/Foncier/ParcelleSupervisionController.php
  - app/Services/Foncier/ParcelleProspectionQuery.php
  - app/Http/Controllers/Foncier/ProspectionController.php
  - resources/js/Pages/Foncier/Supervision/Index.vue
  - resources/js/Layouts/AppShell.vue
  - routes/web.php
  - tests/Unit/ParcelleSupervision1581SourceTest.php
  - database/data/journal_mises_a_jour_post_2026_06_12.php
  - public/build/
  - docs/suivi/SUIVI_1581_supervision_patrimoine_foncier.md
---

# SUIVI #1581 — Supervision du patrimoine foncier

## PROMPT ENVOYÉ

Écran Supervision : où est l’argent immobilisé et ce qui appelle une
décision. Quatre indicateurs, six bulles cliquables, répartition,
qualité des données. Filtres SQL. Pas de colonne ni table. Zéros
affichés. Dates de référence à confirmer depuis le code.

## FICHES LUES

- **#1579 LEÇON** — dernière activité calculée en lecture
  (`DossierFoncierSuiviAggregator`), pas de colonne. Impact : SQL
  GREATEST des mêmes sources, pas N+1, pas de colonne d’autorité.
- **#1576 LEÇON** — `date_derniere_reprise` jamais écrite, `updated_at`
  n’est pas un proxy. Accès `foncier.lecture` + ACL, bypass DG /
  GEMME_PDG / DSI / ADMIN_ERP sur les dossiers. Impact : pas
  d’updated_at ; visibilité parcelles via le scope existant.
- **#1569 / #1568 / #1567** — statut déduit, destination, responsable.
  Non modifiés. L’écran lit `parcelles.statut` / `destination` /
  `responsable_id`.
- **#1514** — bypass parcelle ≠ bypass dossier
  (`BYPASS_PARCELLE_VIEW_GROUPS` plus étroit).

## VISIBILITÉ (établie avant code)

Accès écran : permission `foncier.lecture` (`abort_unless` +
`m365GroupNames()` brut via `PermissionService`). Middleware du groupe
routes Foncier déjà `feature.permission:foncier.lecture`. Menu Foncier
= `FoncierContext::menuVisible` = même permission + module enseigne.

Mapping par défaut : `GS_HECTARE` et `GS_GEMME_PDG` ont
`foncier.lecture`. `GS_DSI` / `GS_ADMIN_ERP` ont `*`.
`GS_DIRECTION_GENERALE` a `!foncier.*` : un compte qui n’a QUE ce
groupe ne voit pas le module Foncier (règle existante, pas changée).
Un dirigeant qui accède a donc un second groupe (HECTARE, ADMIN, DSI,
GEMME_PDG) ou un mapping custom.

Périmètre des chiffres : `Parcelle::query()` donc
`FoncierParcelleVisibiliteScope` (créateur ∪ détention/dossier ∪
bypass `GS_DIRECTION_GENERALE` / `GS_ADMIN_ERP` /
`GS_HECTARE_DIRECTION`). Un développeur GS_HECTARE voit son périmètre
ACL, pas tout le parc. Le filtre développeur n’est PAS appliqué en
plus : l’ACL suffit. GEMME_PDG et DSI voient les dossiers (bypass
dossier) mais PAS tout le parc (bypass parcelle plus étroit).

Filtre développeur (dropdown) ouvert seulement si
`m365GroupNames()` contient `GS_DIRECTION_GENERALE`, `GS_GEMME_PDG`,
`GS_ADMIN_ERP`, `GS_DSI` ou `GS_HECTARE_DIRECTION`. Un `responsable_id`
posté par un autre compte est ignoré côté serveur (pas un `v-if`).

`Gate::before` non utilisé (compte Robin faussement positif).
Le test « compte non-DG » ne vaut rien avec le compte de Robin.

## DATES DE RÉFÉRENCE (confirmées dans le code)

| Bulle | Date retenue | Source |
|---|---|---|
| Réserve dormante | MIN(`actes_fonciers.date_acte`) via `acte_parcelle` | ActeFoncier — pas de `date_signature` sur l’acte |
| Compromis qui traînent | MIN(`promesses.date_signature`) des promesses `statut=signe` via `promesse_parcelle` | même pivot que #1569 `promesse_signee` |
| Opportunité en sommeil | MAX des événements du fil #1579, au niveau parcelle (MAX des dossiers liés) | SQL UNION : created_at dossier, pivot parcelle, offres, date_signature promesse, date_acte, échanges.date, tâches attachées, audits statut/périmètre, documents dossier/offre/promesse/acte |
| Explorée à relancer | identique (fil #1579) | idem |

Non utilisées : `dossiers_fonciers` date d’ouverture seule,
`updated_at`, `date_derniere_reprise` (n’existe pas).

## BULLES NON CALCULABLES

Si le numérateur fiable est 0 ET qu’il reste des parcelles du même
statut sans date : la bulle affiche « non calculable » + la raison.
Sinon le chiffre est celui des parcelles datées ; les sans-date sont
signalées en petit et vont aussi dans Qualité (détenue sans acte, etc.).

- Réserve sans `date_acte` → non mesurable (qualité : détenue sans acte).
- Sous compromis sans `date_signature` → non mesurable.
- Opportunité / Explorée sans dossier → théoriquement rare (le statut
  #1569 exige un dossier) ; filet SQL quand même.

## REQUÊTES DE LA PAGE

Cinq requêtes métier prévues, toutes SQL, pas de reduce JS :

1. options départements (DISTINCT)
2. options communes (DISTINCT, cascade département)
3. options développeurs (DISTINCT users)
4. agrégat indicateurs + bulles + qualité (un SELECT, CASE / SUM)
5. répartition GROUP BY développeur ou commune

Le scope visibilité ajoute un `whereExists` à chaque query Eloquent.
`nb_requetes` est mesuré par `DB::enableQueryLog` et renvoyé au payload
(affiché uniquement en source / synthèse, pas un widget métier).

Pas de N+1 : dernière activité = une sous-requête JOIN, pas
`pourDossier()` par ligne.

## SEUILS

`config/foncier.php` :

- `foncier.supervision.dormance_mois` = 24
- `foncier.supervision.sommeil_mois` = 6
- `foncier.supervision.compromis_mois` = 12

## MAPPING FILTRES (libellé affiché ↔ valeur postée ↔ back)

| Affiché | Posté | Back |
|---|---|---|
| Développeur foncier | `responsable_id` (id user ou vide) | `parcelles.responsable_id` |
| Département | `departement` (ex. 13) | `communes.departement` |
| Commune | `code_insee` (5 car.) | `parcelles.code_insee_commune` |
| Destination | `a_definir` / `support_operation` / `bouchon` / `reserve` / `cession` / `echange` / `retrocession` | `parcelles.destination` |

Bulles : `supervision` = `reserve_dormante` \| `detenu_sans_destination` \|
`opportunite_sommeil` \| `compromis_traine` \| `sorties` \|
`exploree_relancer` (+ `_sans_date` si non calculable) \| clés qualité.

## SYNTHÈSE

Route `/foncier/supervision`. Menu Foncier → Supervision. Drill-down
vers `/foncier/prospection?supervision=…` (mêmes règles SQL). Pas de
charges de portage. Pas d’alerte Teams. Statut / destination /
responsable / bâti / opposition / fil #1579 non modifiés.
Bundles : Supervision `Index-BmnxiCNm.js`, AppShell via `app-DsRp2nlf.js`.
Rebuild après rebase `d5e286e4` (#1584) : `DepotFactureSaisieForm-D55aGOlR.js` conservé.

## DÉPLOIEMENT-TEST

git pull origin main
php artisan optimize:clear
php artisan view:clear
php artisan route:clear
php artisan journal:sync
(pas de migrate)

## LEÇON

Trois « vues complètes » distinctes (dossier ACL, parcelle ACL,
prospection acces_total). Pour un écran patrimoine, le scope parcelle
est la vérité. La dernière activité #1579 n’a pas de colonne : la
reproduire en SQL plutôt que d’appeler l’agrégateur PHP par dossier.
Un zéro est une information. AppShell est une zone sérialisée : un
seul lot à la fois, rebase origin/main avant build.
