---
suivi: 1566
date: 2026-09-16
sujet: Diagnostic — données patrimoine PDG (capital immobilisé, dormance)
chantier: foncier / patrimoine / pdg
type: diagnostic
statut: poussé
hash: 07b93903
fichiers:
  - docs/suivi/SUIVI_1566_diag_patrimoine_pdg.md
---

# SUIVI #1566 — PHASE 1 diagnostic : données réelles pour un tableau de bord « Patrimoine foncier » PDG

## PROMPT ENVOYÉ

PHASE 1 lecture seule. Constat des données réellement présentes
(état/type, prix, portage, valo, développeur, géo, occupation,
cadastre, surfaces, volumétrie, perf) avant toute conception d’un
tableau de bord PDG centré sur le capital immobilisé et la dormance.
Aucune migration, aucun fix. Requêtes SQL à coller (pas d’accès MySQL).
Commit + push main. Pas de Vite.

## BASE GIT

HEAD local = `origin/main` = `a7f4fe2b` (#1565 extraction notice parcelles).
Aucun rebase. Aucun fichier applicatif modifié. Aucun MySQL. Aucun journal
(#412 : diagnostic invisible tant qu’un écran n’est pas livré).

## FICHES LUES

- **#1446 LEÇON** — fusion État + Type dans `etat_foncier` (13 puis 14
  valeurs) ; colonne `type_foncier` conservée sans copie. Impact : le
  référentiel arrêté (état × type distincts) n’est **pas** le schéma
  d’écriture actuel ; une parcelle « réserve foncière » a son vocation
  dans `etat_foncier`, pas dans `type_foncier`.
- **#1528 / #1530 LEÇON** — l’acte ne force plus `etat_foncier = acquise`
  ; détention groupe = `societe_detentrice_id`. Impact : filtrer
  « Acquise / Affectée » rate les réserves classées hors axe.
- **#1561 LEÇON** — 14e valeur `vendue_client` dans `ETATS_FONCIERS`.
- **#1562 LEÇON** — agrégat surface : exclure `statut_cadastral =
  remplacee` ; le dénombrement peut les inclure. Faisable en SQL.
- **#1447 LEÇON** — bloc Charges de portage masqué (`AFFICHER_CHARGES_PORTAGE
  = false`). Masquage UI ≠ table absente. Alimentation = SQL, pas
  l’existence du schéma.
- **#1120** — écran `/patrimoine` déjà en prod : parcelles avec
  `societe_detentrice_id`, hors cloisonnement, sans filtre d’affectation
  (Phase 3 absente).
- **#1130 / #1131 / #1119 / #1107 / #1134 / #1175 / #1074 / #1242 /
  #1341 / #1332 / #1564** — structure portage, valo, occupation, audit,
  frais d’acte, communes, division. Aucune de ces fiches ne mesure le
  remplissage prod.

---

## ÉCART RÉFÉRENTIEL ARRÊTÉ ↔ CODE ACTUEL (ne pas réinterpréter)

Le prompt donne un référentiel **arrêté** : état (cycle de vie, une
valeur) **et** type (vocation, indépendant). Le code **actuel** depuis
#1446 fusionne les deux dans **une seule** colonne `parcelles.etat_foncier`.
Ce diagnostic décrit le code et le schéma tels qu’ils sont. Il ne
choisit pas lequel des deux mondes est « le bon ».

Conséquence pour la dormance (définition de travail : acquise, non
affectée, type qui ne justifie pas la détention, détenue longtemps) :
un filtre `etat_foncier IN ('acquise') AND type_foncier NOT IN (…)`
est **inopérant** sur les parcelles dont la vocation a été saisie
comme état (`reserve_fonciere`, `opportunite`, `bouchon`, etc.).

---

## 1. ÉTAT ET TYPE FONCIER

### 1.1 Stockage

| Information | Colonne | Table | Nullable | Référentiel |
|-------------|---------|-------|----------|-------------|
| État foncier (tel que saisi aujourd’hui) | `etat_foncier` varchar(32) NOT NULL DEFAULT `'reperee'` | `parcelles` | non | `FoncierReferentiels::ETATS_FONCIERS` (`app/Support/Foncier/FoncierReferentiels.php:31-46`) |
| Type foncier (colonne d’origine #1242) | `type_foncier` varchar(32) | `parcelles` | **oui** | `FoncierReferentiels::TYPES_FONCIER` (`FoncierReferentiels.php:159-166`) |

Création de `etat_foncier` : `database/migrations/2026_07_30_140100_create_parcelles_table.php:29`.
Index `parcelles_etat_idx` (`:43`).

Création de `type_foncier` : `database/migrations/2026_08_07_151000_add_type_foncier_constructible_bati_parcelles.php:25-27`.
Index `parcelles_type_foncier_idx` (`:31-33`). Pas d’ENUM MySQL : varchar + `Rule::in` PHP.

Classe de référentiel : `App\Support\Foncier\FoncierReferentiels` (pas de table).

### 1.2 Mapping valeur technique ↔ libellé affiché

`ETATS_FONCIERS` (liste unique du sélecteur « État foncier » fiche /
création / division / pastilles — `optionsEtatsFonciers()`,
`ParcelleController.php:945`, `Parcelle.php:410-414`) :

| Valeur DB | Libellé UI | Famille dans le référentiel arrêté | Axe d’avancement (`ParcelleEtatAvancement.php:15-34`) |
|-----------|------------|-------------------------------------|------------------------------------------------------|
| `reperee` | Repérée | état | axe |
| `en_negociation` | En négociation | état | axe |
| `sous_compromis` | Sous compromis | état | axe |
| `acquise` | Acquise | état | axe |
| `affectee` | Affectée | état | axe |
| `pas_d_action` | Pas d'action | état | hors axe |
| `projet_abandonne` | Projet abandonné | état | hors axe |
| `reserve_fonciere` | Réserve foncière | **type** (constante `TYPE_FONCIER_RESERVE`) | hors axe |
| `opportunite` | Opportunité | **type** | hors axe |
| `bouchon` | Bouchon | **type** | hors axe |
| `vendue_client` | Vendue client | état (#1561) | hors axe |
| `cession` | Cession | **type** | hors axe |
| `retrocession` | Rétrocession | **type** | hors axe |
| `autre` | Autre | ni l’un ni l’autre (#1446) | hors axe |

`TYPES_FONCIER` (colonne `type_foncier`, création unitaire « Infos
avancées » `Create.vue:437-445`, filtre liste `ParcelleProspectionQuery.php:212-216`) :

| Valeur DB | Libellé |
|-----------|---------|
| `reserve_fonciere` | Réserve foncière |
| `opportunite` | Opportunité |
| `bouchon` | Bouchon |
| `cession` | Cession |
| `retrocession` | Rétrocession |

Fiche Show : libellé « Type foncier » **absent** (`Parcelle1446SourceTest.php:100`).
Le sélecteur visible s’appelle « État foncier » et poste `etat_foncier`
(`Show.vue:1238-1249`). Création unitaire : **les deux** champs
(`Create.vue:327-337` état obligatoire ; `:437-445` type optionnel).

### 1.3 Le type existe-t-il en base, ou seulement l’état ?

Les **deux** colonnes existent. Depuis #1446, l’écriture métier
usuelle de la vocation passe par `etat_foncier`. `type_foncier` n’a
pas été droppée (`Parcelle1446SourceTest.php:39-57`). Proportion
réellement remplie : **SQL Q1** (ne pas extrapoler).

### 1.4 Plusieurs types ? Nullable ?

Une parcelle = **une** valeur `type_foncier` (varchar, pas de pivot).
Nullable : oui (`migration #1242`). `etat_foncier` : NOT NULL, défaut
`reperee` (`migration #1074:29`, rappel #1564).

### 1.5 Historique des changements d’état

Pas de table dédiée `parcelle_etat_historique`.

Mécanisme : `entity_field_audits` (#1107, migration
`2026_07_30_231000_create_entity_field_audits_table.php`), append-only,
granularité **un champ**. `Parcelle::auditedFields()` inclut
`etat_foncier` et `type_foncier` (`Parcelle.php:31-54`). Trait
`AuditsFieldChanges` : hook Eloquent `updating` seulement
(`AuditsFieldChanges.php:45-47`).

Depuis quand : socle poussé le **2026-07-30** (#1107). Les parcelles
nées avant ou dont l’état a été écrit hors Eloquent n’ont pas
d’historique.

Trous (date d’entrée dans l’état courant **non garantie**) :

1. `Query Builder` `update()` : pas d’événement Eloquent — pas de
   ligne d’audit. Constaté #1528 pour l’ancien forçage `acquise`
   (`ActeAcquisitionService` avant #1530).
2. Valeur initiale à la création : le trait n’écoute pas `creating`.
   La date d’entrée dans `reperee` (défaut) = `parcelles.created_at`
   au mieux, pas un audit.
3. Promesse → `sous_compromis` : `saveQuietly` + `recordChange`
   explicite (`ParcelleEtatTransitionService.php:62-71`) — **tracé**.
4. Inline fiche : Eloquent `update` via whitelist — **tracé**.

On **peut** déduire la date d’entrée dans l’état courant **si** une
ligne `champ = 'etat_foncier'` existe pour la parcelle (MAX
`created_at` où `nouvelle_valeur` = état actuel). Sinon : NON
DÉTERMINÉ. Pas d’index sur `entity_field_audits.champ` (seulement
morph + `created_at`, migration `:32-33`).

Silence d’activité : **aucun** champ « dernière action métier ».
Proxies possibles (aucun n’est un silence métier) : `parcelles.updated_at`,
dernier `entity_field_audits` de la parcelle, `dossiers_fonciers.date_revue`
(uniquement si le dossier est en veille, `DossierFoncierController.php:1422-1425`).

### 1.6 Requête

Voir bloc SQL final, section `-- Q1`.

---

## 2. PRIX D’ACQUISITION

### 2.1 Où est le prix

| Porteur | Colonne | Rôle |
|---------|---------|------|
| **Acte** | `actes_fonciers.prix_numeraire` decimal(14,2) NOT NULL | Prix d’achat de l’acte (`migration 2026_07_30_170200:30`, UI « Prix d’achat * » `ActesFonciers/Create.vue:287-288`, validation required min:0 `ActeFoncierController.php:150`) |
| Pivot acte ↔ parcelle | `acte_parcelle.surface_acquise_m2` nullable | Surface acquise (#1127) ; **pas** de montant. Masquée #1530. |
| Parcelle | — | `Parcelle::sensitiveAuditedFields()` vide, commentaire « Aucun champ prix sur parcelle » (`Parcelle.php:58-65`) |
| Dossier | `prix_demande`, `prix_cible` | Prix **demandé / cible** de négociation, pas d’acquisition (`DossierFoncier.php:38-39, 84-85`) |
| Offre | `offres_foncieres.montant` | Offre formelle, pas l’acte (`OffreFonciere.php:36`) |
| Promesse | `promesses.prix_numeraire` | Prix promis ; l’acte a le sien |

### 2.2 Montant par parcelle

**Non.** Un acte porte **un** `prix_numeraire` pour **N** parcelles
(`parcelle_ids` array min:1, `ActeFoncierController.php:146-147` ;
`attach($parcelleIds)` sans ventilation, `ActeAcquisitionService.php:118`).

Clé de répartition existante : `acte_parcelle.surface_acquise_m2`
(nullable = totalité, #1127) **ou** à défaut `parcelles.surface_cadastrale_m2`.
Aucun code ne répartit le prix. L’écran Patrimoine **recopie le prix
d’acte entier sur chaque parcelle** (`PatrimoineFoncierQuery.php:92-155`).
Même logique dans `ParcelleValorisationService::prixAcquisitionReference`
(`:206-236`). Un acte multi-parcelles **surestime** le capital si on
somme les lignes.

Fiabilité « un montant par parcelle » : seulement si l’acte n’a
**qu’une** parcelle. Volume : **SQL Q2**.

### 2.3 HT ou TTC ? Frais de notaire ?

Aucune mention HT/TTC sur `prix_numeraire`. Libellé UI : « Prix
d’achat ». NON DÉTERMINÉ (pas de flag, pas d’aide de saisie).

Frais de notaire **séparés** : pas de colonne montant sur l’acte.
`notaire_fournisseur_id` = FK fournisseur (`ActeFoncier.php:75-78`),
pas un euro. Les `frais_notaire` du code sont ceux des **scénarios
de faisabilité** (`FaisabiliteLotissementMoteur`, hors acquisition
réelle). #1175 concerne la commercialisation (acte en main lots),
pas le foncier.

### 2.4 Requête

Voir `-- Q2`. Périmètre demandé : `etat_foncier IN ('acquise','affectee')`.
Complément obligatoire (écart #1446/#1530) : même décompte sur
`societe_detentrice_id IS NOT NULL` (définition Patrimoine #1120/#1246).

---

## 3. COÛT DE PORTAGE

### 3.1 La table existe

Oui. `charges_portage`, #1130, migration
`database/migrations/2026_07_31_170000_create_charges_portage_table.php`.
Modèle `app/Models/ChargePortage.php` (`protected $table = 'charges_portage'`).

Structure (migration `:28-60`) :

| Colonne | Type | Notes |
|---------|------|-------|
| `parcelle_id` | FK nullable, `nullOnDelete` | Rattachement parcelle |
| `acte_foncier_id` | FK nullable, `nullOnDelete` | Rattachement acte |
| `exercice` | unsignedSmallInteger | Année civile, pas une date |
| `type` | varchar(40) | Voir 3.3 |
| `depot_facture_id` | FK nullable | Lecture du montant ; jamais recopié |
| `montant_manuel` | decimal(15,2) nullable | XOR facture (service `:48-56`) |
| `commentaire` | text nullable | |
| `createur_id` | FK nullable | |

Index : `(parcelle_id, exercice)`, `(acte_foncier_id, exercice)`,
unique `(depot_facture_id, parcelle_id, exercice)`.

Garde applicative : parcelle **et/ou** acte (`ChargePortageService.php:37-41`).

### 3.2 Alimentée en production ?

**NON DÉTERMINÉ sans SQL.** L’existence du schéma ne dit rien.

Indice UI (pas une preuve de vide) : depuis #1447,
`AFFICHER_CHARGES_PORTAGE = false` (`Show.vue:446`, bloc `:2109`).
Les routes POST/DELETE existent encore (`routes/web.php` autour de
921-927). Une saisie API reste possible.

### 3.3 Natures prévues

`FoncierReferentiels::TYPES_CHARGE_PORTAGE` (`:507-514`) :

| Valeur | Libellé |
|--------|---------|
| `taxe_fonciere` | Taxe foncière |
| `interets_emprunt` | Intérêts d’emprunt |
| `entretien` | Entretien |
| `assurance` | Assurance |
| `gardiennage` | Gardiennage |
| `autre` | Autre |

`avis_impositions` (#1420) n’a **pas** de `parcelle_id`
(`AvisImposition.php:19-44`) : ce n’est pas un portage parcellaire.

### 3.4 Parcelle ou dossier ?

Parcelle et/ou acte. **Pas** de `dossier_foncier_id` sur
`charges_portage`.

Montant affiché : TTC facture si présent, sinon HT, sinon
`montant_manuel` (`ChargePortage.php:85-106`). Jamais stocké en
copie depuis la facture (#1130 D20).

### 3.5 Cumulé et annuel par parcelle

**Cumulé** : oui **si** des lignes ont `parcelle_id` — déjà calculé
pour Patrimoine (`ChargePortageService::cumulsPourParcelles`, `:149-176`).
Les charges **acte seul** (`parcelle_id` NULL) sont vues sur la fiche
parcelle (`synthetiserPourParcelle` `:126-133`) mais **exclues** du
cumul patrimoine.

**Annuel** : `exercice` permet un total **par année civile**, pas un
taux de portage annualisé (pas de date de début/fin de charge). Coût
annuel N = SUM des lignes de l’exercice N. Si la table est vide, les
deux indicateurs sont structurellement à 0 — ce n’est pas une
information métier.

### 3.6 Requête

Voir `-- Q3`.

---

## 4. VALORISATIONS

### 4.1 Table dédiée, historisée

Oui. `parcelle_valorisations` (#1131), migration
`2026_07_31_180000_create_parcelle_valorisations_table.php`.

| Colonne | Rôle |
|---------|------|
| `parcelle_id` | FK cascade |
| `date_valorisation` | date, obligatoire |
| `valeur` | decimal(15,2), obligatoire |
| `methode` | varchar(40) |
| `auteur_id` | FK user nullable |
| `commentaire` | text nullable |

Index `(parcelle_id, date_valorisation)`. Une ligne = une
réévaluation. Pas d’écrasement d’une « valeur courante » unique.
#1134 a levé la fenêtre 30 min : `modifier()` / `supprimer()` existent
(`ParcelleValorisationService.php:67-121`) ; l’audit porte la
traçabilité (`ParcelleValorisation::auditedFields()`, valeur en
`sensitiveAuditedFields`).

### 4.2 Dernière valorisation + date

Oui. Ordre code : `date_valorisation DESC, id DESC`
(`Parcelle.php:257-261`, `ParcelleProspectionQuery.php:413-418`,
`ParcelleValorisationService::dernieresPourParcelles` `:164-184`).
Déjà exposé sur `/patrimoine` (`PatrimoineFoncierQuery.php:163-171`).

HT/TTC : **NON DÉTERMINÉ** (colonne `valeur` sans flag).

### 4.3 Origine tracée

Oui, `methode` ∈ `METHODES_VALORISATION` (`FoncierReferentiels.php:528-534`) :

| Valeur | Libellé |
|--------|---------|
| `expertise` | Expertise |
| `comparaison` | Comparaison |
| `charge_fonciere` | Charge foncière |
| `interne` | Interne |
| `autre` | Autre |

Documents d’expertise possibles (`ParcelleValorisation.php:99-101`).
Répartition des méthodes : **SQL Q4**.

### 4.4 Requête

Voir `-- Q4`. Périmètre `acquise`/`affectee` + complément détention groupe.

---

## 5. DÉVELOPPEUR FONCIER

### 5.1 Sur la parcelle ?

Non. `parcelles` n’a pas de `responsable_id`. `createur_id` = auteur
de la fiche (`Parcelle.php:147-149`), pas le développeur métier.

### 5.2 Chemin

`parcelles` → `dossier_foncier_parcelle` → `dossiers_fonciers` →
`dossier_foncier_responsable` → `users`.

Pivot `dossier_foncier_responsable` : `dossier_foncier_id` + `user_id`,
unique couple (`migration 2026_07_30_150300:25-32`). Relation
`DossierFoncier::responsables()` N⇄N vers `User` (`DossierFoncier.php:201-204`).
Ce n’est **pas** un texte libre, **pas** un groupe M365 : des
**utilisateurs**. Un dossier peut avoir **plusieurs** responsables.

Une parcelle peut être dans **plusieurs** dossiers (unique seulement
sur le couple dossier×parcelle). Le « responsable déduit » n’est pas
unique.

Chemin géographique distinct (ne pas confondre) : `secteurs.titulaire_id`
via commune (`ParcelleProspectionQuery::applyMonSecteurFilter`). Ce
n’est pas le responsable du dossier.

### 5.3 Parcelles sans dossier

Point bloquant d’un filtre global. Volume : **SQL Q5**.
`PatrimoineFoncierQuery` n’exige **pas** de dossier : le patrimoine
actuel se cale sur `societe_detentrice_id`.

### 5.4 Requête

Voir `-- Q5` (répartition par user, multi-responsables, sans dossier,
dossier sans responsable).

---

## 6. GÉOGRAPHIE

### 6.1 Département

Pas de colonne département sur `parcelles`. Déduit de
`communes.departement` varchar(3) NOT NULL
(`migration 2026_07_30_120000_create_communes_table.php:19`, index
`communes_departement_idx`). Jointure
`parcelles.code_insee_commune` → `communes.code_insee`
(`Parcelle.php:165-168`). Filtre liste : `whereHas('commune')`
(`ParcelleProspectionQuery.php:194-197`). Patrimoine expose déjà
`commune.departement` (`PatrimoineFoncierQuery.php:46, 132-133`).

### 6.2 Référentiel communes vs parcelles

`code_insee_commune` obligatoire à la création, `exists:communes,code_insee`
(`ParcelleController.php:929`). FK `parcelles_commune_fk` `restrictOnDelete`
(`create_parcelles_table.php:34-37`). Une parcelle **sans** commune en
base viole la FK : le count d’orphelins attendu est 0. SQL Q6 le
vérifie quand même (données historiques / import).

### 6.3 SAINTE LIVRADE — 31530 / 47110

Aucun durcissement 31530/47110 dans le code parcelle. #1332 cite
« sainte livrade » comme **cas de recherche** SIWI, pas comme
désambiguïsation cadastrale. #1341 : normalisation tirets/accents,
**pas** d’expansion st/saint. `EXTRACTION_PARCELLES.md` : exception
IGN Sainte-Livrade = NON DÉTERMINÉ.

L’ambiguïté est à la **saisie** (homonymes, `CommuneSelect`). Une
fois l’INSEE posté, la parcelle est ancrée. SQL Q6 compte les
parcelles sur `31530` vs `47110` (preuves à coller, pas une
interprétation).

### 6.4 Requête

Voir `-- Q6`.

---

## 7. OCCUPATION ET BAUX

### 7.1 Dates

Table `parcelle_occupations` (#1119). Historique successif, pas
d’unicité `parcelle_id`. Courante = `statut != 'libere'`
(`ParcelleOccupation.php:130-133, 165-168`).

| Colonne | Nullable | Libellé UI actuel (`Show.vue:1693-1723, 1849-1854`) | Usage code |
|---------|----------|------------------------------------------------------|------------|
| `date_fin_bail` | oui | **Date de signature** (renommage #1446 ; le nom SQL n’a pas suivi) | Butoir préavis = `date_fin_bail − preavis_mois` (`ParcelleOccupation.php:136-148`) |
| `date_echeance` | oui | Date d’échéance (ajout #1446, migration `2026_09_04_144600:53-55`) | Alerte / tâche J−2 mois (`:153-160`) |
| `preavis_mois` | oui | Préavis (mois) | |

Les deux dates sont **exploitables en schéma**. Saisie réelle : **SQL Q7**.
Pas d’index sur `date_echeance` (index existants : `(parcelle_id, statut)`,
`(statut, date_fin_bail)`).

### 7.2 Requête

Échéance 12 mois : `date_echeance` (date de fin métier actuelle), pas
`date_fin_bail`. Le SQL donne aussi le remplissage de `date_fin_bail`
et `preavis_mois` pour ne pas confondre.

---

## 8. STATUT CADASTRAL

### 8.1 Distinction active / remplacée

Colonne `parcelles.statut_cadastral` varchar(16) DEFAULT `'active'`
(`create_parcelles_table.php:30`). Valeurs :
`FoncierReferentiels::STATUTS_CADASTRAL` (`:142-145`) —
`active` / `remplacee`. Index `parcelles_statut_cad_idx`.

Scope `Parcelle::scopeActivesCadastre` (`Parcelle.php:366-372`).
Méthode `estRemplacee()` (`:374-377`). Une remplacée n’est **pas**
soft-deleted : `SoftDeletes` est un troisième axe (`deleted_at`).

### 8.2 Exclusion en agrégat

**Oui, déjà en production.** `ParcelleProspectionQuery::totauxEnsembleFiltre`
(`:385-388`) : surface et valorisation sur `statut_cadastral = 'active'`
seulement ; le COUNT peut inclure les remplacées si
`inclure_remplacees`. Patrimoine : `where('statut_cadastral', STATUT_CAD_ACTIVE)`
(`PatrimoineFoncierQuery.php:37`). Carte d’emprise : skip
`estRemplacee()` (#1562).

### 8.3 Requête

Voir `-- Q8` (nombre + surface cadastrale des remplacées, hors
soft-delete).

---

## 9. SURFACES

Deux colonnes, unité **m²** (decimal 14,2, nullable) :

| Colonne | Usage constaté |
|---------|----------------|
| `surface_cadastrale_m2` | Surface qui **fait foi** pour les totaux patrimoine et liste (`PatrimoineFoncierQuery.php:109-111, 387` ; UI `formatSurface` en m², `Patrimoine/Index.vue:41-43, 192`) |
| `surface_arpentee_m2` | Saisie fiche ; **non** utilisée dans les agrégats patrimoine |

Pas de colonne hectare. Conversion ha = `/ 10000` à l’affichage
éventuel, pas en stock.

Géométrie IGN : `parcelle_geometries.geom` — écart ST_Area vs
cadastrale informatif (#1109), pas la surface patrimoniale.

### 9.1 Requête

Voir `-- Q9`.

---

## 10. VOLUMÉTRIE GÉNÉRALE

SQL `-- Q10` : total, actives, remplacées, soft-deleted, répartition
`etat_foncier`, détention groupe, actes.

Écran déjà là : `/patrimoine` (`PatrimoineFoncierController.php:23-48`),
périmètre = `societe_detentrice_id IS NOT NULL` AND `statut_cadastral
= active`. Note UI : pas de filtre « non affectée à une opération »
(Phase 3 absente, `:44-46`). Il n’existe **pas** de table
d’affectation parcelle → opération ; `etat_foncier = 'affectee'` est
un libellé, `dossiers_fonciers.programme_id` (#1556) est le seul
lien dossier → programme.

**Suffisance pour un dashboard PDG :** NON DÉTERMINÉ avant exécution
des COUNT. Si Q10.total_actives est faible **ou** si Q2 montre une
majorité sans acte unitaire **et** Q3/Q4 à 0, l’écran « capital /
portage / écart valo » serait structurellement vide ou trompeur —
même si la liste patrimoine a des lignes (référence cadastrale +
société). Ne pas conclure « peu saisi » sans coller Q10.

---

## 11. PERFORMANCE

### 11.1 Agrégat unique vs pré-calcul

`PatrimoineFoncierQuery::collecter` charge **toutes** les parcelles
détenues en PHP (`->get()`, `:44-64`) puis valorisations / charges
en 3 requêtes `whereIn`. Tenable **si** le COUNT Q10.detenues reste
modeste. Seuil : NON DÉTERMINÉ (pas de mesure prod). Des compteurs
pré-calculés ne sont pas justifiés par le schéma ; ils le seraient
seulement si Q10 + EXPLAIN Q11 montrent un scan trop lourd.

Un JOIN unique acte + valo + portage + occupation est **écrivable**
(SQL Q11) mais :

- prix d’acte N-parcelles : le JOIN multiplie le montant (même piège
  que le PHP actuel) ;
- charges sans `parcelle_id` : perdues ou doublonnées via l’acte ;
- occupations : plusieurs lignes courantes possibles (pas d’unicité).

### 11.2 Index existants (colonnes de filtre)

`parcelles` : `etat_foncier`, `statut_cadastral`, `type_foncier`,
`societe_detentrice_id`, `code_insee_commune`, `deleted_at`, unique
vivante (commune, section, numéro).

`communes` : `departement`, PK `code_insee`.

`acte_parcelle` : unique (acte, parcelle). `actes_fonciers` :
`date_acte`.

`parcelle_valorisations` : `(parcelle_id, date_valorisation)`.

`charges_portage` : `(parcelle_id, exercice)`.

`parcelle_occupations` : `(parcelle_id, statut)` — **pas**
`date_echeance`.

`entity_field_audits` : morph + `created_at` — **pas** `champ`.
Un calcul d’ancienneté d’état par audit scannerait le morph parcelle
filtré en PHP/SQL sur `champ`.

`dossier_foncier_parcelle` : index `parcelle_id`.
`dossier_foncier_responsable` : index `user_id`.

Pas de table de snapshots / KPI pré-calculés patrimoine.

---

## 12. VERDICT DE FAISABILITÉ

Légende Disponibilité : **oui** = colonne/table + chemin d’écriture
existants. **partiel** = existe mais incomplet, fusionné, ou non
ventilé. **non** = pas de champ. Fiabilité **prod** = NON DÉTERMINÉ
tant que Robin n’a pas collé les COUNT (sauf « non » structurel).

| Information | Disponible ? | Source | Fiabilité (schéma / code) | Bloquant ? |
|-------------|--------------|--------|---------------------------|------------|
| Capital immobilisé | partiel | `actes_fonciers.prix_numeraire` recopié tel quel par parcelle (`PatrimoineFoncierQuery.php:153-155`) | Fiable **uniquement** si 1 parcelle / acte ; sinon montant global non ventilé ; HT/TTC NON DÉTERMINÉ | **Oui** pour un € par parcelle honnête |
| Coût de portage cumulé | partiel | `charges_portage` + facture TTC/HT ou `montant_manuel` | Schéma prêt ; UI masquée #1447 ; remplissage = SQL Q3 | **Oui si Q3.nb_lignes = 0** (indicateur toujours à 0) |
| Coût de portage annuel | partiel | `charges_portage.exercice` | Idem ; c’est un total d’exercice, pas un taux | **Oui si Q3 vide** |
| Écart acquisition / valorisation | partiel | `prix_numeraire` (acte) vs dernière `parcelle_valorisations.valeur` (déjà calculé fiche, `ParcelleValorisationService.php:267-271`) | Hérite des deux faiblesses (prix non ventilé + valo éventuellement absente) | **Oui** tant que Q2 ou Q4 sont creux / multi-parcelles |
| Ancienneté de détention | partiel | `actes_fonciers.date_acte` du premier acte chargé (`PatrimoineFoncierQuery.php:100-107`) ; à défaut audit `etat_foncier` incomplet | Date d’acte = date d’acquisition **si** acte saisi ; sinon NON DÉTERMINÉ. Pas la date d’entrée dans l’état « dormance » | Non pour une v1 « durée depuis l’acte » ; oui pour « silence dans l’état courant » |
| Silence d’activité | non | aucun champ | Proxies `updated_at` / audit / `date_revue` dossier ≠ silence métier | **Oui** (à concevoir : règle + saisie ou proxy documenté) |
| Développeur responsable | partiel | `dossier_foncier_responsable.user_id` (User, N⇄N) | Parcelles sans dossier ou dossier sans responsable = non attribuables (Q5) ; multi-dossiers = multi-users | **Oui** pour un filtre global propre si Q5.sans_dossier est élevé |
| Motif de dormance | non | aucun champ (pas de grep `dormance` / `motif_detention`) ; `parcelles.commentaire` = texte libre non typé | — | **Oui** (saisie ou référentiel à créer) |
| Département | oui | `communes.departement` via INSEE | FK obligatoire ; homonymes à la saisie seulement | Non |
| Commune | oui | `parcelles.code_insee_commune` → `communes` | FK restrict ; orphelins attendus 0 (Q6) | Non |

### Ce qui est faisable en v1 (sans nouveau champ)

- Liste des parcelles **détenues** (`societe_detentrice_id` + cadastre
  `active`) : écran `/patrimoine` déjà là.
- Commune, département, surface cadastrale, société, occupation
  courante (booléen + libellé), dernière valo **si saisie**, date
  d’acte **si saisie**.
- Répartition par `etat_foncier` **tel que stocké aujourd’hui**
  (y compris vocations fusionnées) — à légender clairement.

### Ce qui demande une saisie préalable (schéma déjà là)

- Prix d’acte (et discipline 1 parcelle / acte, ou ventilation
  manuelle absente aujourd’hui).
- Valorisations datées.
- Charges de portage (et réaffichage UI #1447).
- `type_foncier` **ou** resaisie de la vocation hors de `etat_foncier`
  si on applique le référentiel arrêté.
- Responsable dossier + rattachement parcelle ↔ dossier.
- Dates d’échéance d’occupation.

### Ce qui demande un développement

- Dissocier état et type (référentiel arrêté ≠ code #1446) : migration
  de données **après** Q1, pas avant.
- Ventilation du `prix_numeraire` par parcelle (clé surface déjà là,
  non branchée).
- Motif de dormance (nouveau champ / référentiel).
- Règle de silence (événement métier à définir).
- Filtre « non affectée à une opération » (Phase 3 absente ; pas de
  table d’affectation).
- Définition SQL de la dormance **une fois** l’écart état/type tranché.

---

## DÉPLOIEMENT-TEST

Pas de code. Après push :

```
git pull origin main
```

Pas de migrate, pas de `journal:sync`, pas de Vite.

Robin : coller la sortie brute du bloc SQL ci-dessous (MySQL prod).
Sans ces COUNT, aucune conclusion de remplissage n’est autorisée.

## LEÇON

Une table livrée (#1130 portage, #1131 valo, #1242 `type_foncier`)
n’est pas une donnée. Le sélecteur unique « État foncier » depuis
#1446 a **absorbé** la vocation : un dashboard qui croise état × type
comme deux axes indépendants lira le schéma d’avant la fusion, pas
celui que les chargés remplissent. Le prix d’acte est un montant
**d’acte**, déjà recopié tel quel sur chaque ligne patrimoine — un
total « capital immobilisé » sommé sur ces lignes est faux dès qu’un
acte couvre plusieurs parcelles.

---

## REQUÊTES SQL (coller l’ensemble en une fois)

MySQL 8. Soft-deletes : `parcelles.deleted_at IS NULL` sauf Q10.d.
Morph audit : pas de `Relation::morphMap` dans le repo →
`auditable_type = 'App\\Models\\Parcelle'`.

```sql
-- ============================================================
-- SUIVI #1566 — volumétrie patrimoine PDG
-- Exécuter en prod. Coller la sortie brute (y compris les en-têtes).
-- ============================================================

-- Q1.a Répartition par etat_foncier (vivantes)
SELECT
  COALESCE(p.etat_foncier, '(NULL)') AS etat_foncier,
  COUNT(*) AS nb,
  ROUND(100 * COUNT(*) / SUM(COUNT(*)) OVER (), 2) AS pct
FROM parcelles p
WHERE p.deleted_at IS NULL
GROUP BY p.etat_foncier
ORDER BY nb DESC;

-- Q1.b Répartition par type_foncier (colonne dédiée, y compris NULL)
SELECT
  COALESCE(p.type_foncier, '(NULL)') AS type_foncier,
  COUNT(*) AS nb,
  ROUND(100 * COUNT(*) / SUM(COUNT(*)) OVER (), 2) AS pct
FROM parcelles p
WHERE p.deleted_at IS NULL
GROUP BY p.type_foncier
ORDER BY nb DESC;

-- Q1.c Croisement etat_foncier × type_foncier (combinaisons réelles)
SELECT
  COALESCE(p.etat_foncier, '(NULL)') AS etat_foncier,
  COALESCE(p.type_foncier, '(NULL)') AS type_foncier,
  COUNT(*) AS nb
FROM parcelles p
WHERE p.deleted_at IS NULL
GROUP BY p.etat_foncier, p.type_foncier
ORDER BY nb DESC;

-- Q1.d Nullité / défaut
SELECT
  SUM(p.etat_foncier IS NULL) AS etat_null,
  SUM(p.etat_foncier = 'reperee') AS etat_reperee,
  SUM(p.type_foncier IS NULL OR p.type_foncier = '') AS type_null_ou_vide,
  SUM(p.type_foncier IS NOT NULL AND p.type_foncier <> '') AS type_renseigne,
  COUNT(*) AS total_vivantes
FROM parcelles p
WHERE p.deleted_at IS NULL;

-- Q1.e Audit etat_foncier : couverture et date d'entrée dans l'état courant
SELECT
  COUNT(DISTINCT p.id) AS parcelles_vivantes,
  COUNT(DISTINCT CASE WHEN a.id IS NOT NULL THEN p.id END) AS parcelles_avec_au_moins_un_audit_etat,
  COUNT(DISTINCT CASE WHEN a.id IS NULL THEN p.id END) AS parcelles_sans_aucun_audit_etat
FROM parcelles p
LEFT JOIN entity_field_audits a
  ON a.auditable_type = 'App\\Models\\Parcelle'
 AND a.auditable_id = p.id
 AND a.champ = 'etat_foncier'
WHERE p.deleted_at IS NULL;

SELECT MIN(a.created_at) AS premier_audit_etat, MAX(a.created_at) AS dernier_audit_etat, COUNT(*) AS nb_lignes_audit_etat
FROM entity_field_audits a
WHERE a.auditable_type = 'App\\Models\\Parcelle'
  AND a.champ = 'etat_foncier';

-- Q2.a Actes : 1 parcelle vs N (ventilation)
SELECT
  CASE WHEN n.nb_parcelles = 1 THEN '1_parcelle' ELSE 'multi_parcelles' END AS profil,
  COUNT(*) AS nb_actes,
  SUM(n.nb_parcelles) AS nb_liens_parcelle,
  SUM(af.prix_numeraire) AS somme_prix_actes
FROM actes_fonciers af
JOIN (
  SELECT ap.acte_foncier_id, COUNT(*) AS nb_parcelles
  FROM acte_parcelle ap
  GROUP BY ap.acte_foncier_id
) n ON n.acte_foncier_id = af.id
GROUP BY CASE WHEN n.nb_parcelles = 1 THEN '1_parcelle' ELSE 'multi_parcelles' END;

-- Q2.b Parcelles etat acquise/affectee : prix exploitable (acte unitaire) vs global vs absent
SELECT
  SUM(p.etat_foncier IN ('acquise', 'affectee')) AS nb_acquise_ou_affectee,
  SUM(p.etat_foncier IN ('acquise', 'affectee') AND ua.acte_id IS NOT NULL) AS dont_acte_unitaire_avec_prix,
  SUM(p.etat_foncier IN ('acquise', 'affectee') AND ma.acte_id IS NOT NULL) AS dont_seulement_acte_multi,
  SUM(p.etat_foncier IN ('acquise', 'affectee') AND ua.acte_id IS NULL AND ma.acte_id IS NULL) AS sans_aucun_acte
FROM parcelles p
LEFT JOIN (
  SELECT ap.parcelle_id, MIN(ap.acte_foncier_id) AS acte_id
  FROM acte_parcelle ap
  JOIN (
    SELECT acte_foncier_id FROM acte_parcelle GROUP BY acte_foncier_id HAVING COUNT(*) = 1
  ) u ON u.acte_foncier_id = ap.acte_foncier_id
  JOIN actes_fonciers af ON af.id = ap.acte_foncier_id AND af.prix_numeraire IS NOT NULL
  GROUP BY ap.parcelle_id
) ua ON ua.parcelle_id = p.id
LEFT JOIN (
  SELECT ap.parcelle_id, MIN(ap.acte_foncier_id) AS acte_id
  FROM acte_parcelle ap
  JOIN (
    SELECT acte_foncier_id FROM acte_parcelle GROUP BY acte_foncier_id HAVING COUNT(*) > 1
  ) m ON m.acte_foncier_id = ap.acte_foncier_id
  GROUP BY ap.parcelle_id
) ma ON ma.parcelle_id = p.id
WHERE p.deleted_at IS NULL;

-- Q2.c Même décompte sur le périmètre Patrimoine (société détentrice, cadastre active)
SELECT
  COUNT(*) AS nb_patrimoine,
  SUM(ua.acte_id IS NOT NULL) AS acte_unitaire_avec_prix,
  SUM(ua.acte_id IS NULL AND ma.acte_id IS NOT NULL) AS seulement_acte_multi,
  SUM(ua.acte_id IS NULL AND ma.acte_id IS NULL) AS sans_acte
FROM parcelles p
LEFT JOIN (
  SELECT ap.parcelle_id, MIN(ap.acte_foncier_id) AS acte_id
  FROM acte_parcelle ap
  JOIN (
    SELECT acte_foncier_id FROM acte_parcelle GROUP BY acte_foncier_id HAVING COUNT(*) = 1
  ) u ON u.acte_foncier_id = ap.acte_foncier_id
  JOIN actes_fonciers af ON af.id = ap.acte_foncier_id AND af.prix_numeraire IS NOT NULL
  GROUP BY ap.parcelle_id
) ua ON ua.parcelle_id = p.id
LEFT JOIN (
  SELECT ap.parcelle_id, MIN(ap.acte_foncier_id) AS acte_id
  FROM acte_parcelle ap
  JOIN (
    SELECT acte_foncier_id FROM acte_parcelle GROUP BY acte_foncier_id HAVING COUNT(*) > 1
  ) m ON m.acte_foncier_id = ap.acte_foncier_id
  GROUP BY ap.parcelle_id
) ma ON ma.parcelle_id = p.id
WHERE p.deleted_at IS NULL
  AND p.statut_cadastral = 'active'
  AND p.societe_detentrice_id IS NOT NULL;

-- Q2.d HT/TTC : preuve d'absence de colonne (0 = pas de colonne prix_ht/ttc sur l'acte)
SELECT COUNT(*) AS nb_actes, SUM(prix_numeraire IS NULL) AS prix_null, SUM(prix_numeraire = 0) AS prix_zero
FROM actes_fonciers;

-- Q3.a Volume charges_portage
SELECT
  COUNT(*) AS nb_lignes,
  COUNT(DISTINCT parcelle_id) AS nb_parcelles_distinctes,
  SUM(parcelle_id IS NULL) AS lignes_sans_parcelle,
  SUM(acte_foncier_id IS NOT NULL) AS lignes_avec_acte,
  SUM(depot_facture_id IS NOT NULL) AS lignes_facture,
  SUM(montant_manuel IS NOT NULL) AS lignes_manuel,
  MIN(exercice) AS exercice_min,
  MAX(exercice) AS exercice_max,
  MIN(created_at) AS created_min,
  MAX(created_at) AS created_max
FROM charges_portage;

-- Q3.b Montant total (TTC facture sinon HT sinon manuel) + par type
SELECT
  cp.type,
  COUNT(*) AS nb,
  ROUND(SUM(COALESCE(df.montant_ttc, df.montant_ht, cp.montant_manuel)), 2) AS montant_total
FROM charges_portage cp
LEFT JOIN depot_factures df ON df.id = cp.depot_facture_id
GROUP BY cp.type
ORDER BY nb DESC;

SELECT ROUND(SUM(COALESCE(df.montant_ttc, df.montant_ht, cp.montant_manuel)), 2) AS montant_total_toutes_lignes
FROM charges_portage cp
LEFT JOIN depot_factures df ON df.id = cp.depot_facture_id;

-- Q3.c Parcelles patrimoine avec au moins une charge à parcelle_id
SELECT
  COUNT(*) AS nb_patrimoine,
  SUM(c.nb_charges IS NOT NULL) AS avec_charge_parcelle,
  SUM(c.nb_charges IS NULL) AS sans_charge_parcelle
FROM parcelles p
LEFT JOIN (
  SELECT parcelle_id, COUNT(*) AS nb_charges
  FROM charges_portage
  WHERE parcelle_id IS NOT NULL
  GROUP BY parcelle_id
) c ON c.parcelle_id = p.id
WHERE p.deleted_at IS NULL
  AND p.statut_cadastral = 'active'
  AND p.societe_detentrice_id IS NOT NULL;

-- Q4.a Valorisations : couverture acquise/affectee
SELECT
  SUM(p.etat_foncier IN ('acquise', 'affectee')) AS nb_acquise_ou_affectee,
  SUM(p.etat_foncier IN ('acquise', 'affectee') AND v.nb IS NOT NULL) AS avec_au_moins_une_valo,
  SUM(p.etat_foncier IN ('acquise', 'affectee') AND v.nb IS NULL) AS sans_valo
FROM parcelles p
LEFT JOIN (
  SELECT parcelle_id, COUNT(*) AS nb FROM parcelle_valorisations GROUP BY parcelle_id
) v ON v.parcelle_id = p.id
WHERE p.deleted_at IS NULL;

-- Q4.b Même chose périmètre patrimoine + ancienneté moyenne de la dernière valo
SELECT
  COUNT(*) AS nb_patrimoine,
  SUM(d.date_valorisation IS NOT NULL) AS avec_valo,
  SUM(d.date_valorisation IS NULL) AS sans_valo,
  ROUND(AVG(DATEDIFF(CURDATE(), d.date_valorisation)), 1) AS anciennete_moyenne_jours_derniere_valo,
  MIN(d.date_valorisation) AS valo_la_plus_ancienne,
  MAX(d.date_valorisation) AS valo_la_plus_recente
FROM parcelles p
LEFT JOIN (
  SELECT pv.parcelle_id, pv.date_valorisation
  FROM parcelle_valorisations pv
  INNER JOIN (
    SELECT parcelle_id, MAX(CONCAT(date_valorisation, ':', LPAD(id, 20, '0'))) AS k
    FROM parcelle_valorisations
    GROUP BY parcelle_id
  ) x ON x.parcelle_id = pv.parcelle_id
    AND CONCAT(pv.date_valorisation, ':', LPAD(pv.id, 20, '0')) = x.k
) d ON d.parcelle_id = p.id
WHERE p.deleted_at IS NULL
  AND p.statut_cadastral = 'active'
  AND p.societe_detentrice_id IS NOT NULL;

-- Q4.c Méthodes
SELECT COALESCE(methode, '(NULL)') AS methode, COUNT(*) AS nb
FROM parcelle_valorisations
GROUP BY methode
ORDER BY nb DESC;

-- Q5.a Parcelles acquise/affectee sans aucun dossier
SELECT
  SUM(p.etat_foncier IN ('acquise', 'affectee')) AS nb_acquise_ou_affectee,
  SUM(p.etat_foncier IN ('acquise', 'affectee') AND dfp.parcelle_id IS NULL) AS sans_dossier,
  SUM(p.etat_foncier IN ('acquise', 'affectee') AND dfp.parcelle_id IS NOT NULL) AS avec_au_moins_un_dossier
FROM parcelles p
LEFT JOIN (SELECT DISTINCT parcelle_id FROM dossier_foncier_parcelle) dfp ON dfp.parcelle_id = p.id
WHERE p.deleted_at IS NULL;

-- Q5.b Périmètre patrimoine : attribuabilité développeur
SELECT
  CASE
    WHEN dfp.nb_dossiers IS NULL THEN 'sans_dossier'
    WHEN resp.nb_users IS NULL OR resp.nb_users = 0 THEN 'dossier_sans_responsable'
    WHEN resp.nb_users = 1 THEN 'un_responsable'
    ELSE 'plusieurs_responsables'
  END AS profil,
  COUNT(*) AS nb_parcelles
FROM parcelles p
LEFT JOIN (
  SELECT parcelle_id, COUNT(*) AS nb_dossiers
  FROM dossier_foncier_parcelle
  GROUP BY parcelle_id
) dfp ON dfp.parcelle_id = p.id
LEFT JOIN (
  SELECT dfp2.parcelle_id, COUNT(DISTINCT dfr.user_id) AS nb_users
  FROM dossier_foncier_parcelle dfp2
  LEFT JOIN dossier_foncier_responsable dfr ON dfr.dossier_foncier_id = dfp2.dossier_foncier_id
  GROUP BY dfp2.parcelle_id
) resp ON resp.parcelle_id = p.id
WHERE p.deleted_at IS NULL
  AND p.statut_cadastral = 'active'
  AND p.societe_detentrice_id IS NOT NULL
GROUP BY profil
ORDER BY nb_parcelles DESC;

-- Q5.c Répartition par user responsable (une parcelle peut compter plusieurs fois)
SELECT
  u.id AS user_id,
  u.name AS responsable,
  COUNT(DISTINCT dfp.parcelle_id) AS nb_parcelles_patrimoine
FROM dossier_foncier_responsable dfr
JOIN users u ON u.id = dfr.user_id
JOIN dossier_foncier_parcelle dfp ON dfp.dossier_foncier_id = dfr.dossier_foncier_id
JOIN parcelles p ON p.id = dfp.parcelle_id AND p.deleted_at IS NULL
  AND p.statut_cadastral = 'active' AND p.societe_detentrice_id IS NOT NULL
GROUP BY u.id, u.name
ORDER BY nb_parcelles_patrimoine DESC;

-- Q6.a Parcelles sans commune résolue (FK attendue : 0)
SELECT
  SUM(p.code_insee_commune IS NULL OR p.code_insee_commune = '') AS sans_code_insee,
  SUM(c.code_insee IS NULL) AS code_insee_non_resolu,
  COUNT(*) AS total_vivantes
FROM parcelles p
LEFT JOIN communes c ON c.code_insee = p.code_insee_commune
WHERE p.deleted_at IS NULL;

-- Q6.b Homonymes Sainte-Livrade (INSEE 31530 Haute-Garonne / 47110 Lot-et-Garonne)
SELECT c.code_insee, c.nom, c.departement, c.code_postal, COUNT(p.id) AS nb_parcelles
FROM communes c
LEFT JOIN parcelles p ON p.code_insee_commune = c.code_insee AND p.deleted_at IS NULL
WHERE c.code_insee IN ('31530', '47110')
   OR c.nom LIKE '%Livrade%'
GROUP BY c.code_insee, c.nom, c.departement, c.code_postal
ORDER BY c.code_insee;

-- Q6.c Parcelles dont la commune n'a pas de département
SELECT COUNT(*) AS nb
FROM parcelles p
JOIN communes c ON c.code_insee = p.code_insee_commune
WHERE p.deleted_at IS NULL
  AND (c.departement IS NULL OR c.departement = '');

-- Q7.a Occupations : volume et dates
SELECT
  COUNT(*) AS nb_occupations,
  SUM(statut <> 'libere') AS nb_courantes,
  SUM(statut <> 'libere' AND date_echeance IS NOT NULL) AS courantes_avec_date_echeance,
  SUM(statut <> 'libere' AND date_echeance IS NULL) AS courantes_sans_date_echeance,
  SUM(statut <> 'libere' AND date_fin_bail IS NOT NULL) AS courantes_avec_date_fin_bail_sql,
  SUM(statut <> 'libere' AND preavis_mois IS NOT NULL) AS courantes_avec_preavis,
  SUM(
    statut <> 'libere'
    AND date_echeance IS NOT NULL
    AND date_echeance BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL 12 MONTH)
  ) AS courantes_echeance_12_mois
FROM parcelle_occupations;

-- Q7.b Parcelles patrimoine occupées
SELECT
  COUNT(DISTINCT p.id) AS nb_patrimoine,
  COUNT(DISTINCT CASE WHEN o.id IS NOT NULL THEN p.id END) AS avec_occupation_courante
FROM parcelles p
LEFT JOIN parcelle_occupations o ON o.parcelle_id = p.id AND o.statut <> 'libere'
WHERE p.deleted_at IS NULL
  AND p.statut_cadastral = 'active'
  AND p.societe_detentrice_id IS NOT NULL;

-- Q8.a Remplacées : nombre + surface
SELECT
  SUM(p.statut_cadastral = 'remplacee') AS nb_remplacees,
  SUM(CASE WHEN p.statut_cadastral = 'remplacee' THEN p.surface_cadastrale_m2 ELSE 0 END) AS surface_cadastrale_m2_remplacees,
  SUM(p.statut_cadastral = 'active') AS nb_actives,
  SUM(CASE WHEN p.statut_cadastral = 'active' THEN p.surface_cadastrale_m2 ELSE 0 END) AS surface_cadastrale_m2_actives,
  SUM(p.statut_cadastral NOT IN ('active', 'remplacee') OR p.statut_cadastral IS NULL) AS autre_statut
FROM parcelles p
WHERE p.deleted_at IS NULL;

-- Q9.a Surfaces manquantes acquise/affectee (cadastre active)
SELECT
  SUM(p.etat_foncier IN ('acquise', 'affectee') AND p.statut_cadastral = 'active') AS nb,
  SUM(p.etat_foncier IN ('acquise', 'affectee') AND p.statut_cadastral = 'active'
      AND p.surface_cadastrale_m2 IS NULL) AS sans_surface_cadastrale,
  SUM(p.etat_foncier IN ('acquise', 'affectee') AND p.statut_cadastral = 'active'
      AND p.surface_arpentee_m2 IS NULL) AS sans_surface_arpentee
FROM parcelles p
WHERE p.deleted_at IS NULL;

-- Q9.b Périmètre patrimoine
SELECT
  COUNT(*) AS nb_patrimoine,
  SUM(surface_cadastrale_m2 IS NULL) AS sans_surface_cadastrale,
  SUM(surface_cadastrale_m2 IS NOT NULL) AS avec_surface_cadastrale,
  ROUND(SUM(surface_cadastrale_m2), 2) AS surface_totale_m2
FROM parcelles p
WHERE p.deleted_at IS NULL
  AND p.statut_cadastral = 'active'
  AND p.societe_detentrice_id IS NOT NULL;

-- Q10.a Totaux
SELECT
  (SELECT COUNT(*) FROM parcelles) AS total_y_compris_soft_delete,
  (SELECT COUNT(*) FROM parcelles WHERE deleted_at IS NULL) AS vivantes,
  (SELECT COUNT(*) FROM parcelles WHERE deleted_at IS NOT NULL) AS soft_deleted,
  (SELECT COUNT(*) FROM parcelles WHERE deleted_at IS NULL AND statut_cadastral = 'active') AS actives_cadastre,
  (SELECT COUNT(*) FROM parcelles WHERE deleted_at IS NULL AND statut_cadastral = 'remplacee') AS remplacees,
  (SELECT COUNT(*) FROM parcelles WHERE deleted_at IS NULL AND statut_cadastral = 'active' AND societe_detentrice_id IS NOT NULL) AS patrimoine_groupe,
  (SELECT COUNT(*) FROM actes_fonciers) AS nb_actes,
  (SELECT COUNT(*) FROM parcelle_valorisations) AS nb_valorisations,
  (SELECT COUNT(*) FROM charges_portage) AS nb_charges_portage,
  (SELECT COUNT(*) FROM parcelle_occupations) AS nb_occupations,
  (SELECT COUNT(*) FROM dossiers_fonciers) AS nb_dossiers;

-- Q10.b Répartition etat_foncier des actives cadastre
SELECT COALESCE(etat_foncier, '(NULL)') AS etat_foncier, COUNT(*) AS nb
FROM parcelles
WHERE deleted_at IS NULL AND statut_cadastral = 'active'
GROUP BY etat_foncier
ORDER BY nb DESC;

-- Q11 EXPLAIN de l'agrégat type dashboard (ne pas s'étonner du type ; coller Extra / rows)
EXPLAIN
SELECT
  p.id,
  p.etat_foncier,
  p.type_foncier,
  p.surface_cadastrale_m2,
  c.nom AS commune,
  c.departement,
  af.prix_numeraire,
  af.date_acte,
  pv.valeur AS derniere_valo,
  pv.date_valorisation,
  cp_agg.montant_portage,
  o.date_echeance
FROM parcelles p
LEFT JOIN communes c ON c.code_insee = p.code_insee_commune
LEFT JOIN acte_parcelle ap ON ap.parcelle_id = p.id
LEFT JOIN actes_fonciers af ON af.id = ap.acte_foncier_id
LEFT JOIN parcelle_valorisations pv ON pv.id = (
  SELECT pv2.id FROM parcelle_valorisations pv2
  WHERE pv2.parcelle_id = p.id
  ORDER BY pv2.date_valorisation DESC, pv2.id DESC
  LIMIT 1
)
LEFT JOIN (
  SELECT parcelle_id, SUM(COALESCE(df.montant_ttc, df.montant_ht, cp.montant_manuel)) AS montant_portage
  FROM charges_portage cp
  LEFT JOIN depot_factures df ON df.id = cp.depot_facture_id
  WHERE cp.parcelle_id IS NOT NULL
  GROUP BY parcelle_id
) cp_agg ON cp_agg.parcelle_id = p.id
LEFT JOIN parcelle_occupations o ON o.parcelle_id = p.id AND o.statut <> 'libere'
WHERE p.deleted_at IS NULL
  AND p.statut_cadastral = 'active'
  AND p.societe_detentrice_id IS NOT NULL;
```
