---
suivi: 1206
date: 2026-08-05
sujet: Diagnostic — tâches de suivi sur dossier foncier (avant modèle)
chantier: foncier / taches
type: diagnostic
statut: poussé
hash: 109b7ab4
fichiers:
  - tools/diag/diag_taches_dossier_foncier_1206.php
  - docs/suivi/SUIVI_1206_diag_taches_dossier_foncier.md
  - docs/dictionnaire/taches.md
---

# SUIVI #1206 — PHASE 1 DIAGNOSTIC (lecture seule)

## PROMPT ENVOYÉ

Cartographie tâches de suivi × dossier foncier avant décision de modèle.
Q6=B polymorphe sauf si >15 lecteurs de `taches_suivi.programme_id` → alors A.
Q7=B (Mes tâches + retard + digest). Q8=A (visibilité = habilitation dossier).
Q9=A (panneau SYNTHÈSE uniquement). Pas de code métier.

## DÉCISIONS ROBIN (instruite, non rediscutées)

| Q | Choix | Implication |
|---|-------|-------------|
| Q6 | B polymorphe (sauf seuil 15) | Voir objet 2 — **seuil non atteint** |
| Q7 | B | Remonter dans Mes tâches / retard / digest 9h |
| Q8 | A | Visibilité alignée habilitation dossier (D62) |
| Q9 | A | Panneau sur SYNTHÈSE seulement |

---

## OBJET 1 — Schéma réel `taches_suivi` + satellites

### `taches_suivi` (migration `2026_07_02_100000` + ajouts)

| Colonne | Null | Notes |
|---------|------|-------|
| id | NON | PK |
| uuid | NON | unique |
| objet | NON | varchar(300) |
| description | OUI | text |
| date_echeance | OUI | date — retard = calcul (`isPast` + non fermée), pas de colonne |
| url | OUI | |
| importance | NON | défaut `normal` — constantes modèle |
| statut | NON | défaut `non_demarre` — constantes modèle |
| validation_mode | NON | défaut évolué vers `createur_et_associes` |
| createur_id | OUI | FK users **nullOnDelete** |
| projet_id | OUI | ajout #1012 — FK projets |
| position | NON | kanban #1028 |
| termine_at / termine_par_id | OUI | |
| timestamps + softDeletes | | |

**`programme_id` : ABSENT** (confirmé `Schema::hasColumn` local + dictionnaire).
**Pas de `AuditsFieldChanges`** sur `TacheSuivi`. SoftDeletes oui.
Scopes : `ouvertes`, `parStatut`, `enRetard`, `visiblePourUtilisateur`.
Pas de morph `attachable_*` aujourd’hui.

### Statuts (constantes `TacheSuivi::`)

`non_demarre` · `a_traiter` · `en_cours` · `en_attente` · `en_pause` · `termine` · `abandonne`
Écrits par `TacheSuiviService` / contrôleur (changerStatut, valider, kanban).

### Satellites

| Table | Nature |
|-------|--------|
| `tache_suivi_personnes` | pivot BelongsToMany User (+ assignee_at) |
| `tache_suivi_etiquette` | pivot BelongsToMany Etiquette |
| `etiquettes` | `type` programme\|personnelle\|projet ; **`programme_id` nullable** ici |
| `tache_suivi_commentaires` (+ mentions) | HasMany |
| `tache_suivi_documents` | HasMany disk local |
| `tache_suivi_checklist_items` | HasMany |
| `projets` / `projet_personnes` | regroupement #1012 |

**Rattachement programme actuel = étiquette `TYPE_PROGRAMME` + pivot**, pas une FK sur la tâche.

---

## OBJET 2 — Lecteurs de `taches_suivi.programme_id` (chiffre qui décide)

### Verdict colonne

**TOTAL lecteurs de `taches_suivi.programme_id` = 0**
La colonne n’existe pas. Aucun `where('taches_suivi.programme_id'`, aucun fillable.

→ Seuil Robin « > 15 ⇒ recommander A » : **non atteint** → **recommandation Q6 = B (polymorphe)** confirmée par le critère.

### Architecture réelle à ne pas confondre

Le « rattachement programme » vit dans `etiquettes.programme_id` + `tache_suivi_etiquette`.
Lecteurs de **ce mécanisme** (à reprendre pour un attachable) — tableau :

| fichier:zone | nature | bloquant polymorphe ? |
|--------------|--------|----------------------|
| `TacheSuivi.php` `scopeVisiblePourUtilisateur` (~294) | lecture visibility chemin 3 | oui — ajouter chemin dossier |
| `ProgrammeDashboardController.php` `tachesSuiviPourProgramme` (~207) | lecture panneau | oui — miroir dossier |
| `TacheSuiviPresenter.php` `serializeEtiquette` | lecture/prop | adaptable (origine attachable) |
| `MesTachesController.php` filtre + virtuelles #1051 | lecture/filtre | oui — filtre/origine dossier |
| `resources/js/Pages/MesTaches/Index.vue` | prop/filtre Vue | oui — libellé origine |
| `resources/js/utils/mesTachesGrouping.js` | groupement Vue | oui — groupe « Dossier » |
| `TacheSuiviProgrammeTag.vue` + `tacheSuiviLabels.js` | UI tag | adaptable |
| `ProgrammeTacheSuiviLigne.vue` | UI panneau | adaptable (réutilisable) |
| `TacheSuiviFormDialog.vue` `programmes` / `prefillProgrammeId` | écriture étiquette | oui — préremplir dossier |
| `TacheSuiviService.php` + `EtiquetteProgrammeService` | écriture sync étiquettes | oui — sync attachable |
| `EtiquetteLibreService.php` garde TYPE_PROGRAMME | lecture | adaptable |
| `AssistantTacheSuiviRetrievalService.php` `programme_libelle` | lecture | adaptable |
| `AssistantConsolidationTools.php` select etiquettes | lecture | adaptable |
| `User.php` PREF filtre programme | préférence | adaptable |
| Routes `/taches-suivi/*` | **pas** imbriquées sous `/programmes/{id}` | favorable à B |

**Lecteurs distincts du mécanisme étiquette-programme (ordre de grandeur) : ~14 fichiers app+front métier** — sous le seuil 15 **de la colonne** (0), et sous/proche du seuil si on compte le mécanisme : **B reste la recommandation Robin**.

### Ce que B impose de reprendre

1. Colonnes `attachable_type` / `attachable_id` (+ index morph) sur `taches_suivi`.
2. Backfill : tâches liées à étiquette programme → `Programme` morph (garder étiquette en transition).
3. Filtre panneau / Mes tâches / digest / visibility : brancher sur morph **et** conserver étiquette programme pour BC UI.
4. Formulaire création : accepter `dossier_foncier_id` (ou attachable) en plus des programmes.
5. Q8 : AND habilitation dossier sur tous les chemins de liste.

**Option A (`dossier_foncier_id` nullable)** : non retenue par le critère Robin (0 lecteur colonne). Moins généralisable (marché/lot/acquéreur).

---

## OBJET 3 — Panneau « Tâches du programme »

| Élément | Emplacement |
|---------|-------------|
| Page | `resources/js/Pages/Programmes/Dashboard.vue` (~354–422) |
| Ligne | `Components/TachesSuivi/ProgrammeTacheSuiviLigne.vue` |
| Payload | prop `taches_suivi_programme` : `{ items, total, truncated }` |
| PHP | `ProgrammeDashboardController::tachesSuiviPourProgramme` → `TacheSuiviPresenter::serializeListItem` |
| Création | `TacheSuiviFormDialog` avec `prefillProgrammeId=programme.id` → POST `/taches-suivi` |
| Détail | `TacheSuiviDetailModal` |

**Réutilisabilité :** la ligne + le dialog sont génériques ; le **titre** et le **filtre** sont couplés programme (whereHas étiquette). Extraire un panneau `EntityTachesSuiviPanel` avec props `title`, `items`, `prefillAttachable` est faisable sans réécrire le design.

**Requêtes :** 1 count + 1 get limit 20 + with createur/personnes + withCount commentaires/documents/checklist — **pas de N+1** sur le panneau actuel. Sur dossier Show (déjà lourd) : +2 requêtes si même pattern — acceptable.

---

## OBJET 4 — Digest 9h, notifications, Mes tâches

### Digest

- Middleware web `DeclencheDigestQuotidien` (GET authentifié ≥9h ouvrés) → `terminate` → `Artisan::call('taches:digest')` **en PHP-FPM**.
- `TachesDigestCommand` + `MesTachesResumeService::resumePourDigest` : sélection = créateur ∪ pivot personnes (pas de regroupement SQL par programme).
- Regroupement affichage digest : listes retards/urgents — **pas** un bucket « par programme » obligatoire.
- Pour Q7 : une tâche dossier assignée apparaît déjà dans le digest **si** créateur/assigné ; il faudra **filtrer** celles dont le dossier n’est pas visible (Q8) pour éviter la fuite.

### Teams

- Assignation / clôture : `TacheSuiviService::notifierAssignationPersonnes` / `notifierClotureTache` → `TeamsNotificationService` — contexte **web** (action utilisateur).
- Digest Teams : déclenché **web** (middleware), pas cron CLI vers powerplatform.
- Échecs : `Log::error` digest ; assignation ignore encore le booléen retour (#1054 dette).

### Mes tâches

- Origine programme = étiquette `type=programme` (tag UI) ou virtuelle ops (#1051).
- Libellé **pas** entièrement en dur : `libelleEtiquetteProgramme` / `programme.libelle`.
- Pour dossier : ajouter tag/origine `Dossier {reference}` dans presenter + grouping.

---

## OBJET 5 — Visibilité croisée (Q8 = A) — risque n°1

### Tâche aujourd’hui (`scopeVisiblePourUtilisateur`)

1. Créateur **OU**
2. Personne pivot **OU**
3. Étiquette programme + accès programme (ou DG) **OU**
4. Membre du projet rattaché

Policy `view` = `estVisiblePar` (ce scope).

### Dossier aujourd’hui (`HasRecordHabilitations`)

- Scope global `RecordHabilitationScope`
- `userCanViewRecord` : bypass DG/Admin/DSI · créateur · ligne `record_habilitations` lecture|gestion

### Règle combinée à implémenter (Q8 = A)

> Tâche dont `attachable` = `DossierFoncier` : visible **seulement si** `dossier->userCanViewRecord($user)`.
> **Même si** l’utilisateur est assigné ou créateur — sauf décision contraire Robin (exception créateur/assigné hors habilitation = fuite D62).

**Recommandation diag :** appliquer le filtre dossier **strict** (y compris assigné), et documenter qu’assigner quelqu’un hors liste blanche dossier est incohérent — l’UI de sélection de personnes devrait filtrer les habilités.

### Points de fuite si on ajoute le rattachement SANS toucher la visibilité

1. **Mes tâches** — liste créateur/assigné sans check dossier
2. **Digest 9h / badge retard** — idem `MesTachesResumeService`
3. **Teams assignation** — ping à un email hors dossier
4. **`GET /taches-suivi/{id}`** — policy view actuelle
5. **Assistant retrieval / consolidation** — si indexe les tâches
6. **Kanban / filtres** — pas d’export dédié identifié

**= risque n°1 du lot.**

---

## OBJET 6 — Plan de migration proposé (écrit, non appliqué)

### Recommandé : Q6 = B

```text
Schema::table('taches_suivi', function (Blueprint $t) {
  $t->nullableMorphs('attachable'); // attachable_type, attachable_id + index
});
```

**Backfill (script, pas dans up() lourd) :**
```sql
-- pseudo : pour chaque tache liée à une étiquette type=programme active
UPDATE taches_suivi t
JOIN tache_suivi_etiquette p ON ...
JOIN etiquettes e ON e.type='programme' AND e.programme_id IS NOT NULL
SET attachable_type = 'App\\Models\\Programme', attachable_id = e.programme_id
WHERE t.attachable_id IS NULL;
```
Volumétrie : `COUNT(*)` via diag OVH section 2–3 (local stub = 0 — **ne pas citer comme prod**).

**`programme_id` sur taches_suivi :** n’existe pas → rien à conserver/retirer.
**Étiquettes programme :** conserver pendant transition (UI Mes tâches / tags) ; déprécier plus tard une fois le morph généralisé.

**down() :** drop morph columns — sûr si aucun code prod ne les lit encore ; backfill non rejoué.

**Alternative A** (non retenue) : `dossier_foncier_id` nullable FK — moins de fichiers, pas généralisable marché/lot.

---

## OBJET 7 — Découpage en lots (série front)

| Lot | Contenu | Fichiers sensibles | Dépendance |
|-----|---------|-------------------|------------|
| **1206b** | Migration morph + backfill + modèle relations | migrations, `TacheSuivi` | — |
| **1206c** | Visibility Q8 + digest/Mes tâches filtre dossier | `TacheSuivi`, `MesTachesResumeService`, policy | 1206b |
| **1206d** | API création/update attachable dossier + form dialog | `TacheSuiviService`, `TacheSuiviFormDialog.vue` | 1206b |
| **1206e** | Panneau SYNTHÈSE dossier (Q9) | `Dossiers/Show.vue`, `AppShell` si labels, presenter | 1206c+d ; **série** avec autres lots Show |
| **1206f** | Tags/grouping Mes tâches origine dossier | `MesTaches/Index.vue`, `mesTachesGrouping.js` | 1206c ; **série** MesTaches |

Rappel : un seul build `manifest.json` à la fois — ne pas paralléliser 1206e et 1206f avec un autre lot front foncier.

---

## SCRIPT OVH

```bash
php tools/diag/diag_taches_dossier_foncier_1206.php
```

Coller la sortie dans le suivi de déploiement (volumes prod).

## LEÇON

Ne pas inventer `taches_suivi.programme_id` : le rattachement programme est déjà un **morph soft** via étiquettes. Un `attachable` polymorphe est un **second** ancrage plus strict — à synchroniser avec les étiquettes, pas à confondre avec une colonne fantôme.
