# Rapport — v3.15.0 (la fiche AO se lit en PDF, le concurrent s'analyse, l'activité AO se pilote)

**Version du socle : se livre aux DEUX installations, à l'identique, par le même tarball.
Une migration additive et idempotente (`tenders`, cinq colonnes nullable). Aucune dépendance,
aucune suppression, aucun renommage de fichier.**

Jusqu'ici, le seul PDF d'un appel d'offres était la **note d'analyse IA** : précieuse pour
décider, trop dense pour relire un dossier ou y chercher un chiffre. Le gérant demandait à sortir
en PDF **ce qu'il a sous les yeux**. C'est le fait qui commande tout ce lot : le livrable de
lecture, c'est LA PAGE de l'AO — on la met en document fidèlement, on n'y rajoute pas d'analyse.

## 1. Les champs qui manquaient sur l'AO

Cinq colonnes, toutes nullable, toutes ajoutées derrière un `Schema::hasColumn` (la migration est
donc rejouable) :

| Colonne | Ce qu'elle porte |
|---|---|
| `pmmp_url` | le lien de la consultation sur marchespublics.gov.ma, collé à la main |
| `ref_consultation` | l'identifiant de la consultation sur le portail |
| `org_acronyme` | l'acronyme de l'organisme acheteur (`g3h`, `o3f`, `m1w`, `z0h`, `q1s`…) |
| `our_elimination_stage` | **notre** étape d'élimination — jusqu'ici seulement noyée dans les notes |
| `jury_date` | la date de la commission, distincte de `result_date` |

`Tender::notrePosition()` dérive « notre position » (l'étape où nous avons été écartés, sinon ce
que dit le statut, sinon « — ») : la fiche à l'écran et son PDF lisent la même méthode, sur le
modèle — aucun des deux ne réinvente le libellé.

`Tender::pmmpLink()` rend `pmmp_url` s'il est saisi ; sinon, si les deux identifiants sont là, il
**reconstruit** l'URL du suivi de commission (`…page=entreprise.SuiviConsultation&refConsultation=…&orgAcronyme=…`) ;
sinon `null` — et la section portail disparaît alors de la fiche comme du PDF.

Formulaire : le lien PMMP en pleine largeur au-dessus du lien Drive (onglet « Liens & notes »),
les deux identifiants dessous ; `jury_date` et « Notre étape d'élimination » à la suite de « Date
du résultat » (onglet « Concurrence »). Fiche : une section « Portail PMMP » avant « Suivi »,
visible seulement si l'un des trois champs est rempli, plus « Jury / commission » et « Notre
position » dans l'analyse de la concurrence.

## 2. Les trois documents

Tous trois étendent le gabarit commun `resources/views/pdf/layout.blade.php` (DejaVu Sans, charte
de l'**installation** — jamais une marque en dur), passent par le lecteur de documents, sont tracés
en audit et servent `?inline=1` en aperçu, l'`attachment` sinon. **Règle dompdf de la v3.13.5
respectée** : chaque contrôleur instancie sa vue et la rend une seule fois.

| Document | Route | Permission | Trace |
|---|---|---|---|
| Fiche appel d'offres | `tender.fiche-pdf` — `GET /ao/{tender}/fiche` | `tenders.export` | `tender_fiche_pdf` |
| Analyse concurrent | `competitor.insight-pdf` — `GET /concurrents/{competitor}/analyse` | `competitors.export` | `competitor_insight_pdf` |
| Analyse des AO | `tenders.overview-pdf` — `GET /ao-analyse-globale` | `tenders.export` | `tenders_overview_pdf` |

**La fiche** reprend la page section par section : identité (statut en badge coloré + étape),
montants en cartes, portail PMMP, groupement, notes. Et **uniquement si l'AO est décidé**
(`retenu` / `ecarte` / `annule`) : une rangée de cartes (concurrent retenu, offre gagnante, date
du résultat + jury, soumissionnaires, **notre position**) puis le tableau de concurrence issu de
`CompetitorInsightService::tenderComparison()` — la ligne retenue surlignée doré, les écartés
grisés avec l'étape en pastille, et la note « écart / notre offre : positif = concurrent plus cher
que nous ». Sur un AO en préparation, ce bloc n'existe pas.

**L'analyse concurrent** s'appuie sur `CompetitorInsightService::competitorStats()` — rien n'y est
recalculé — et y ajoute l'historique complet de ses soumissions, du plus récent au plus ancien,
avec le **face-à-face** : notre offre sur le même AO, ou « <installation> retenue » quand l'AO nous
est revenu. Le nom vient de `Identite::nomCourt()`, jamais d'une société écrite dans le code.

**L'analyse globale** respecte la doctrine « notre part » : le CA gagné est la somme des
`notrePart(our_offer)` sur les retenus, pas la somme brute des offres du groupement. KPI, top 8
des acheteurs, top 15 des concurrents rencontrés, puis la synthèse ligne à ligne.

Boutons : « Fiche PDF » en en-tête de `ViewTender` juste après « Modifier » (c'est **le** document
de lecture) et en action de ligne ; « Analyse PDF » sur `ViewCompetitor` et en ligne dans
`CompetitorsTable` ; « Analyse PDF » sur `ListTenders`, juste avant l'export Excel. La note
d'analyse IA reste exactement où elle était.

⚠️ `/ao-analyse-globale` ne commence pas par `/ao/` : la liste blanche du lecteur de documents a
reçu son propre préfixe, ainsi que `/concurrents/`. Sans quoi le bouton aurait mené à un 403.

## 3. Recette

Suite complète verte, `pint`, `npm run build`. Couverture ajoutée
(`tests/Feature/Tenders/TenderPortailFieldsTest.php`, `tests/Feature/Pdf/TenderPdfExportsTest.php`) :

- les cinq colonnes existent après `migrate` ; la migration **rejouée** ne casse pas ;
- `pmmpLink()` : URL saisie → rendue ; sinon reconstruite depuis les deux identifiants ; sinon
  `null` (un seul des deux ne suffit pas) ; `our_elimination_stage` bien casté ;
- les trois routes répondent **200 / `application/pdf`** avec la permission, **403** sans ;
- le bloc « Résultat & concurrence » **n'apparaît pas** sur un AO en préparation et **apparaît**
  sur un AO écarté, avec sa ligne de concurrence et « Notre position » ;
- l'analyse concurrent liste chaque soumission et marque le face-à-face de l'installation sur un
  AO gagné par nous ;
- les KPI tombent juste (2 gagnés / 3 perdus → 40 %, CA = somme des `notrePart` des retenus) et la
  page **se rend sur une base vide** ;
- non-régression : la note d'analyse IA (`tender.analysis-pdf`) répond toujours ;
- le lecteur de documents encadre bien les trois nouvelles URL.

## 4. Livraison

Le tarball se compose du diff de version :

```bash
git diff --name-only --diff-filter=ACMR v3.14.1 v3.15.0
```

**Les deux installations reçoivent le même paquet.** La migration est additive et idempotente :
`php artisan migrate --force` est sûr des deux côtés — y compris sur RUWAD, qui n'a aucun AO et
où les trois documents se rendent en zéros et tableaux vides.

Ordre, comme toujours :

1. **Sauvegarde phpMyAdmin de la base** (avant tout) ;
2. extraction du paquet ;
3. `php artisan migrate --force` ;
4. `php artisan optimize:clear` (les routes changent : le cache de routes doit tomber).

## 5. Donnée de référence — IRADA uniquement

`audit-portail-2026-08/enrichissement-passe12-identifiants-portail.sql` relie les AO IRADA déjà
relevés à leur consultation portail : il remplit `ref_consultation` / `org_acronyme`
**seulement s'ils sont vides** (fill-only, rejouable) et pose `our_elimination_stage` sur les
quatre AO où IRADA a été écartée (t8 technique, t15 financière, t36 et t37 technique).

**À jouer sur IRADA seulement, APRÈS la migration.** RUWAD n'a pas ces AO — les identifiants y
désigneraient d'autres lignes. Ce n'est volontairement **pas** une commande `reprise:*` : c'est une
correction de données ponctuelle, pas une reconstruction.

## 6. Fichiers

**Ajoutés** — `database/migrations/2026_08_26_100000_add_portail_fields_to_tenders_table.php`,
`app/Http/Controllers/TenderFichePdfController.php`,
`app/Http/Controllers/CompetitorInsightPdfController.php`,
`app/Http/Controllers/TendersOverviewPdfController.php`,
`resources/views/pdf/tender-fiche.blade.php`,
`resources/views/pdf/competitor-insight.blade.php`,
`resources/views/pdf/tenders-overview.blade.php`,
`app/Filament/Admin/Resources/Competitors/Actions/CompetitorActions.php`,
`tests/Feature/Tenders/TenderPortailFieldsTest.php`,
`tests/Feature/Pdf/TenderPdfExportsTest.php`.

**Modifiés** — `app/Models/Tender.php`, `routes/web.php`,
`app/Http/Controllers/LecteurDocumentController.php`,
`app/Filament/Admin/Resources/Tenders/Schemas/TenderForm.php`,
`app/Filament/Admin/Resources/Tenders/Schemas/TenderInfolist.php`,
`app/Filament/Admin/Resources/Tenders/Actions/TenderSubmissionActions.php`,
`app/Filament/Admin/Resources/Tenders/Pages/ViewTender.php`,
`app/Filament/Admin/Resources/Tenders/Pages/ListTenders.php`,
`app/Filament/Admin/Resources/Tenders/Tables/TendersTable.php`,
`app/Filament/Admin/Resources/Competitors/Pages/ViewCompetitor.php`,
`app/Filament/Admin/Resources/Competitors/Tables/CompetitorsTable.php`,
`tests/Feature/Documents/LecteurDeDocumentsTest.php`, `CHANGELOG.md`.

**Aucune suppression, aucun renommage.**
