---
suivi: 1354
date: 2026-08-14
sujet: widget_factures_bloquees
chantier: dashboard
type: feat
statut: poussé
hash: 7344f9eb
fichiers:
  - config/dashboard_widgets.php
  - app/Services/Dashboard/WidgetDataService.php
  - app/Services/Dashboard/DashboardDefaultLayoutService.php
  - resources/js/Components/Dashboard/WidgetBody.vue
  - database/data/journal_mises_a_jour_post_2026_06_12.php
  - tools/diag/preuve_factures_bloquees_1354.php
  - public/build/
  - docs/suivi/SUIVI_1354_widget_factures_bloquees.md
---

## PROMPT ENVOYÉ

SUIVI #1354 — widget dashboard « Factures bloquées » (liste + redirect),
réutiliser le statut bloqué existant, filtrage FactureVisibility serveur,
opt-in catalogue (pas défaut), preuve compteurs peuplés, pas de prune,
ne pas toucher workflow / DepotFactureController / Comptabilisation /
AppShell / useFormatters / lots #1350–#1353.

## FICHES LUES

- `#1352` LEÇON : opt-in catalogue ; prune NON lancé ; zone dashboard
  commune — **rebasé sur main après #1352/#1353**.
- `#1333` LEÇON : limite 4 lignes ; hauteur catalogue ; FactureVisibility.
- `#1331` LEÇON : orphelins conservés ; prune explicite seulement.
- `#1327` : `position_x = 0` seed — dette connue, **non corrigée**.

## SYNTHÈSE

### Définition « bloqué » (réutilisée, non inventée)

| Élément | Valeur |
|---|---|
| Table / champs | `workflow_validations.bloque` + `statut` |
| Critère | `bloque = true` **ET** `statut = 'en_attente'` |
| Modèle | `WorkflowValidation::estBloquee()` — `app/Models/WorkflowValidation.php` L49-51 |
| Pose | `FactureController::processBloquer` ~L1306-1310 (`bloque`, `bloque_motif`, `bloque_par_user_id`, `bloque_at`) |
| UI existante | `WorkflowBloqueBadge.vue` + `WorkflowValidationBloquePresenter` |

Le widget **lit** uniquement. Aucune écriture workflow.

### Données affichées

fournisseur, programme (ou marché `intitule`), montant HT, date de
blocage (`bloque_at`), motif (`bloque_motif`), lien `/factures/fiche/{id}`.
Limite 4 + lien « + N autres » → `/validations`. État vide :
« Aucune facture bloquée ».

### Droits

`FactureVisibility::canViewFacture` après tampon SQL (même pattern
`factures_a_valider`). Groupes via `m365GroupNames()` brut (pas
`hasAnyM365Group`). Catalogue `required_permissions => []`.

### Widgets avant / après

| Clé | Avant | Après |
|---|---|---|
| `factures_a_valider` | catalogue + défaut | inchangé |
| `depot_facture` | catalogue (pas défaut) | inchangé |
| `factures_bloquees` | absent | catalogue seulement (pas défaut) |
| 10 clés obsolete | orphelines conservées | inchangé — **prune non lancé** |

### Preuve filtrage

Script `tools/diag/preuve_factures_bloquees_1354.php` : total live +
fixture transactionnelle ROLLBACK si vide. Discrimination admin 3 /
GS_HECTARE 2 / GS_ENVOL 1. Piège évité : `User::$id` non fillable +
`validateur_id` NULL ⇒ `(int)0===(int)0`.

### Hors périmètre

DepotFactureController, ComptabilisationController, AppShell,
useFormatters, ObjectSubNavBar, workflow écriture, #1350–#1353.
Pas de migration. `position_x = 0` non réparé.

## DÉPLOIEMENT-TEST

```bash
git pull origin main
php artisan optimize:clear && php artisan view:clear
php artisan journal:sync
php -d display_errors=1 tools/diag/preuve_factures_bloquees_1354.php
```

Pas de migrate. Pas de route:clear (API widget existante).
**Ne pas** lancer `dashboard:prune-obsolete-widgets`.

## LEÇON

1. Statut bloqué = `WorkflowValidation::estBloquee()` uniquement — ne pas
   redéfinir.
2. Preuve de droits : peupler le filtre (compteurs) ; `m365GroupNames()`
   brut ; compte non-admin ; attention `(int) null === (int) null` sur
   `validateur_id` / `user->id` non assigné.
3. Opt-in dashboard : catalogue oui, `resolveDefaultWidgetKeys` non ;
   jamais prune orphelins.
4. `depotFacture` latestOfMany : colonnes `depot_factures.*` qualifiées.
