---
suivi: 1052
date: 2026-07-29
sujet: Diagnostic étiquettes personnelles / projet (portée + cohabitation Q2A)
chantier: taches_suivi
type: diagnostic
statut: poussé
hash: c552c943
fichiers:
  - tools/diag/diag_etiquettes_1052.php
  - docs/suivi/SUIVI_1052_diag_etiquettes_personnelles_projet.md
---

## PROMPT ENVOYÉ

SUIVI #1052 — PHASE 1 diagnostic uniquement. Besoin : étiquettes personnelles (hors projet)
et communes aux membres (dans un projet), cohabitation Q2A sans fuite, regroupement Kanban
par étiquette (Q3) accepté. Préserver étiquettes type programme. Lecture code + SELECT
prod. Aucun fix / migration / .vue.

## SYNTHÈSE

### A — État des lieux

#### A1. Types en prod
Aujourd’hui le code ne **crée** que `type=programme` (`EtiquetteProgrammeService`).
Constantes modèle : `programme` | `groupe` | `projet` — `groupe`/`projet` **jamais créés**.
Volumes réels : exécuter `php tools/diag/diag_etiquettes_1052.php` en prod (section A1).

#### A2. Points d’écriture (exhaustif)
| Fichier | Lignes | Rôle |
|---|---|---|
| `EtiquetteProgrammeService.php` | 16–50 | **Seul INSERT** `etiquettes` (`firstOrCreate` type=programme) + réactive `actif` |
| `TacheSuiviService::syncEtiquettes` | 699–702 | **Seul sync pivot** `etiquettes()->sync($ids)` |
| `TacheSuiviService::creer/modifier` | 68–71 / 119–124 | Déclenche sync |
| `resolveEtiquetteIdsFromData` | 990–1003 | `programmes[]` → service (peut créer) ; `etiquettes[]` → filtre IDs existants |
| `TacheSuiviController::store/update` | 33–38 / 78–84 | Entrée HTTP |
| `AssistantTacheSuiviService` | ~110–125 | Peut passer `programmes[]` → même pipeline |

**`syncEtiquettes` EXISTE** : méthode **privée** de `TacheSuiviService` (L699), pas une API publique.
Aucun CRUD admin étiquettes. Suppression tâche → cascade pivot (`tache_suivi_id`) ; l’étiquette programme **reste**.

#### A3. Lectures
- Mes tâches suivi : `MesTachesController` L100 eager + L192–197 serialize
- Mes tâches ops : étiquette **virtuelle** #1051 (`virtuelle: true`, non DB)
- Presenter fiche : `TacheSuiviPresenter` L57–64
- Formulaire : `TacheSuiviFormDialog` L109–111 (`programmes` dérivés de `etiquettes[].programme_id`)
- Regroupement : `mesTachesGrouping.js` L134–168 (`prog-*`)
- Assistant consolidation : `AssistantConsolidationTools` (libelle exposé comme `nom`)
- Dashboard programme : filtre `type=programme` + `actif`

#### A4. UI gestion
**Aucune** UI admin d’étiquettes. Création = **lazy** au rattachement programme d’une tâche
(`firstOrCreate`). `libelle` DB souvent NULL ; UI affiche `programmes.libelle`.

#### A5. `actif`
Positionné `true` à la création ; réactivé si false dans `EtiquetteProgrammeService`.
Utilisé pour **visibilité** tâche (`scopeVisiblePourUtilisateur` L272–273) et dashboard
programme — **pas** filtré dans le serialize Mes tâches / Presenter.

#### A6. Affichage cartes
| Surface | Étiquettes ? |
|---|---|
| `TacheSuiviListItem` (cartes + liste) | Oui — `TacheSuiviProgrammeTag` |
| `TacheSuiviKanbanCard` | **Non** (#1048 : `#{id}` + avatar créateur ; pas de tags) |
| `MesTachesOperationalListItem` | **Non** |
| DetailModal / Show / ProgrammeTacheSuiviLigne | Oui |

### B — Ce qui manque

#### B1. Schéma cible proposé
Réutiliser `type` **et** FKs (les deux) — un enum seul ne porte pas la portée.

```
etiquettes :
  id
  type ENUM/string : programme | personnelle | projet   (# TYPE_GROUPE inutilisé → ne pas recycler sans décision)
  programme_id NULLABLE FK programmes     -- UNIQUEMENT si type=programme
  user_id      NULLABLE FK users           -- UNIQUEMENT si type=personnelle (auteur/propriétaire)
  projet_id    NULLABLE FK projets         -- UNIQUEMENT si type=projet
  libelle      VARCHAR NOT NULL pour personnelle/projet ; NULL ok pour programme (libellé via programmes)
  actif
  timestamps
```

Contraintes :
- CHECK / app : exactement une portée selon type (mutuellement exclusif)
- UNIQUE `(type, programme_id)` pour programme (déjà le pattern firstOrCreate)
- UNIQUE `(type, user_id, libelle)` personnelle ; UNIQUE `(type, projet_id, libelle)` projet
- Index : `(type, user_id)`, `(type, projet_id)`, `(type, programme_id)`

`TYPE_PROJET` existe déjà dans le modèle mais **jamais persisté** — réutilisable pour les
étiquettes communes projet. Ajouter `TYPE_PERSONNELLE = 'personnelle'`.

#### B2. Pivot
`tache_suivi_etiquette (tache_suivi_id, etiquette_id)` **suffit** pour lier tâche↔étiquette.
Pas besoin d’y stocker la portée (elle est sur `etiquettes`).

⚠️ **Ops (`tasks`) hors pivot.** Conséquence : les étiquettes libres (personnelles/projet)
concernent **uniquement les tâches de suivi**, sauf chantier séparé (nouveau pivot `task_id`).
Les ops gardent l’étiquette programme **virtuelle** #1051 pour le regroupement programme.

#### B3. Membre projet
Table `projet_personnes (projet_id, user_id)`.
Membre = `projets.createur_id = user` OR `projet_personnes.user_id = user`
(`Projet::estMembre`, scope visibilité Option A L288–297).

#### B4. Filtre portée (invariant Q2A) — couche DONNÉES
Une étiquette est visible pour `$user` ssi :

```
type = programme
OR (type = personnelle AND user_id = :userId)
OR (type = projet AND projet_id IN (
      SELECT id FROM projets WHERE createur_id = :userId
      UNION
      SELECT projet_id FROM projet_personnes WHERE user_id = :userId
    ))
```

**Où l’appliquer (inviolable)** :
1. Relation / scope Eloquent `Etiquette::scopeVisiblesPour(User)` (défaut de toute lecture)
2. Eager load Mes tâches / Presenter : `etiquettes` **contraint** par ce scope (pas un filtre Vue)
3. Endpoints create/update/sync : refuser d’attacher une étiquette hors portée
4. Liste « picker » d’étiquettes : même scope

⚠️ Q2A : le filtre porte sur l’**étiquette**, pas sur « tâche dans un projet ». Une personnelle
sur une tâche projet reste invisible aux autres membres.

#### B5. Volumes
Exécuter le script en prod (section B5). Local Cursor = non preuve.

### C — Risques

#### C1. Régression `prog-*` (RISQUE N°1)
Le front fait `key = programme_id ?? id ?? libelle`. Une étiquette personnelle **sans**
`programme_id` créerait une fausse colonne `prog-{id_etiquette}` et sortirait la tâche de
« Sans programme » (car `etiquettes.length > 0`).

**Parade obligatoire phase 2** : dans `mesTachesGrouping` (branche programme), ne considérer
que `et.type === 'programme' || et.programme_id != null` (et/ou séparer le payload).
Sans ce filtre, le chantier casse le regroupement programme.

#### C2. Virtuelle #1051
`virtuelle: true`, `id: null`, type programme. Cartes ops **n’affichent pas** les tags ;
formulaire édition = tâches de suivi uniquement → pas de confusion édition. Préserver le
flag / ne jamais `sync` une virtuelle.

#### C3. Regroupement par étiquette (Q3)
Aujourd’hui : **pas** de groupe `etiquette` dans Mes tâches (options : echeance|statut|
programme|projet|aucun). Kanban colonnes = 100 % front (`groupMesTachesItems`).
Deux users → colonnes différentes = **OK** (décision Q3).
`position` (#1028) est **partagée** : tri intra-colonne ; des colonnes différentes ne
partagent pas l’ordre visuel — acceptable, à documenter (pas d’ordre Kanban « étiquette »
commun).

#### C4. Perf
Eager load actuel `etiquettes.programme` = 2 requêtes batch. Après : même pattern + filtre
scope sur la relation (`where` dans `with([... => fn])`) — toujours O(1), pas N+1.
IDs projets membres : 1 requête (ou cache request) avant le with.

### D — Recommandation (sans coder)

#### D1. Migration à venir (esquisse)
- `etiquettes` : +`user_id`, +`projet_id` ; élargir `type` ; contraintes/index ci-dessus
- pivot inchangé
- backfill : 0 (existantes = programme uniquement)
- AUCUNE écriture de données métier au diag

#### D2. Filtrage inviolable
Scope Eloquent + contrainte dans **tous** les serialize / sync / pickers (couche 1).
Jamais un `v-if` front comme seule barrière.
Phase 2 devra aussi durcir `mesTachesGrouping` programme (C1) avant d’émettre des
étiquettes non-programme dans le même tableau.

## DÉPLOIEMENT-TEST

1. `git pull origin main`
2. OVH : `php tools/diag/diag_etiquettes_1052.php`
3. Coller A1 + B5 dans le ticket. Aucune migration / view:clear / journal:sync.

## LEÇON

Le tableau `etiquettes` porte aujourd’hui un seul usage métier (rattachement programme),
mais le modèle a déjà des types réservés non branchés. Étendre sans filtrer le regroupement
`prog-*` ni la portée au niveau données transformerait un classement crédible en fuite
d’information personnelle (Q2A) ou en fausses colonnes programme (C1).
