# Rapport — v3.28.0 (filtres combinables, préréglages, affichage dense, Dossier IA sur l'étoilé)

**Se livre aux DEUX installations, à l'identique. UNE migration (`tender_filter_presets`, additive
et idempotente), aucune dépendance composer nouvelle, aucune suppression de fichier.**

> ⚠️ **Aucune IA nouvelle dans ce lot.** Le Dossier IA n'est ni recalculé, ni dupliqué : il change
> d'**endroit d'affichage**. Le déclencheur reste l'étoile, la donnée reste `ai_analysis` /
> `decision_aid`, et les deux fiches — AO étoilé de la veille, AO passé en préparation — rendent
> désormais **les mêmes composants** sur **les mêmes colonnes**.

> ⚠️ **Aucun build sur le serveur.** L'affichage dense est UN fichier CSS statique livré dans
> `public/`, injecté par un render hook. Les hébergements cPanel n'ont ni Node ni Vite : rien à
> compiler, et l'archive delta ne transporte qu'un `.css` et un peu de PHP.

---

## Le problème

Depuis la moissonneuse v3.28, la Veille ne reçoit plus une poignée d'AO filtrés par mots-clés mais
**tous les AO de dix catégories du portail**. Trois conséquences, toutes vécues par le gérant :

1. le tri fin ne se refait pas à la main tous les matins ;
2. dix lignes par page et une écriture large font d'une boîte de réception une corvée ;
3. et quand il **étoile** un AO — le geste qui déclenche l'IA, la brique payante — il ouvre la fiche
   et n'y voit rien : ni GO/NO-GO, ni exigences, ni vigilance, ni bordereau. Tout ce travail ne
   s'affichait que sur la fiche d'un AO **passé en préparation**.

## §A — Des filtres qui se cumulent

Tout se cumule en ET, et tout est rappelable par un préréglage. L'essentiel existait déjà dans
`FiltresAo` (famille multi-sélection, échéance, montant min/max, période de dépôt, recherche plein
texte sur référence + acheteur + objet) ; ce lot ajoute :

- **acheteur multi-sélection** (`FiltresAo::acheteur(portee, multiple: true)`) — facultatif, pour
  que les écrans qui cherchent un acheteur à la fois gardent leur liste simple et que la **forme**
  de leur état de filtre ne change pas sous les pieds d'un préréglage ;
- **trois marqueurs de triage en positif** — « ⭐ Étoilés seulement », « Pas encore vus », « Écartés
  seulement ».

⚠️ **Le piège évité.** Les marqueurs portent sur les mêmes colonnes que les filtres « Masquer les AO
étoilés / écartés / vus », dont deux sont **cochés par défaut**. « Masquer les étoilés » + « Étoilés
seulement » ne rend jamais rien, et ne le dit pas. La règle est donc écrite une fois, dans
`FiltresAo::marqueurImpose()` : **dès que le marqueur est réglé explicitement, il a le dernier mot**
et le « masquer » correspondant se tait. Les trois filtres historiques gardent leur nom et leur
défaut — aucun test existant ne bouge.

## §B — Les combinaisons de filtres enregistrables

La table **`tender_filter_presets`** : `user_id` (cascade), `name`, `filters` (json), `sort`,
timestamps, index `(user_id, sort)`. Migration gardée par `Schema::hasTable` — rejouable.

Trois prudences, chacune à un seul endroit :

- **le scellé au propriétaire** vit en **portée globale** sur `TenderFilterPreset` (et non dans
  chaque requête d'écran) : un nouvel écran ne peut pas l'oublier. Il ne s'applique que lorsqu'un
  utilisateur est connecté — hors requête web, une commande de maintenance lit la table entière ;
- **le nettoyage** vit dans `PresetsDeFiltres::nettoyer()` : seules des clés de texte et des feuilles
  scalaires, profondeur bornée à `filtre → champ → valeurs`. Un objet Eloquent, une donnée métier ou
  une profondeur suspecte n'entrent jamais en base ;
- **la tolérance** vit dans `PresetsDeFiltres::appliquer()` : une clé de filtre que la table ne
  connaît plus est **ignorée en silence**. Un préréglage de l'an dernier applique ce qui vaut encore.

À l'écran (`PresetsDeFiltres` côté Filament, en actions d'entête de la table Veille) :
« Enregistrer les filtres » lit l'état **appliqué** (`tableFilters`), un menu « Mes filtres »
rappelle / renomme / supprime, et « Réinitialiser » enlève tout. Le rappel remplace l'état entier —
un préréglage décrit une combinaison, y compris ce qu'elle laisse décoché — puis repositionne la
pagination en première page.

## §D — L'affichage dense

Une seule feuille : `public/css/socle-dense.css`, injectée par un render hook `HEAD_END` du panneau
d'administration, avec l'empreinte du fichier en paramètre d'URL (le navigateur reprend la nouvelle
feuille dès la mise à jour, sans vider son cache). Les sélecteurs sont ceux relevés dans les vues de
Filament v5 (`fi-ta-cell`, `fi-ta-header-cell`, `fi-ta-text-description`, `fi-in-text`,
`fi-header-actions-ctn`, `fi-ac`, `fi-main`) — une classe inventée serait une règle morte.

Ce qu'elle fait : police 13 px et cellules resserrées, entêtes de colonnes **figées** au défilement
avec un fond **opaque en clair comme en sombre**, gouttière de gauche réduite, et surtout **aucune
règle `overflow: hidden`** — les barres d'actions passent à la ligne au lieu d'être coupées.

Côté PHP (`App\Filament\Support\TablesDenses`), les trois tables d'AO s'accordent : 25 / 50 / 100 /
**200** lignes par page, objet tronqué net avec le texte entier au survol, colonnes secondaires
masquables. Et sur la fiche de veille, les actions secondaires passent sous un menu « Actions » :
aucune n'a disparu, elles ont changé de place.

**Réversible** : retirer le render hook et le fichier rend exactement l'affichage d'avant.

## §E — Le Dossier IA sur la fiche de l'AO étoilé

`SectionsDossierIa` porte les trois sections d'analyse — Interprétation & prix, Aide à la décision
GO/NO-GO (avec le détail des exigences par dossier et les points de vigilance), projection du dépôt.
`TenderInfolist` et `VeilleInfolist` les posent toutes les deux. **Un seul jeu de composants**, sur
`ai_analysis` / `decision_aid` / `ai_projection` : étoiler puis « Passer en préparation » ne
reproduit ni n'écrase rien — c'est la même fiche vue depuis un autre écran.

Le déclencheur reste l'**étoile** :

- un AO **étoilé sans dossier encore calculé** affiche « **Dossier IA en attente** » — jamais des
  sections vides — et le bouton **« Analyser le dossier (IA) »**, qui emprunte exactement l'analyse
  d'AO existante (même file, même service, mêmes colonnes) pour ne pas attendre le passage de la
  moissonneuse, toutes les trois heures ;
- un AO **non étoilé** garde sa fiche légère : aucune IA n'est dépensée pour un AO que personne n'a
  retenu ;
- **retirer l'étoile n'efface pas** un dossier déjà calculé — on ne jette pas le travail — et ne
  relance rien.

## Ce qu'il faut vérifier après déploiement

1. `php artisan migrate --force` — la table `tender_filter_presets` apparaît (elle est vide).
2. Veille → monter une combinaison, « Enregistrer les filtres », « Réinitialiser », puis
   « Mes filtres → Rappeler » : la combinaison entière revient.
3. Un second compte ne voit **aucun** préréglage du premier.
4. Les trois tables d'AO : lignes plus denses, entête de colonnes figée au défilement, 200 lignes
   proposées, aucun bouton coupé à droite — en clair **et** en sombre.
5. Un AO étoilé avec un dossier : GO/NO-GO, exigences, vigilance, bordereau, tous visibles sur
   `/veille/{id}`. Un AO étoilé sans dossier : le bloc « en attente ». Un AO non étoilé : fiche
   légère, inchangée.

## Retour arrière

Aucune donnée n'est détruite par ce lot. En cas de besoin : retirer le render hook `socle-dense` et
le fichier `public/css/socle-dense.css` (l'affichage d'avant revient), et la table
`tender_filter_presets` peut rester en place sans effet — elle ne porte que des préférences
d'affichage. **Jamais de `migrate:rollback`** : la stratégie de retour arrière du socle reste la
restauration de la sauvegarde.
