---
suivi: 1462
date: 2026-09-07
sujet: Diagnostic fiche parcelle — société détentrice trompeuse + chevauchement Urbanisme
chantier: foncier / parcelles / Show
type: diagnostic
statut: poussé
hash: 65769437
fichiers:
  - docs/suivi/SUIVI_1462_diag_fiche_parcelle_detentrice_urbanisme.md
---

# SUIVI #1462 — PHASE 1 DIAGNOSTIC — fiche parcelle (détentrice + Urbanisme)

## PROMPT ENVOYÉ

PHASE 1 lecture seule. Deux sujets : (1) « Société détentrice » HECTARE + badge
SANS ACTE sur parcelle en négociation ; (2) chevauchement libellé/valeur dans le
bloc Urbanisme. Aucun code applicatif, aucun rebuild. Requêtes SQL pour Robin.
Base attendue `fe79ca8a` (#1460) — vérifier main à jour.

## FICHES LUES

- **#1246** LEÇON — colonne `parcelles.societe_detentrice_id` → `entites` ;
  acte prime ; saisie manuelle **historique** volontaire ; badge « Sans acte » ;
  Patrimoine filtré sur la colonne. Ne pas confondre avec `proprietaires`.
- **#1449** — grille fiche (paliers 860 / 1280), rang 1 Ident / Surfaces / Urb ;
  jamais de masquage horizontal (#1271 / #1444).
- **#1452** LEÇON — `grid-area` + `align-self: start` sur plan ; ne pas étirer
  les cartes. Acquis de grille à conserver.
- **#1447** — lieu-dit déplacé vers Identification (après Société détentrice).

## BASE GIT

```
git log --oneline -1 origin/main
10e63d0f chore(suivi): hash fiche #1461
```

Attendu prompt : `fe79ca8a` (#1460). **Main a avancé** : #1461 (fix
`JSON_QUOTE` / normalisation valideur) est déjà sur `origin/main`. Hors périmètre
foncier — **aucun conflit** avec ce diagnostic. Travail sur `10e63d0f`.

MySQL local : **refusé** (pas de connexion). Aucun chiffre de volume. Aucun
rendu navigateur. Les requêtes ci-dessous sont à coller en prod par Robin.

---

## CLAUSE D’ARRÊT

### Sujet 1 — **PARTIELLEMENT DÉCLENCHÉE (signaler, ne pas coder)**

La colonne `societe_detentrice_id` **est** une donnée **saisie manuelle possible**
et **significative** : #1246 l’a conçue pour le patrimoine historique « société
renseignée sans acte ERP ». Elle alimente **Patrimoine** (`whereNotNull`) et la
création rapide Prospection. Un masquage **systématique** sur la fiche ferait
perdre le seul point d’édition inline de cette valeur (Patrimoine = lecture ;
Prospection = création).

**Décision Robin requise avant le lot de fix** (proposition §1.6 / §Fix) :
retirer l’affichage trompeur **sans** supprimer la colonne, en arbitrant le sort
de l’édition manuelle sur la fiche.

### Sujet 2 — **NON déclenchée**

Le motif libellé/valeur est **dupliqué en template** dans `Show.vue`, pas un
composant Vue mutualisé. Le bloc Urbanisme a une particularité locale
(`sm:grid-cols-2` sur le `<dl>`). Correctif **local** possible sans duplication
forcée. Cause = classes Tailwind du projet, **pas** le thème PrimeVue.

---

## SUJET 1 — Société détentrice + badge SANS ACTE

### 1.1 Origine de la valeur « Société détentrice »

**Colonne de table** (pas calculée, pas héritée du dossier à l’affichage).

| Élément | Source |
|--------|--------|
| Colonne | `parcelles.societe_detentrice_id` (`unsignedBigInteger` **nullable**) |
| FK | `parcelles_societe_detentrice_fk` → `entites.id` ON DELETE SET NULL |
| Index | `parcelles_societe_detentrice_idx` |
| Migration | `database/migrations/2026_08_07_161000_add_societe_detentrice_id_to_parcelles_table.php` L26–45 |
| Relation | `Parcelle::societeDetentrice()` → `belongsTo(Entite::class, 'societe_detentrice_id')` (`app/Models/Parcelle.php` L171–174) |
| Libellé UI | `entites.nom` via `serializeSocieteDetentrice` |

Extrait migration (création colonne) :

```
// database/migrations/2026_08_07_161000_add_societe_detentrice_id_to_parcelles_table.php L26–30
if (! Schema::hasColumn('parcelles', 'societe_detentrice_id')) {
    Schema::table('parcelles', function (Blueprint $table): void {
        $table->unsignedBigInteger('societe_detentrice_id')->nullable()->after('type_foncier');
    });
}
```

Sérialisation fiche (`ParcelleController::serializeParcelle` L1034 + L1064–1068) :

```
// app/Http/Controllers/Foncier/ParcelleController.php L1064–1068
'societe_detentrice_id' => $societeMeta['id'],
'societe_detentrice_libelle' => $societeMeta['libelle'],
'societe_detentrice_source' => $societeMeta['source'],
'societe_detentrice_acte' => $societeMeta['acte'],
'societe_sans_acte' => $societeMeta['sans_acte'],
```

`serializeSocieteDetentrice` (L1113–1160) lit **uniquement** `societe_detentrice_id`
(+ relation / lookup `Entite::nom`) et l’existence d’un `actesFonciers`. **Aucune**
lecture du dossier foncier, de l’enseigne, ni valeur par défaut en dur « HECTARE ».

### 1.2 Renseignée en base ou affichée par défaut / héritée ?

**Renseignée en base** si `societe_detentrice_id` non NULL. Pas de DEFAULT SQL
(nullable, migration sans `->default()`). Pas d’héritage dossier à la lecture.

Alimentation mesurée :

1. **Manuelle** — patch champ `societe_detentrice_id` (whitelist contrôleur L591) ;
   création `ParcelleCreationService` (nullable, L42 / L67–68) ;
   grille Prospection (`ProspectionSaisieDialog.vue`).
2. **Acte** — `ActeAcquisitionService::creer` pose
   `societe_detentrice_id = société acquéreuse` et `etat_foncier = acquise`
   (`app/Services/Foncier/ActeAcquisitionService.php` L134–155).

Si la fiche 2104 montre « HECTARE », c’est que la FK pointe vers l’entité
nommée HECTARE **en base** (ou a été saisie ainsi) — **pas** un fallback UI
vers l’enseigne. Preuve code : display
`:display-value="local.societe_detentrice_libelle || 'non renseignée'"`
(`Show.vue` L1211) — sans id → « non renseignée », pas HECTARE.

**Non observé ici** : contenu réel de la ligne 2104 (pas de MySQL).

#### Requêtes à coller (parcelle 2104 + volumes)

Pivot actes = **`acte_parcelle`** (pas `acte_foncier_parcelle`) —
`Parcelle::actesFonciers()` L213 ; migration
`database/migrations/2026_07_30_170300_create_acte_parcelle_table.php`.

```sql
-- 1.2.a — parcelle 2104 : colonne, libellé entité, nb actes liés, état
SELECT
  p.id,
  p.section_cadastrale,
  p.numero,
  p.etat_foncier,
  p.statut_cadastral,
  p.societe_detentrice_id,
  e.nom AS societe_detentrice_nom,
  (SELECT COUNT(*) FROM acte_parcelle ap WHERE ap.parcelle_id = p.id) AS nb_actes
FROM parcelles p
LEFT JOIN entites e ON e.id = p.societe_detentrice_id
WHERE p.id = 2104;

-- 1.2.b — actes liés (détail)
SELECT af.id, af.reference_acte, af.date_acte, af.societe_acquereur_id
FROM acte_parcelle ap
JOIN actes_fonciers af ON af.id = ap.acte_foncier_id
WHERE ap.parcelle_id = 2104
ORDER BY af.date_acte DESC, af.id DESC;

-- 1.2.c — volumes (actifs cadastraux)
SELECT
  COUNT(*) AS nb_parcelles_actives,
  SUM(p.societe_detentrice_id IS NOT NULL) AS avec_societe,
  SUM(
    p.societe_detentrice_id IS NOT NULL
    AND NOT EXISTS (
      SELECT 1 FROM acte_parcelle ap WHERE ap.parcelle_id = p.id
    )
  ) AS societe_sans_acte,
  SUM(
    p.societe_detentrice_id IS NOT NULL
    AND EXISTS (
      SELECT 1 FROM acte_parcelle ap WHERE ap.parcelle_id = p.id
    )
  ) AS societe_avec_acte,
  SUM(
    p.societe_detentrice_id IS NOT NULL
    AND p.etat_foncier <> 'acquise'
    AND NOT EXISTS (
      SELECT 1 FROM acte_parcelle ap WHERE ap.parcelle_id = p.id
    )
  ) AS societe_sans_acte_non_acquise
FROM parcelles p
WHERE p.deleted_at IS NULL
  AND p.statut_cadastral = 'active';

-- 1.2.d — échantillon « trompeur » (société + sans acte + non acquise)
SELECT p.id, p.section_cadastrale, p.numero, p.etat_foncier, e.nom
FROM parcelles p
LEFT JOIN entites e ON e.id = p.societe_detentrice_id
WHERE p.deleted_at IS NULL
  AND p.statut_cadastral = 'active'
  AND p.societe_detentrice_id IS NOT NULL
  AND NOT EXISTS (SELECT 1 FROM acte_parcelle ap WHERE ap.parcelle_id = p.id)
  AND p.etat_foncier <> 'acquise'
ORDER BY p.id
LIMIT 50;
```

(Adapter `deleted_at` / `statut_cadastral` / `etat_foncier` si soft-deletes ou
codes différés — colonnes confirmées côté modèle / referentiels ; ne pas inventer
d’autres noms.)

### 1.3 Badge « SANS ACTE » — condition et autres valeurs

**Condition exacte (back)** — `ParcelleController::serializeSocieteDetentrice` L1159 :

```
'sans_acte' => $id !== null && $actePayload === null,
```

Donc : **société renseignée** ET **aucun** acte trouvé via `actesFonciers`
(dernier acte par `date_acte` / id — L1127–1135). Payload Inertia :
`parcelle.societe_sans_acte`.

**Affichage (front)** — `Show.vue` L1199–1203 :

```
<span
  v-if="parcelle.societe_sans_acte"
  class="ml-1 inline-block rounded bg-amber-100 ..."
  title="Société renseignée sans acte dans l’ERP (historique)"
>Sans acte</span>
```

**Autres « valeurs » de ce badge** : **aucune**. C’est un booléen unique
(affiché ou non). Les états voisins ne sont **pas** des badges alternatifs :

| Situation | `source` | Badge | Sous-texte |
|-----------|----------|-------|------------|
| id NULL | `null` | — | display « non renseignée » |
| id set, pas d’acte | `'manuel'` | **Sans acte** | — |
| id set + acte lié | `'acte'` | — | « Renseigné par l’acte … » (L1217–1228) |

Logique `source` L1147–1152 : acte → `'acte'` ; sinon si id → `'manuel'`.

Contradiction observée en prod (HECTARE + Sans acte + En négociation) =
**cohérente avec le code** : société manuelle / historique affichée comme
détentrice alors qu’il n’y a pas d’acte — le badge dit exactement ça.

### 1.4 Notion d’ACTE rattachée à la parcelle + bouton acquisition

**Oui** — modèle `ActeFoncier`, pivot **`acte_parcelle`**
(`acte_foncier_id`, `parcelle_id` ; + `surface_acquise_m2` #1127).

Relation : `Parcelle::actesFonciers()` L211–215.

Champs utiles côté acte (sérialisation société) : `id`, `date_acte`,
`reference_acte`, URL `/foncier/actes-fonciers/{id}`. Colonne acte acquéreur :
`actes_fonciers.societe_acquereur_id` (#1246).

**Bouton « Déclarer une acquisition »** — **pas** un POST inline qui pose la
société. C’est un `Link` Inertia vers le formulaire de création d’acte :

```
<!-- Show.vue L771–773 (header) -->
<Link :href="`/foncier/actes-fonciers/create?parcelle_id=${parcelle.id}`">
  <Button label="Déclarer une acquisition" size="small" />
</Link>
```

Doublon dans le bloc « Actes d’acquisition » si `actes_lies.length` (L809–815).

À l’enregistrement, `ActeAcquisitionService` greffe `societe_detentrice_id` +
passe l’état à `acquise` (L134–155). Tant que ce flux n’a pas tourné, la fiche
peut afficher une société **manuelle** avec badge Sans acte.

### 1.5 Lecture ailleurs dans l’ERP (masquer ≠ supprimer)

Usages **code** de `societe_detentrice` / `societe_detentrice_id` (hors tests /
build) :

| Lieu | Rôle |
|------|------|
| `PatrimoineFoncierQuery` + `Patrimoine/Index.vue` | Liste patrimoine : `whereNotNull(societe_detentrice_id)` ; compteur `sans_acte` ; colonne affichée |
| `ProspectionSaisieDialog.vue` | Colonne création rapide |
| `ParcelleController` patch + serialize | Fiche + API champ |
| `ActeAcquisitionService` / `ActeFoncierController` | Greffe + flash écrasement |
| `FieldAuditService` | Libellé audit champ |
| `ParcelleCreationService` | Validation création |

**Aucun** hit Sage / Intacct / export CSV trouvé sur ce nom de champ.

**Conséquence** : retirer l’affichage sur `Show.vue` **ne casse pas** Patrimoine
ni la greffe acte, **si** la colonne et les services restent. Point déterminant
Robin respecté : **masquer / conditionner l’UI fiche**, ne pas drop colonne.

### 1.6 Recommandation (ne pas implémenter)

**Ne pas masquer systématiquement** si `id` est set (perte d’info utile + perte
du seul éditeur inline historique — clause d’arrêt).

**Conditionner** l’affichage « détentrice » au sens métier Robin (= acte /
groupe propriétaire) :

**Condition exacte proposée (alignée code existant)** :

```
v-if="parcelle.societe_detentrice_source === 'acte'"
```

équivalent pratique :

```
v-if="parcelle.societe_detentrice_id && !parcelle.societe_sans_acte"
```

Effet : plus de ligne HECTARE + badge Sans acte côte à côte sur une parcelle
en négociation. Quand un acte existe, la ligne reste (verrouillée, sous-texte
acte).

**Arbitrages Robin avant fix :**

1. **Édition manuelle historique** : si la ligne disparaît quand `sans_acte`,
   où corriger / vider une société erronée (ex. 2104) ? Options :
   - (a) garder un mode édition discret si `peut_gestion && source !== 'acte'`
     sans libellé « détentrice » trompeur ; ou
   - (b) outil ponctuel / SQL / écran Patrimoine (aujourd’hui non éditeur) ; ou
   - (c) vider en masse les `societe_sans_acte` non acquises après mesure 1.2.
2. **Cohérence Patrimoine** : le compteur « sans acte » reste pertinent ; ne
   pas le casser.
3. **Ne pas** supprimer la colonne ni changer la greffe acte.

Avis : **(condition acte)** + arbitrage (1a) ou (1c) selon volumes 1.2.c/d.

---

## SUJET 2 — Chevauchement bloc Urbanisme

### 2.1 Composant et bloc exacts

**Fichier unique** : `resources/js/Pages/Foncier/Parcelles/Show.vue`

Bloc `.pf-urb` L1324–1376 environ :

```
<!-- Show.vue L1324–1329 -->
<div class="pf-urb rounded-lg border border-[#E5E7EB] bg-white p-4 shadow-sm">
  <h2 class="font-semibold text-[#1B2B4B]">Urbanisme</h2>
  <dl class="mt-3 grid gap-2 text-sm sm:grid-cols-2">
    <div class="flex items-center justify-between gap-2">
      <dt class="text-[#6B7280]">Zonage PLU</dt>
      <dd class="min-w-[10rem] text-right">
```

Paires : Zonage PLU · Constructible · Risque inondable · Risque feu de forêt
(chacune `flex items-center justify-between gap-2` + `dd.min-w-[10rem] text-right`).

Grille page (acquis #1449/#1452) — L2515–2529 : à `min-width: 1280px`,
`.pf-urb` est la **3ᵉ colonne** de
`grid-template-columns: minmax(0, 1fr) × 3` / area `"ident surf urb"`.

### 2.2 Cause mesurée (classes réellement appliquées)

**Pas** : hauteur fixe, `position: absolute`, `whitespace-nowrap`,
`overflow-x-hidden`, thème PrimeVue sur ce `<dl>`.

**Oui — combinaison mesurée dans le template + CSS scoped page :**

1. **Conteneur étroit** : à ≥1280px, `.pf-urb` = 1/3 de ~1280px moins gaps
   (commentaire style L2445 : paliers 860 / 1280). `minmax(0, 1fr)` +
   `.parcelle-fiche-main > * { min-width: 0 }` (L2466–2468).
2. **Sous-grille 2 colonnes dès `sm` (≥640px)** : le `<dl>` porte
   `grid … sm:grid-cols-2` (L1326). Donc **dans** la colonne déjà étroite,
   « Risque inondable » et « Risque feu de forêt » sont **côte à côte**.
3. **Paire horizontale rigide** : chaque cellule
   `flex items-center justify-between gap-2` +
   `dd.min-w-[10rem] text-right` (L1354–1369). Le `min-w-[10rem]` réserve
   ~160px à la valeur ; le libellé long wrappe ; la valeur reste à droite
   sur la même ligne flex → collision visuelle avec la cellule voisine
   (« Nofeu de » / « forêt   Non » décrit par Robin).

**Contraste mesuré** : Identification et Surfaces utilisent
`dl class="mt-3 space-y-2 text-sm"` (**pas** `sm:grid-cols-2`) — L1150,
L1250. Même motif flex + `min-w-[10rem]`, mais **une seule colonne** de
paires → largeur utile ≈ colonne entière, pas moitié.

À 860–1279px, `.pf-urb` span `urb urb` (pleine largeur du main 2-col) —
le bug est **surtout** au palier 3 colonnes (≥1280), cohérent avec une
capture desktop large. Non observé visuellement ici (pas de navigateur).

### 2.3 Mutualisation ?

| Question | Réponse |
|----------|---------|
| Composant Vue partagé (ex. `LabelValueRow.vue`) ? | **NON** |
| Même fichier ? | Oui — motifs copiés-collés dans `Show.vue` |
| Même `sm:grid-cols-2` sur Ident / Surfaces / Prop actuels (rang 1) ? | **NON** pour Ident/Surfaces (`space-y-2`) ; Urbanisme **seul** parmi les 3 du rang 1 |
| Autre `sm:grid-cols-2` + paires dans Show ? | Oui plus bas (ex. occupations L1580, etc.) — hors bug signalé |

**Impact d’une correction locale Urbanisme** : limité au bloc `.pf-urb`.
**Impact d’une correction globale** de toutes les paires
`flex…justify-between` + `min-w-[10rem]` : toute la fiche Show (Ident,
Surfaces, Occupations, Bâti, Valo…) — plus large, hors demande Robin.

**Proposition** : correctif **local** au `<dl>` Urbanisme (et éventuellement
aux 4 `div` enfants). Robin arbitre s’il veut plus tard un pattern global
« empilement ».

### 2.4 Correctif CSS/template proposé (non implémenté)

Respect : pas d’`overflow-x-hidden` ; ne pas élargir `.pf-urb` au détriment
des voisins ; ne pas toucher `grid-template-areas` #1449/#1452 ; option A
Robin = **valeur sous le libellé** si largeur insuffisante.

**Correctif local recommandé (minimal) :**

1. Remplacer sur le `<dl>` Urbanisme :
   - `class="mt-3 grid gap-2 text-sm sm:grid-cols-2"`
   - par `class="mt-3 space-y-2 text-sm"`
   (aligné Ident/Surfaces — supprime la demi-largeur fatale).
2. Sur chaque paire du bloc Urbanisme uniquement, empiler (option A) :
   - `class="flex flex-col items-stretch gap-0.5"`
   - `dd` : retirer `min-w-[10rem]`, passer en `text-left` (ou garder
     `text-right` sous le libellé si préférence visuelle).
3. Ne **pas** modifier `.parcelle-fiche-main` / media queries 860/1280.
4. Rebuild Vite + commit `public/build/` dans le **même** commit que le
   `.vue` (règle front) — lot de fix, pas ce diagnostic.

Variante « responsive » si on veut garder côte-à-côte quand c’est large
*à l’intérieur* d’urb (ex. palier 860 où urb est full-width) : container
queries ou empilement par défaut + flex-row au-delà d’un seuil custom **sur la paire**,
toujours **sans** `sm:grid-cols-2` sur le dl à ≥1280. Plus fragile ;
préférer l’empilement simple dans urb.

Vérifs manuelles (Robin / lot fix) : largeurs **1440 / 1100 / 800** — pas
de scrollbar horizontale parasite ; Ident/Surfaces/Plan inchangés.

---

## PROPOSITION D’IMPLÉMENTATION (lot fix ultérieur)

### Fichiers à toucher (proposés)

| Fichier | Sujet |
|---------|-------|
| `resources/js/Pages/Foncier/Parcelles/Show.vue` | 1 + 2 |
| `public/build/` (manifest + bundle Show) | rebuild obligatoire |
| `docs/suivi/SUIVI_XXXX_…` | fiche fix |
| Journal | **uniquement** si impact utilisateur perçu (masquage détentrice + lisibilité urb) — une entrée vulgarisée |

**Ne pas toucher** : migration colonne, `PatrimoineFoncierQuery`,
`ActeAcquisitionService`, grille CSS `#1449/#1452`, lots #1458/#1460/#1461.

### Sujet 1 — correctif exact (après arbitrage)

- Envelopper le `<div>` Société détentrice (L1196–1231) dans
  `v-if="parcelle.societe_detentrice_source === 'acte'"`  
  **ou** variante arbitrée (1a) : afficher si acte **ou**
  (`peut_gestion &&` mode correction).
- **Ne pas** nullifier la colonne en base dans le même lot sans mesure 1.2
  et validation Robin.
- Pas de changement Gate / `hasAnyM365Group` — hors sujet.

### Sujet 2 — correctif exact

- Local `.pf-urb` : `space-y-2` + `flex-col` sur les 4 paires ; retirer
  `min-w-[10rem]` sur ces `dd`.
- Rebuild + hash bundle Show dans la synthèse.

### Points restant à arbitrer (Robin)

1. Condition d’affichage détentrice exacte + sort de l’édition manuelle.
2. Purge éventuelle des `societe_sans_acte` non acquises (après SQL 1.2).
3. Empilement **toujours** dans Urb vs empilement seulement sous un seuil.
4. Correction globale des paires ailleurs dans Show : **non** recommandé
   dans le même lot.

---

## NON OBSERVÉ (limites agent)

- Contenu MySQL parcelle 2104 / volumes (requêtes fournies).
- Rendu pixel à 1440 / 1100 / 800 (pas de navigateur).
- Droits UI (pas de test `hasAnyM365Group` / Gate::before — non requis ici).

## DÉPLOIEMENT-TEST (ce lot diagnostic)

Aucune commande OVH applicative. Après push de la fiche seule :

```
git pull origin main
```

## LEÇON

- « Sans acte » **prouve** que le code sait déjà que la société n’est pas
  adossée à un acte : l’afficher à côté du libellé « Société détentrice »
  est le bug métier, pas le badge.
- Pivot actes = `acte_parcelle` (nom court) — ne pas inventer
  `acte_foncier_parcelle`.
- Urbanisme casse parce que **deux** grilles se multiplient (page 3-col ×
  dl 2-col) + `min-w-[10rem]` ; Ident/Surfaces n’ont que la première.
- Masquer une UI ≠ DROP COLUMN : Patrimoine dépend de la colonne (#1246).
