---
suivi: 1451
date: 2026-09-04
sujet: Demandes juridiques — sélecteur suiveurs depuis l’annuaire M365
chantier: juridique
type: feature
statut: poussé
hash: be09b684
fichiers:
  - app/Support/Juridique/DemandeJuridiqueSuiveurResolver.php
  - app/Http/Requests/Juridique/StoreSuiveurDemandeJuridiqueRequest.php
  - app/Http/Requests/Juridique/StoreDemandeJuridiqueRequest.php
  - app/Http/Controllers/DemandeJuridiqueController.php
  - app/Support/Juridique/DemandeJuridiquePresenter.php
  - app/Services/DemandeJuridiqueService.php
  - resources/js/Pages/Juridique/Create.vue
  - resources/js/Pages/Juridique/Show.vue
  - tests/Unit/DemandeJuridiqueSuiveursGraph1451SourceTest.php
  - tests/Feature/DemandeJuridiqueSuiveurTest.php
  - tests/Feature/DemandeJuridiqueStoreTest.php
  - database/data/journal_mises_a_jour_post_2026_06_12.php
  - public/build/
  - docs/suivi/SUIVI_1451_suiveurs_annuaire_m365.md
---

# SUIVI #1451 — Suiveurs depuis l’annuaire M365

## PROMPT ENVOYÉ

Sélecteur de suiveurs (création + fiche) : proposer tous les collaborateurs
de l’annuaire M365, y compris jamais connectés à l’ERP. Réutiliser le
mécanisme Graph existant, pas de nouveau client. Pas de migration sans
arbitrage. Ne pas toucher au juriste (#1398), ni au format Teams (#1399).

## FICHES LUES

- **#1379 LEÇON** : un seul cache `graph:m365_users:list:v366` (60 min) ;
  résolution ERP à l’enregistrement via `M365CommercialUserResolver`, pas
  à l’ouverture de page (évite 35 comptes fantômes). **Réutilisé tel quel
  pour les suiveurs.** Le #1389 cité dans le prompt n’a pas de fiche
  `docs/suivi/` : le mécanisme d’annuaire ERP est celui du #1379 / #1352
  (`getUtilisateursGraphOptionsCached`).
- **#1398** : juriste = `JuristeResolver` + `GS_JURIDIQUE` — **non touché**.
- **#1399** : format HTML Teams du dépôt — **non touché**
  (`NotifierAjoutSuiveurDemandeJuridique` inchangé).
- **#1377 LEÇON** : droits via `m365GroupNames()` brut, pas
  `hasAnyM365Group` / Gate. Appliqué à `peutGererSuiveurs`.

## SYNTHÈSE

### Mécanisme Graph réutilisé

| Couche | Fichier / lignes |
|--------|------------------|
| Liste membres groupe Hectare | `GraphApiService::getTousLesUtilisateurs()` L136–189 |
| Cache 60 min | `GraphApiService::getUtilisateursGraphOptionsCached()` L213–240, clé `graph:m365_users:list:v366` |
| Jeton | `resolveMicrosoftToken()` L508+ (délégué, session / `users.microsoft_token`) — lecture seule, même jeton que #1379 |
| Mapping select | `DemandeJuridiqueDemandeurResolver::optionsGraphPourSelect()` L102–136 |
| Création ERP à la volée | `M365CommercialUserResolver::resolveToUser()` L45–85 |
| Filtre e-mail interne | `GraphInternalMemberFilter::isInternalEmail()` L57–67 |

Aucun nouveau client Graph. Endpoint `/api/graph/users/search-collaborators`
(`searchUtilisateursTenant`, 1 appel Graph par frappe) **non utilisé** :
inacceptable ici.

### Cache

Durée **60 minutes**. Un nouvel arrivant dans GS_GROUPE peut manquer jusqu’à
1 h. **Ce n’est pas** le cache ACL 5 min (#1389 / Karine TROIANO) : les
groupes M365 de session ne sont pas relus ici. Effet analogue possible :
collègue ajouté au groupe Hectare invisible pendant le TTL du cache liste.

Chargement : **un** appel Graph (ou lecture cache) à l’ouverture de la
page. La recherche PrimeVue (`filter`) est **côté client**. Cinq frappes
« beno » = **0** appel Graph.

### Table `demande_suiveurs`

MySQL `127.0.0.1:3306` refusé au moment de la mesure (cible refuse la
connexion). Schéma lu dans
`database/migrations/2026_07_03_100000_create_juridique_recolte_tables.php`
L71–89 :

- `id`
- `demande_juridique_id` FK `demandes_juridiques.id` ON DELETE CASCADE
- `user_id` FK `users.id` ON DELETE CASCADE
- `timestamps`
- UNIQUE `(demande_juridique_id, user_id)`

FK vers `users.id` : un azure_id seul ne peut pas être inséré.

### Rattachement d’un jamais-connecté

**Retenu : créer le `users` à la volée** (même #1379 demandeur). Pas de
migration. À la sélection, si l’option porte un UUID Graph, `resolveToUser`
crée `azure_id` + e-mail M365 + `name` = displayName + `preapproved_at`.
Le pivot stocke `user_id`. Alternative azure_id dans le pivot = migration
— écartée.

### Notifications Teams

`NotifierAjoutSuiveurDemandeJuridique` envoie `destinataire_email` au
webhook Power Automate (#1399). L’e-mail M365 suffit ; le compte n’a pas
besoin de s’être connecté à l’ERP. `resolveToUser` recopie `mail` Graph
sur `users.email` avant l’envoi. Cloche ERP : `alertes.user_id` pointe
vers le user créé ; visible à la première connexion.

### Tri

Conserve le tri Graph : `mb_strtolower(label)` = displayName
(prénom puis nom : Alexandra PORTALES, Anne PORTALES, …).

### Filtrage comptes de service

Liste = membres du groupe Hectare (`GROUPE_HECTARE_ID`), type user,
plus filtre domaines `hectaregroupe.fr` / `hectare.fr` / `envol.fr`.
Invités et domaines externes exclus. Compte désactivé / boîte partagée
présents dans GS_GROUPE avec un mail interne : **peuvent apparaître**
(le cache #1379 ne porte pas `accountEnabled`). Une boîte hors groupe
n’apparaît pas.

### Droits

`peutGererSuiveurs` : `m365GroupNames()` brut ∩
`GS_JURIDIQUE` / `GS_DSI` / `GS_ADMIN_ERP`. Juriste inchangé.

### Mapping UI ↔ back (suiveurs)

| Libellé affiché | Valeur postée | Colonne DB | Libellé back |
|-----------------|---------------|------------|--------------|
| `label` Graph | `users.id` si compte ERP, sinon `azure_id` UUID | `demande_suiveurs.user_id` → FK `users.id` | `users.name` |

## DÉPLOIEMENT-TEST

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

Pas de migration. Pas de `route:clear` (routes inchangées).

## LEÇON

Le sélecteur suiveurs lisait `users` (première connexion). Le demandeur
#1379 lisait déjà Graph. Réutiliser `optionsGraphPourSelect` + création
à la volée, et **ne pas** caster l’id en `Number()` côté Vue (ça
éliminait les UUID). Recherche = filter PrimeVue sur la liste cachée,
jamais `searchUtilisateursTenant` à chaque frappe.
