---
suivi: 1602
date: 2026-09-17
sujet: Responsable foncier / développement sur le programme
chantier: programmes
type: feature
statut: poussé
hash: 67a1ad4c
fichiers:
  - database/migrations/2026_09_17_180000_create_programme_responsable_foncier_table.php
  - app/Models/ProgrammeResponsableFoncier.php
  - app/Models/Programme.php
  - app/Support/ProgrammeResponsablesFonciers.php
  - app/Http/Controllers/Concerns/AuthorizesProgrammeVefaAccess.php
  - app/Http/Controllers/ProgrammeController.php
  - resources/js/Pages/Programmes/Edit.vue
  - resources/js/Pages/Programmes/Create.vue
  - tests/Unit/ProgrammeResponsablesFonciers1602SourceTest.php
  - database/data/journal_mises_a_jour_post_2026_06_12.php
  - public/build/
  - docs/suivi/SUIVI_1602_responsable_foncier_programme.md
---

# SUIVI #1602 — Responsable foncier / développement du programme

## PROMPT ENVOYÉ

Ajouter sur la fiche programme un rôle « Responsable foncier / développement »
(plusieurs titulaires). Deux origines : développeurs hérités du dossier
(lecture seule, jamais copiés) et développeurs saisis sur le programme
(MultiSelect, même vivier que les trois rôles existants). Ne pas modifier
les trois rôles uniques. Recenser les autres écrans, proposer sans ajouter
partout. Sérialiser après #1600.

## FICHES LUES

- **#1556 LEÇON** — `dossiers_fonciers.programme_id` ; ne pas passer par
  `Programme::dossierFoncier()` pour un lecteur programme (ACL dossier).
  Impact : hérités via `DossierProgrammeLink::dossierPourProgrammeSansAcl`.
- **#1576** — confirme `dossier_foncier_responsable.user_id` N⇄N. Impact :
  relation `DossierFoncier::responsables()`, aucune écriture Foncier.
- **#1567 LEÇON** — `m365GroupNames()` brut, pas Gate::before. Impact :
  mêmes groupes lus en brut pour UI + `abort_unless`.
- **#1372** — `Programmes/Edit.vue` INFOS / responsables. Impact : bloc
  existant conservé ; MultiSelect ajouté à la suite, même `:options="users"`.
- **#1598 LEÇON** (rappel prompt) — php -l / Reflection ne voient pas une
  variable d’une branche. Impact : revue des closures store/update.
- **#1600** — sérialisation : ce lot ne touche pas AppShell ni la page
  bancaire. `Edit.vue` n’est pas dans la liste des zones les plus disputées
  mais rebase Vite après #1600.

## CHEMIN DES HÉRITÉS (établi avant code)

1. Programme → dossier vivant : `dossiers_fonciers.programme_id`
   (`DossierProgrammeLink::dossierPourProgrammeSansAcl`, #1556 / #1597).
   Sans `RecordHabilitationScope` : un éditeur programme doit voir les
   noms même s’il n’a pas l’ACL dossier. On ne passe **pas** par
   `Programme::dossierFoncier()` (HasOne, scope habilitation).
2. Dossier → utilisateurs : table `dossier_foncier_responsable`
   (`dossier_foncier_id`, `user_id`, unique), créée
   `database/migrations/2026_07_30_150300_create_dossier_foncier_pivots_table.php`.
   Modèle : `DossierFoncier::responsables()` belongsToMany User.
3. Rien n’est recopié dans `programme_responsable_foncier`. Un changement
   sur le dossier se voit au prochain affichage du programme.

## DROITS

Les trois rôles existants (suivi commercialisation, RP, comptable) se
sauvent aujourd’hui par `PUT programmes.update` **sans** `abort_unless`
métier (session auth du groupe web). `programmes.ecriture` n’est **pas**
le critère (GS_HECTARE / GS_ENVOL n’ont souvent pas cette permission).

Règle retenue pour le foncier **saisi** : `m365GroupNames()` brut ∩
`GS_ENVOL`, `GS_HECTARE`, `GS_ADMIN_ERP`, `GS_DIRECTION_GENERALE`, `GS_DSI`.
`abort_unless` uniquement si les IDs manuels **changent**. Un compte hors
groupes peut encore enregistrer INFOS. Un POST qui change les IDs → 403.
`v-if` / `:disabled` seuls ne suffisent pas. Le compte Robin (GS_ADMIN_ERP)
ne prouve pas le refus (`Gate::before`).

Lecture des hérités : tout utilisateur qui ouvre l’édition programme.

## JUSTIFICATION PIVOT

`programme_responsables` est UNIQUE (`programme_id`, `role`) : un titulaire
par rôle. Le foncier est N utilisateurs. Nouvelle table
`programme_responsable_foncier` (FKs courtes `prf_*`). Les hérités du
dossier n’y figurent pas.

## ÉCRANS RECENSÉS (question 4) — propositions

| Écran | Les 3 rôles aujourd’hui | Foncier #1602 |
|---|---|---|
| `Programmes/Edit.vue` INFOS | Saisie | **FAIT** (hérités + manuels) |
| `Programmes/Create.vue` | Saisie | **FAIT** (hérités si `from_dossier`) |
| `Programmes/CreateHorsErp.vue` | Absents | **skip** |
| Liste `Programmes/Index.vue` | Non affichés (complétude : suivi + RP + comptable) | **proposer skip** (rôle facultatif) |
| Dashboard programme | Non | **proposer skip** |
| Completeness / bandeau incomplet | 3 rôles obligatoires | **proposer skip** |
| `Programmes/Responsables.vue` (Graph / microsoft_id) | Autre écran, rôles différents | **proposer skip** |
| Hub `Programmes/Foncier.vue` | Non | **proposer skip** |
| Validation NV1 facture | `responsable_programme` | **skip** (interdit d’élargir) |
| Notifications / tâches | RP / commercial | **skip** (interdit d’en créer) |
| Exports (trésorerie, grille, AO) | Non | **proposer skip** |

Robin arbitrera les « proposer ». Rien n’a été ajouté hors Edit/Create.

## MAPPING SÉLECTEUR MULTIPLE

Libellé affiché : `users.name` (email en sous-ligne d’option).
Valeur postée : `responsable_foncier_ids[]` = `users.id` (entiers).
Libellé back : `programme_responsable_foncier.user_id`.
Vivier : `User::query()->orderBy('name')` — **le même** que les trois
Select existants, pas `users_responsables` du dossier.

Flag `responsable_foncier_sync=true` : FormData omet un tableau vide ;
sans ce flag un client ancien ne vide pas la pivot par accident.

Hérités : non postés.

## SYNTHÈSE

Deux bandes visuelles : hérités (fond gris, lien dossier, pas de croix) ;
manuels (MultiSelect chips). Doublon : bandeau ambre + flash `warning`.
Pas de N+1 liste (hérités uniquement sur create/edit, 1 dossier + 1 load
responsables). Module Foncier non modifié.

## DÉPLOIEMENT-TEST

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

## LEÇON

Ne pas recopier `dossier_foncier_responsable` sur le programme : la copie
diverge. Lire via `DossierProgrammeLink` sans ACL. `abort_unless` seulement
si les IDs manuels changent, sinon INFOS devient 403. FormData + tableau
vide : flag de sync. php -l ne voit pas une variable d’une branche.
