---
suivi: 1254
date: 2026-08-07
sujet: Hotfix croisement piloté/libre explorateur SharePoint chantier
chantier: sharepoint / suivi-chantier / marches
type: hotfix
statut: poussé
hash: 98e490a5
fichiers:
  - app/Services/SharePointChantierExplorerService.php
  - app/Services/SharePointService.php
  - app/Http/Controllers/MarcheChantierExplorerController.php
  - resources/js/Components/SharePoint/SharePointGedExplorer.vue
  - tests/Unit/SharePointChantierExplorerServiceTest.php
  - database/data/journal_mises_a_jour_post_2026_06_12.php
  - public/build/
  - docs/suivi/SUIVI_1254_hotfix_croisement_explorateur.md
---

# SUIVI #1254 — Hotfix croisement explorateur chantier

## PROMPT ENVOYÉ

Hotfix : croisement piloté / libre cassé (#1250). Cause exacte, fix item_id
uniquement, refus serveur, vérif multi-emplacements, commit+push.

## FICHES LUES

- `#1250` — explorateur + `buildErpItemIndex` + trois états ; refus serveur déjà posé
  sur rename/delete/move via `assertWritableItem`.
- `#1236` / `#1239` — croiser sur `item_id`, jamais URL ; IDs rattrapés en base.
- `#1232` — `erp_index.ids` vide → faux « dépôts manuels » ; rafraîchir l’index.
- `#1184` — eager load partiel = donnée invisible sans erreur.
- `#1251` — déjà poussé ; **Show.vue non modifié** (sérialisation respectée).

## OBJET 1 — Cause exacte

**Cause retenue : comparaison d’`item_id` sensible à la casse (et normalisation absente),
combinée à un index Eloquent `whereHas` moins robuste.**

Preuve unitaire (#1254) :

- Index clé `01T6OSQJLOOTFLYRRREV` (casse DB / rattrapage)
- Fichier Graph `id` = `01t6osqjloOtFlyRrrev` → **avant** : `isset($index[$id])` = false
  → `erp_status=libre`, boutons rename/delete affichés
- **après** `normalizeItemId()` (trim, BOM, contrôles, `strtoupper`) → match `pilote`

Autres défauts corrigés dans le même passage :

1. Index reconstruit en **jointures SQL** (`marche_documents` ⋈ `marches`, etc.) —
   plus de `whereHas` / eager load partiel (#1184) pour les devis AO.
2. Listing : tout élément **non-dossier** est un fichier (plus d’exclusion si facet
   `file` vide) ; croisement aussi sur `remoteItem.id` (raccourcis).
3. `assertWritableItem` : refuse aussi les **dossiers** présents dans l’index (ex. dossier AO).
4. Front : `can_write === false` honoré **même si** `showErpLockStatus` était faux.

Non retenues : croisement par URL (code #1250 ne le faisait pas) ; scope marché seul
(index déjà programme-wide) ; prop front absente (bundle a bien `show-erp-lock-status:!0`).

## SYNTHÈSE

| Point | Détail |
|-------|--------|
| Critère | `normalizeItemId(sharepoint_item_id)` ∈ index |
| Tables | `marche_documents`, `avenant_documents`, `appels_offres` (DCE, estimatif, folder), `ao_devis` |
| Refus serveur | déjà #1250 ; renforcé (dossiers pilotés + message avec écran) |
| Coût | inchangé : 0 Graph au rendu fiche ; 1 list (+ breadcrumb) au dépliage ; 1 index SQL |
| Show.vue | **non touché** (#1251 déjà livré) |

## DÉPLOIEMENT-TEST

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

Pas de migrate. Pas de `npm`/`composer` sur OVH.

## LEÇON

Les `item_id` Graph ne sont pas garantis dans la même casse que la valeur stockée
(rattrapage inventaire vs listing app). Toute table de croisement doit **normaliser**
(casse + trim) des deux côtés. Un `isset($index[$id])` brut suffit à transformer
186 documents « avec item_id » en faux libres — sans erreur visible.
