---
suivi: 1579
date: 2026-09-16
sujet: Fil Suivi dossier foncier (événements + échanges)
chantier: module-foncier
type: feature
statut: poussé
hash: 83c5ea11
fichiers:
  - database/migrations/2026_09_16_180000_create_dossier_foncier_echanges_table.php
  - app/Models/DossierFoncierEchange.php
  - app/Models/DossierFoncier.php
  - app/Support/Foncier/DossierFoncierEchangeReferentiels.php
  - app/Services/Foncier/DossierFoncierSuiviAggregator.php
  - app/Http/Controllers/Foncier/DossierFoncierEchangeController.php
  - app/Http/Controllers/Foncier/DossierFoncierController.php
  - app/Support/Foncier/FoncierDocumentProfile.php
  - app/Services/Foncier/FoncierEntityDocumentAccess.php
  - app/Services/Foncier/FoncierEntityDocumentService.php
  - routes/web.php
  - resources/js/Components/Foncier/DossierSuiviColumn.vue
  - resources/js/Pages/Foncier/Dossiers/Show.vue
  - resources/js/Pages/Programmes/Foncier.vue
  - tests/Unit/DossierFoncier1579SourceTest.php
  - tests/Feature/Foncier/FoncierEntityDocumentsSourceTest.php
  - database/data/journal_mises_a_jour_post_2026_06_12.php
  - public/build/
  - docs/suivi/SUIVI_1579_fil_suivi_dossier.md
---

# SUIVI #1579 — Fil Suivi du dossier foncier

## PROMPT ENVOYÉ

Colonne Suivi à droite du dossier : événements automatiques agrégés
en lecture + table `dossier_foncier_echanges` (date, canal, sens,
issue DPO, propriétaire, note, auteur). Pièces jointes via le pipeline
documents existant. Opposition #1578 proposée, pas imposée. Dernière
activité calculée en lecture. Pas de purge 5 ans, pas de Bloctel.

## FICHES LUES

- **#1576 LEÇON** — l’onglet Suivi est `entite_commentaires`, pas un
  journal ; `date_derniere_reprise` est morte ; `updated_at` n’est pas
  un proxy métier ; documents polymorphes déjà là, pas sur le
  commentaire. Impact : nouvelle table d’échanges, agrégation en
  lecture, commentaires conservés, pas de colonne d’activité.
- **#1578** — marqueur `opposition_at` / `poserOpposition` avec
  confirmation. Impact : l’issue `opposition` propose ce marqueur,
  ne le pose pas toute seule.
- **#1117** — un seul pipeline documents. Impact : profil
  `DossierFoncierEchange`, pas un second dépôt.
- **#1533** — 32 → 16 requêtes sur le dossier. Impact : eager-load
  des échanges, une requête audits, une requête tâches lean.

## DROITS RETENUS (avant code)

Lecture du fil = visibilité du dossier (`authorize('view')` déjà
sur `show`). Pas d’ACL plus fine.

Saisie d’un échange = `userCanManageRecord` (même porte que
`update` / `associerProgramme`). `abort_unless`, pas un `v-if` seul.
Pas de middleware `foncier.gestion` sur le POST échange (le
`update` du dossier n’en a pas non plus).

Pose d’opposition depuis l’échange = `foncier.gestion`, comme #1578.
Pièces jointes = même règle que les documents du dossier
(`foncier.gestion` + `update`, middleware sur store/destroy).

`m365GroupNames()` lu via `PermissionService` / `userCanManageRecord`
(bypass déjà dans `HasRecordHabilitations`). Pas de `Gate::` dans
le contrôleur d’échanges. Compte Robin / `GS_ADMIN_ERP` non utilisé
comme preuve.

## ÉVÉNEMENTS AUTOMATIQUES AGRÉGÉS

| Type | Source | Date |
|------|--------|------|
| `creation` | `dossiers_fonciers.created_at` | created_at |
| `parcelle_rattachee` | pivot `dossier_foncier_parcelle` (déjà eager-loadé) | pivot.created_at |
| `parcelle_retiree` | `entity_field_audits` champ `perimetre_parcelle` action=retrait | created_at audit |
| `offre_emise` | `offres_foncieres` (eager) | `date_offre` sinon created_at ; montant si `foncier.prix` |
| `promesse_signee` | `promesses` si `date_signature` non null | date_signature |
| `acte_enregistre` | `actes_fonciers` (eager) | `date_acte` sinon created_at |
| `document_depose` | `entity_documents` déjà chargés (dossier, offres, promesses, avenants, actes) — pas les PJ d’échange | created_at |
| `tache_creee` | `taches_suivi` morph dossier, SELECT lean | created_at |
| `statut_change` | même requête audits, champ `statut` | created_at |

Pas de table d’événements. Les commentaires `[Périmètre]` restent
dans l’onglet discussion (évite un doublon avec l’audit de retrait).
Les retraits bulk `synchroniserDepuisDetentions` n’écrivent pas
d’audit : trou historique signalé, pas inventé ici.

## DERNIÈRE ACTIVITÉ

Max des `date_sort` du fil agrégé, en mémoire, sans colonne nouvelle.
Affichage en-tête : `Dernière activité : ` + `Carbon::locale('fr')->diffForHumans()`.
`date_derniere_reprise` non alimentée. Performance OK à l’échelle
d’une fiche (pas de GREATEST SQL supplémentaire).

## REQUÊTES FICHE DOSSIER

Avant (#1533 / `showInertiaProps` d’`origin/main` 7d9c396f) : eager-load
existant (~37 clés `with`) + requête tâches panneau + `dernierContactAt()`
+ `types_composantes_prix`. Ordre de grandeur #1533 : **16** requêtes.

Après : les mêmes, plus
- 3 clés eager `echanges.{auteur,proprietaire,documents.uploader}` (typiquement 4–5 requêtes Eloquent)
- 1 `entity_field_audits` (`statut` + `perimetre_parcelle`)
- 1 `taches_suivi` lean (id, objet, created_at)

Soit **environ +6 / +7** requêtes, total estimé **~22–23**. Pas de N+1
par événement. Compte runtime MySQL non mesuré ici (pas de prod sous
la main). Colonne d’activité **non** ajoutée.

`dernierContactAt()` (MAX commentaires) est encore appelé dans
`serializeShow` : conservé pour le champ `dernier_contact` existant,
distinct de la dernière activité.

## AMBIGUÏTÉ D’INTERFACE (signalée, non tranchée)

L’onglet **Suivi** reste le fil `entite_commentaires` (titre « Fil de
suivi »). La **colonne** à droite s’appelle aussi Suivi et porte le
journal. Les deux coexistent volontairement. Pas de renommage de
l’onglet dans ce lot.

## MAPPING UI ↔ POST ↔ BACK

Canal, sens, issue : voir synthèse livrable. Colonne SQL `date`
(date métier de l’échange, pas `created_at`).

## DÉPLOIEMENT-TEST

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

## LEÇON

Un journal DPO n’est pas un fil de discussion : table typée +
agrégation en lecture des faits déjà datés. Confondre refus de vendre
et opposition se joue dans les libellés, pas dans un booléen. La
dernière activité se calcule sur le fil, `updated_at` ne dit rien.
Ne pas créer de table d’événements « pour plus tard » : les sources
existent déjà.
