---
suivi: 1567
date: 2026-09-16
sujet: Parcelle — responsable de développement (entête fiche)
chantier: foncier / parcelles
type: feature
statut: poussé
hash: 90f9ff62
fichiers:
  - database/migrations/2026_09_16_100000_add_responsable_id_to_parcelles.php
  - app/Models/Parcelle.php
  - app/Support/Foncier/ParcelleResponsableDeveloppement.php
  - app/Services/Foncier/ParcelleCreationService.php
  - app/Services/Foncier/FieldAuditService.php
  - app/Http/Controllers/Foncier/ParcelleController.php
  - routes/web.php
  - resources/js/Pages/Foncier/Parcelles/Show.vue
  - tests/Unit/Parcelle1567SourceTest.php
  - database/data/journal_mises_a_jour_post_2026_06_12.php
  - public/build/
  - docs/suivi/SUIVI_1567_responsable_developpement_parcelle.md
---

# SUIVI #1567 — Responsable de développement sur la fiche parcelle

## PROMPT ENVOYÉ

Remplacer dans l’entête de la fiche parcelle « Secteur XXX » / « Hors de
votre secteur » par un responsable de développement (colonne
`parcelles.responsable_id`, défaut = créateur, modifiable, audit, droits
restreints). Secteur conservé partout ailleurs. Push `origin/main`.

## FICHES LUES

- **#1447 LEÇON** — mention « Pour modifier, cliquez sur une valeur » +
  `InlineEditableField` ; ne pas défaire. Impact : même composant pour
  le responsable dans l’entête.
- **#1449** — grille / nav / pas d’`overflow-x-hidden`. Impact : entête
  seulement, `grid-template-areas` inchangées.
- **#1514 LEÇON** — le secteur n’est plus un cran de visibilité ; tables
  et écran Secteurs conservés. Impact : ne pas retirer `secteur_suggestion`
  ni le filtre liste.
- **#1564 LEÇON** — `Show.vue` zone commune, rebase Vite. Impact : HEAD
  `origin/main` = #1566 avant build.
- **#1116** — `createur_id` posé à la création, backfill historique.
  Impact : source unique retenue pour la reprise.

## SYNTHÈSE

### Source créateur retenue

`parcelles.createur_id` — colonne dédiée depuis #1116, renseignée à
chaque création via `ParcelleCreationService::createPrepared`. Pas de
`created_by` sur `parcelles`. Pas d’usage d’une table d’audit comme
source de reprise. Pas de repli (user 1 interdit). Les lignes sans
`createur_id` restent `responsable_id` NULL et sont comptées par la
migration (echo `[#1567] Reprise…`).

### Liste du sélecteur

Utilisateurs **actifs** = `users.preapproved_at IS NOT NULL`. Liste
**ouverte** (tous les préapprouvés), pas restreinte à un groupe M365.
Le responsable courant est ajouté s’il n’est plus préapprouvé, pour
que le select affiche la valeur en cours. Option « Choisir… »
uniquement tant que la valeur est vide (jamais postée).

### Mapping sélecteur

| Libellé affiché | Valeur postée | Colonne DB | Libellé back |
|---|---|---|---|
| Prénom NOM (`users.name`) | `users.id` (entier) | `parcelles.responsable_id` | Responsable développement |
| non attribué (affichage seul) | — (non posté) | NULL | — |
| Choisir… (édition si vide) | — (non posté, `required` serveur) | — | — |

### Droits

Peuvent changer : responsable actuel, `GS_DIRECTION_GENERALE`,
`GS_ADMIN_ERP`. Contrôle `abort_unless` + `m365GroupNames()` brut dans
`updateChamp` (helper `ParcelleResponsableDeveloppement`). Pas de Gate,
pas d’`authorize`, pas de `hasAnyM365Group` pour ce test (le
`Gate::before` admin fausserait un refus).

Le PATCH `/foncier/parcelles/{parcelle}/champ` n’a plus le middleware
`foncier.gestion` : le groupe parent est `foncier.lecture`, pour que DG
atteigne `abort_unless`. Les autres champs restent protégés par
`assertGestion()` dans la même méthode.

Une fois attribué, `required` + absence d’option vide empêchent de
revenir à NULL.

### Audit

`responsable_id` ajouté à `Parcelle::auditedFields()` / labels.
`FieldAuditService::formatValeurAffichee` résout l’id vers `users.name`.

## DÉPLOIEMENT-TEST

```
git pull origin main
php artisan migrate --force
php artisan optimize:clear
php artisan view:clear
php artisan journal:sync
```

Relever dans le log migrate : `[#1567] Reprise responsable_id depuis createur_id — total=… mis_a_jour=… restes_null=…`

## LEÇON

Le créateur de parcelle est `createur_id`, pas un `created_by`. Pour un
droit « pas l’admin via Gate::before », lire `m365GroupNames()` brut.
Un middleware `foncier.gestion` sur la route inline empêche DG d’atteindre
le `abort_unless` métier : le relâcher à la lecture du groupe, et garder
`assertGestion()` pour les autres champs. Ne pas vider un responsable
déjà posé (`required`, pas d’option nulle une fois attribué).
