# Rapport — v3.20.0 (le parcours de l'AO : tri, filtres, rattrapage)

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

## Le constat

Le cockpit Appels d'Offres ne se lisait pas comme un cycle. Le menu offrait « Appels d'offres » —
une liste unique où le dossier à peine ouvert voisinait avec le marché déjà tranché —, « Veille »,
« Marchés » et « Concurrents ». L'avancement d'un AO ne se voyait nulle part.

Et la Veille, remplie par la moissonneuse, gardait un angle mort : le découpage du bloc source
(v3.19.0) se fait **à l'arrivée**. Les ~120 fiches entrées **avant** cette version portaient encore
leur bloc brut — objet illisible, acheteur « à compléter », aucune date de clôture, donc aucun
filtre qui les attrape.

## §1 — `php artisan veille:rattraper`

Une commande qui relit le bloc **déjà stocké** et complète la fiche. Elle n'appelle aucun modèle,
n'ouvre aucune page, ne va chercher aucune donnée nouvelle : tout ce qu'elle écrit était déjà là,
mal rangé.

Elle **ne duplique aucune règle** : le découpage vient de `VeilleBlocSource`, la complétion de
`VeilleImportService::completerDepuisBlocSource()` — nouvelle méthode publique qui s'appuie sur le
`completer()` de l'ingestion. Donc la même garantie, mot pour mot : **on complète, on n'écrase
pas**. Un objet corrigé à la main n'est plus un bloc source et n'est même pas examiné ; un acheteur,
une date ou une heure déjà saisis sont préservés.

- **Idempotente** : une fiche rattrapée n'est plus un bloc source, le second passage l'ignore.
- **Par lots** (`chunkById`, `--par=200` par défaut) : la table n'est jamais chargée en entier.
- **Base vide** : 0 traité, et elle le dit — pas d'erreur.
- Elle affiche son compte : « X fiche(s) rattrapée(s), Y ignorée(s) ».
- Elle est **transverse au socle** : elle ne cite aucun identifiant, aucun montant, aucune raison
  sociale. Elle n'a donc rien à faire sous `reprise:` et ne porte aucune garde de société.

## §2 — L'étoile

Deux colonnes nouvelles sur `tenders`, additives et idempotentes : **`favori`** et **`masque`**
(booléens, défaut `false`, indexés).

L'étoile est **transversale** : un AO de n'importe quelle étape peut être suivi de près. Elle se
pose d'un clic depuis la colonne ⭐ de la Veille et des AO, depuis le menu d'actions d'une ligne,
depuis la fiche, et **en lot**. Elle ne touche ni `participation` ni `status` : **une veille étoilée
reste une veille, donc hors de tous nos indicateurs** — c'est prouvé par test.

## §3 — Le parcours en menu

Le groupe « Marchés & appels d'offres » se lit désormais dans l'ordre du cycle :

| # | Entrée | Portée |
|---|---|---|
| 10 | **Veille (AO non soumissionnés)** | `participation = veille`, sans les étoilés ni les écartés |
| 15 | **⭐ AO étoile** | `favori = true`, toutes étapes confondues (badge = le compte) |
| 20 | **AO à préparer** | `participation = soumission`, pas encore déposé |
| 25 | **AO soumissionnés** | `participation = soumission`, déposé / admissible / retenu / écarté |
| 30 | **Marchés** | inchangé |
| 35 | **Concurrents** | inchangé |

Les quatre vues d'AO sont des **portées** sur la même table, avec les mêmes fiches, les mêmes
policies et le même formulaire — rien n'est dupliqué :

- « AO à préparer » et « AO soumissionnés » sont **deux pages de `TenderResource`**, exposées par un
  `getNavigationItems()` qui rend deux entrées. L'ancienne entrée « Appels d'offres » a disparu :
  la page d'index **est** « AO soumissionnés ». Aucune entrée ne liste plus tout.
- « ⭐ AO étoile » est une ressource à **une seule page**. Elle ne possède pas de fiches, elle en
  désigne : « Ouvrir la fiche » renvoie chaque AO vers **sa** page — la Veille ou les AO. Sans quoi
  le socle aurait une troisième page de consultation à maintenir pour les mêmes données.

**Un AO avance d'un clic** : ⭐ étoiler → « Passer en préparation » (veille/étoile → soumission +
en préparation + étape préparation) → « Marquer déposé » → « → marché ». Les transitions vivent dans
`App\Services\Tenders\ParcoursAoService`, pas dans une action Filament : elles doivent être vraies
depuis une ligne, depuis une fiche, depuis un lot et depuis un test.

⚠️ **Le seul pas qui fait entrer un AO dans nos indicateurs est « Passer en préparation »**, et il
est explicite (confirmation à l'appui). Un AO déjà déposé ne peut pas revenir en arrière : on ne
dé-dépose pas un dossier.

### Où vont les AO annulés

« AO à préparer » est le **complément exact** de « AO soumissionnés » : tout ce qui n'est pas
déposé. Un AO annulé n'a jamais été déposé — le ranger parmi nos dépôts serait faux, et le laisser
hors des deux vues le ferait **disparaître du menu**. Il reste donc du côté pré-dépôt.

## §4 — Tri & filtres, écrits une fois

`App\Filament\Admin\Resources\Tenders\Tables\FiltresAo` porte les filtres des AO, et les quatre vues
y puisent : acheteur, **famille d'objet** (la déduction v3.19.0, en multi-sélection), statut, étape,
mode de passation, **fourchette de montant**, **échéance** (≤ 7 / 15 / 30 j, à venir, passée),
période de dépôt. Ce qui se cherche dans la Veille se cherche pareil dans nos offres.

La recherche porte partout sur **objet + acheteur + référence** ; le tri par défaut est
`deposit_date` — les clôtures les plus proches en tête pour les vues actives (Veille, ⭐), les
dépôts les plus récents en tête pour les vues clôturées.

⚠️ **Un piège SQLite évité** : la fourchette de montant s'écrit en deux comparaisons de colonne, et
non en `COALESCE(estimated_amount, market_price) >= ?`. SQLite n'applique l'affinité numérique d'une
colonne que lorsque la colonne est l'un des deux termes ; passée dans un `COALESCE`, l'expression la
perd, le paramètre reste du texte (PDO ne connaît pas le type `float`) et **la comparaison est
toujours fausse** — un filtre qui ne rend jamais rien, sans la moindre erreur pour le dire.

## §5 — Actions groupées & masquer le bruit

Sur la Veille : **étoiler**, **passer en préparation** et **écarter** plusieurs fiches d'un coup.

« Écarter de la veille » pose `masque` : la fiche **sort de la liste mais reste en base**. Rien
n'est supprimé — ses concurrents, son prix gagnant et ses écarts continuent de nourrir l'estimation
et le palmarès. « Réafficher » annule, à la ligne ou en lot.

Le vivier cache par défaut les AO **étoilés** (ils ont leur page) et les AO **écartés** — par deux
filtres **cochés par défaut**, jamais par un `where` en dur : décocher les ramène à l'écran, où
l'action « Réafficher » les attend. Une portée en dur les aurait rendus inatteignables.

## La recette

`tests/Feature/Veille/VeilleRattrapageTest.php` (6 tests) et
`tests/Feature/Tenders/ParcoursAoTest.php` (16 tests), aucune donnée réelle :

- **Rattrapage** : une fiche brute est rattrapée (objet propre + acheteur + date + heure, bloc
  d'origine conservé dans les notes, **aucun job en file**) ; un objet corrigé à la main et un
  acheteur/date déjà saisis restent intacts ; **idempotente** ; nos soumissions ne sont pas
  touchées ; **base vide** → 0, pas d'erreur ; le traitement par lots n'oublie rien.
- **Étoile** : le service pose et retire sans toucher la participation ; la vue liste exactement
  les favoris, veille et soumissions confondues ; **étoiler une veille ne bouge ni le taux de
  réussite ni le CA gagné** ; le clic sur la ligne fonctionne.
- **Les vues** : chacune montre exactement ses AO ; un AO qu'on fait avancer change de vue ; un AO
  déposé ne revient pas en préparation ; un AO annulé ne disparaît pas du menu.
- **Écarter** : la fiche sort de la liste, n'est pas supprimée, et « Réafficher » la ramène ; les
  quatre actions groupées travaillent par paquets.
- **Tri & filtres** : tri `deposit_date` dans les deux sens ; famille, acheteur, montant, échéance,
  recherche.
- **Menu** : le parcours est dans l'ordre, et `TenderResource` n'expose plus que ses deux pas.
- **Migration rejouable**, **base vide** : les quatre vues se rendent sans un seul AO.

Non-régression : la suite existante est verte, y compris `VeilleScopingTest` (la veille reste hors
KPI), `VeilleIngestApiTest` (v3.18), `VeilleEnrichissementTest` (v3.19) et la projection v3.16.

Les tests de colonnes des AO (`TendersTableColumnsTest`, `LiensExternesTest`) ont été ajustés au
partage des vues : ceux qui portent sur un AO tranché restent sur « AO soumissionnés », ceux qui
portent sur un AO non déposé ont rejoint « AO à préparer ». Les colonnes elles-mêmes n'ont pas
changé.

## À faire après déploiement

1. **Lancer une fois** `php artisan veille:rattraper` sur chaque installation, et lire son compte.
2. Ouvrir le menu : le parcours doit se lire Veille → ⭐ → à préparer → soumissionnés → Marchés →
   Concurrents.
3. Étoiler deux ou trois AO depuis la Veille, vérifier qu'ils quittent le vivier pour « ⭐ AO
   étoile », puis en passer un en préparation.
4. Écarter quelques fiches hors cible, puis décocher « Masquer les AO écartés » pour vérifier
   qu'elles sont bien là, et non perdues.
