# Module statistiques Laravel (`statistique_laravel`)

Ce document décrit le portage complet vers Laravel de l'ancien système de rapports PHP procédural situé sous `public/statistique/*` (piloté historiquement par `public/statistique/index.php`, un dossier par statistique). Il couvre l'architecture retenue, la liste des **44 statistiques migrées**, les mécanismes d'export (web, PDF, Excel), la configuration KoolReport nécessaire, et les corrections apportées par rapport au comportement d'origine.

L'ancien système reste présent tel quel dans `public/statistique/` (non supprimé) ; ce module Laravel est un portage indépendant, servi sous un préfixe de route distinct.

## 1. Pourquoi ce module

L'ancien système comptait ~44 dossiers de statistiques, chacun un script PHP procédural autonome (connexion PDO manuelle, SQL construit par concaténation de `$_GET`, rendu HTML par `echo` direct). Points bloquants qui ont motivé le portage :

- Filtres SQL construits par interpolation directe des paramètres de requête (`$_GET`) — faille d'injection SQL sur l'ensemble des rapports.
- Logique dupliquée à l'identique dans une dizaine de rapports (regroupement société/centrale, calcul TTC ligne à ligne, quantité facturée d'une zone de transport, classement "palmarès"...).
- Aucun export Excel natif ; le PDF (quand il existait) dépendait de Chrome/Node headless, absents de l'environnement de déploiement cible.
- Pas de authentification/autorisation centralisée (les scripts étaient accessibles directement sous `public/`).

Le module `statistique_laravel` répond à ces points : requêtes préparées partout, logique commune extraite dans `app/Reports/Support/`, export PDF 100% PHP (mpdf via `koolreport/printpdf`), export Excel natif (`koolreport/excel`), et accès protégé par les middlewares `auth:sanctum` + `interne`.

## 2. Architecture générale

```
routes/web.php
  └─ prefix /statistique_laravel  (middleware auth:sanctum + interne)
       ├─ GET  /                        → index()               page maître de filtres
       ├─ GET  /chantiers_par_client    → chantiers_par_client() AJAX (dropdown dépendant)
       ├─ GET  /{slug}                  → show()                 rendu web (DataTables)
       ├─ GET  /{slug}/pdf              → pdf()                  export PDF
       └─ GET  /{slug}/excel            → excel()                export Excel

app/Http/Controllers/statistique_laravel_controller.php
  └─ registry(): array   'slug' => FQCN de la classe de rapport   (44 entrées)

app/Reports/<slug>/
  ├─ <slug>.php              classe KoolReport, extends App\Reports\Support\BaseReport
  ├─ <slug>.view.php         vue web (widget koolreport\datagrid\DataTables)
  ├─ <slug>.pdf.view.php     vue export PDF (HTML statique pour mpdf)
  └─ <slug>.excel.view.php   vue export Excel (koolreport\excel\Table)

app/Reports/Support/        classes et traits partagés (voir §4)

resources/views/statistique/
  ├─ index.blade.php                page maître de filtres (formulaire)
  ├─ show.blade.php                 vue générique (boutons PDF/Excel + rendu du rapport)
  └─ partials/options_chantier.blade.php   partiel AJAX pour le dropdown chantier
```

### 2.1 Contrôleur générique à registre

Plutôt qu'une méthode + une route par statistique (ingérable à 44 rapports), un **seul contrôleur** expose 3 actions génériques (`show`, `pdf`, `excel`) paramétrées par le segment de route `{slug}`. Le mapping `slug => classe` est centralisé dans `registry()` :

```php
protected function registry(): array
{
    return [
        'facture_chiffre_affaire_societe' => \App\Reports\facture_chiffre_affaire_societe\facture_chiffre_affaire_societe::class,
        // ... 44 entrées au total
    ];
}
```

`reportClass($slug)` fait un `abort_if(..., 404)` si le slug est inconnu. Chaque action instancie la classe avec les paramètres de la requête (`new $reportClass($request->all())`), lance `run()`, puis :

- `show()` : rend la vue `statistique.show`, qui appelle `$report->render()` (widget web `DataTables`, interactif, tri/pagination/recherche côté client).
- `pdf()` : `$report->printPdfFast($slug . '.pdf')->pdf([...])->saveAs($filePath)`, puis `response()->download(...)->deleteFileAfterSend(true)`.
- `excel()` : `$report->exportToExcel($slug . '.excel')->saveAs($filePath)`, puis téléchargement direct.

Les routes explicites (`/`, `/chantiers_par_client`) sont déclarées **avant** la route catch-all `{slug}` pour éviter toute collision.

**Important** : ce registre est distinct de celui de `app/Http/Controllers/statistique_controller.php`, qui héberge 3 rapports créés avant la migration de masse (`chiffre_affaire_par_societe`, `test_amazing`, `bl_service_par_societe_centrale`) sous des slugs différents — aucune collision entre les deux contrôleurs.

### 2.2 Page maître de filtres

`resources/views/statistique/index.blade.php` reprend le formulaire de l'ancien `public/statistique/index.php` : plage de dates, et l'ensemble des listes déroulantes (société, région, centrale, client, chantier, formule, véhicule, chauffeur, service, ajout, fournisseur, transporteur, commercial, famille d'article, sous-famille client) alimentées par `statistique_laravel_controller::index()`.

Le choix de la statistique se fait via des boutons radio, groupés par thème (Facturation / BL / Achat), à l'identique du menu legacy. Un objet JS `criteriaMap` associe à chaque slug la liste des champs de filtre pertinents, pour n'afficher/activer que les critères utiles à la statistique sélectionnée. La soumission ouvre le rapport dans un nouvel onglet : `window.open(statistiqueBaseUrl + slug + '?' + formulaire, '_blank')`.

Le dropdown chantier est dépendant du client sélectionné (rechargé en AJAX via `chantiers_par_client`, qui renvoie le partiel `options_chantier.blade.php`).

**Simplification assumée** : les popups de recherche "avancée" pour client/chantier/formule (`public/statistique/recherche_generale/*` dans le legacy) n'ont pas été portées ; remplacées par de simples listes déroulantes standards.

## 3. Sécurité et accès

Toutes les routes du module sont protégées par le groupe de middleware `['auth:sanctum', 'interne']` — cohérent avec le reste des routes internes de l'application. Contrairement au legacy (scripts PHP accessibles directement sous `public/statistique/`), il n'y a pas d'accès anonyme possible.

Tous les filtres passent par des requêtes préparées PDO (bindings nommés `:xxx`), via `LaravelDataSource::query($sql, $sqlParams)` — plus aucune valeur de filtre n'est concaténée directement dans le SQL (voir §6, faille corrigée).

## 4. Classes et traits partagés (`app/Reports/Support/`)

| Classe / Trait | Rôle |
|---|---|
| `BaseReport` | Classe abstraite dont héritent toutes les statistiques. Câble les traits `Friendship` (KoolReport ↔ Laravel), `amazing\Theme`, `ExcelExportable`, `FastPdfExportable`, `FilterableReport`, et déclare la datasource Eloquent par défaut (`elo`) dans `settings()`. |
| `FilterableReport` | Trait fournissant `buildFilters(array $filters)` (retourne `[sql, params]` avec bindings nommés `:f_xxx`, ignore les filtres dont la valeur vaut `skipValue`, `'0'` par défaut), `dateRangeFilter($colonne, $debut, $fin)`, et `societeOuCentraleFilter($societeId, $centraleId, $colonneCentrale)` (motif "si centrale renseignée on l'utilise, sinon si société renseignée sous-requête sur `t_centrale`, sinon pas de filtre", répété dans ~15 rapports BL). |
| `PdfGroupedTable` | `render($rows, $groupBy, $columns, $sumColumns, $formatters = [])` — regroupe récursivement un tableau de lignes plates selon N colonnes et génère des blocs `<table>` HTML imbriqués avec sous-totaux par niveau, pour les vues `.pdf.view.php` (mpdf n'exécute pas le JS du widget web, il faut donc un rendu HTML statique équivalent). |
| `ExcelGroupedTable` | `rowGroup($groupBy, $sumColumns, $labels = [])` — construit la configuration `rowGroup` du widget `\koolreport\excel\Table` (regroupement + sommes calculés nativement côté Excel). |
| `FastPdfHandler` / `FastPdfExportable` | Variante de `koolreport\printpdf\Handler` qui désactive `autoScriptToLang`/`autoLangToFont` sur l'instance mpdf (options codées en dur dans le vendor, dupliquées ici volontairement). Gain mesuré : ~9% en moyenne sur plusieurs runs — la majeure partie du temps de génération PDF vient de la mise en page mpdf elle-même sur des tableaux de plusieurs milliers de lignes, pas de ces deux options. |
| `ZoneFactureQuantite` | `resolveZoneQuantiteEtMontant($factureNumero, $blCode)` — reproduit le parsing legacy de `t_ligne_facture.ligne_facture_liste_bl` (format `xxx_xxx_qte`) pour retrouver la quantité/prix facturé d'une zone de transport sur un BL, avec repli sur `t_ligne_bl` (type 4) si absent. |
| `LigneBlAggregation` | `resolveLignesBlTotalTtc($blCode)` — parcourt les lignes d'un BL (`t_ligne_bl`), applique le taux de TVA correct selon `ligne_bl_type_article` (1=service, 2=ajout, 3=adjuvant/produit, 4=zone, 5=granulat/produit, défaut=formule), retourne `[qte, codeProduit, libelleProduit, totalTtc]`. |
| `PalmaresReport` | `buildPalmaresData(...)` — calcul générique de classement (position, pourcentage, cumul) à partir des tables `t_palmares_*` pré-agrégées. |

## 5. Liste des statistiques migrées (44)

### Facturation (19)

`facture_chiffre_affaire_societe`, `facture_chiffre_affaire_societe_centrale`, `facture_chiffre_affaire_client`, `facture_chiffre_affaire_chantier`, `facture_chiffre_affaire_sous_famille_client`, `facture_chiffre_affaire_client_par_article`, `facture_chiffre_affaire_client_par_famille_par_article`, `facture_chiffre_affaire_client_par_centrale_par_famille_par_article`, `facture_chiffre_affaire_vehicule`, `facture_chiffre_affaire_zone_transporteur_vehicule`, `facture_chiffre_affaire_societe_centrale_formule`, `facture_chiffre_affaire_societe_centrale_formule_normee`, `palmares_client`, `palmares_client_n_n1`, `palmares_formule`, `facture_chiffre_affaire_cout`, `facture_chiffre_affaire_client_eligible_rep`, `facture_chiffre_affaire_code_comptable_type_famille_article`, `facture_chiffre_affaire_assurance_credit`.

### BL (15)

`client_chantier_bl`, `client_chantier_bl_formule`, `bl_annule`, `vehicule_bl`, `chauffeur_bl`, `transporteur_vehicule_centrale_bl`, `consommation`, `consommation_bl`, `consommation_avec_cout`, `centraliste_bl_formule`, `type_article_article_client_chantier_bl`, `bl_societe_centrale_formule_normee`, `palmares_formule_m3`, `bl_ajout`, `bl_service`.

### Achat (1)

`entree_matiere`.

### Rapports orphelins (présents sous `public/statistique/` mais absents du menu `index.php` legacy) (9)

`facture_chiffre_affaire_centrale_par_client_par_famille_par_article`, `facture_chiffre_affaire_formule`, `facture_chiffre_affaire_vehicule_a_garder`, `facture_chiffre_affaire_vehicule_anc`, `facture_client_formule`, `client_formule`, `journal_comptable`, `journal_vente`, `tarif_edition`.

### Contenu de `public/statistique/` explicitement exclu du portage

| Dossier | Raison |
|---|---|
| `examples/` | Exclusion explicite demandée. |
| `koolreport/` | Copie de la bibliothèque KoolReport à titre de référence documentaire, pas une statistique. |
| `recherche_generale/` | Endpoints AJAX de recherche utilisés par les popups avancées du formulaire legacy, pas une page de statistique. |
| `geo_chart/` | Démo générique KoolReport avec données World Bank fictives, sans lien avec les données métier de l'application. |

## 6. Corrections apportées par rapport au legacy

Conformément à la consigne de corriger les anomalies rencontrées (pas seulement reproduire le comportement existant à l'identique), les problèmes suivants ont été identifiés et corrigés pendant la migration :

- **Injection SQL** : tous les rapports legacy interpolaient les valeurs de `$_GET` directement dans les requêtes SQL. Dans le portage, tous les filtres passent par des bindings PDO nommés (`FilterableReport::buildFilters`).
- **Filtre chantier/société lu mais jamais appliqué** : plusieurs rapports (`bl_service`, `facture_chiffre_affaire_vehicule`, `bl_ajout`, etc.) récupéraient un filtre `chantier` dans la requête HTTP mais utilisaient par erreur la variable `$client` dans la clause `WHERE` — corrigé pour utiliser la bonne variable/colonne.
- **`facture_chiffre_affaire_chantier` groupait par client** : le rapport était une copie de `facture_chiffre_affaire_client` jamais adaptée au regroupement par chantier — corrigé.
- **Signe des avoirs manquant** dans `facture_chiffre_affaire_formule` : incohérent avec les rapports similaires qui inversent le signe des montants sur avoir — ajouté.
- **Mauvaise colonne de famille d'article** : dans `type_article_article_client_chantier_bl`, la vérification de famille utilisait systématiquement `famille_service_id`, quel que soit le type réel de la ligne (service/ajout/adjuvant/zone/formule n'ont pas tous cette colonne) — corrigé en résolvant la colonne de famille appropriée par type d'article.
- **Caractère littéral parasite** dans une comparaison de date SQL de `bl_societe_centrale_formule_normee` — supprimé.
- **Filtre de date jamais appliqué** dans `journal_comptable` et `journal_vente` : `date_debut`/`date_fin` étaient lus depuis la requête mais jamais utilisés dans le `WHERE` — filtre ajouté sur `facture_date`.
- **Erreur PDO "mixed named and positional parameters"** dans `facture_chiffre_affaire_client_eligible_rep` : des `?` positionnels coexistaient avec les bindings nommés de `buildFilters()` — converti en bindings nommés partout.
- **Mauvaise table jointe** dans `facture_chiffre_affaire_societe_centrale_formule_normee` : la colonne `formule_normalisee` se trouve sur `t_formule_detail`, pas `t_formule` — corrigé après une `PDOException: Column not found`.
- **`colspan="0"` invalide** dans `PdfGroupedTable::render()` quand `$sumColumns` est vide (cas rencontré sur `tarif_edition`, qui n'a pas de colonnes à sommer) — la cellule de totaux est désormais omise plutôt que générée avec un `colspan` nul.

## 7. Export PDF et Excel

### PDF

Basé sur `koolreport/printpdf` (moteur mpdf, 100% PHP — pas de dépendance à Chrome ou Node, absents de l'environnement de déploiement). Chaque rapport fournit une vue `<slug>.pdf.view.php` qui reconstruit le tableau en HTML statique via `PdfGroupedTable::render()` (le widget web `DataTables`, basé sur du JS, n'est pas exécutable par mpdf).

Génération lancée via `FastPdfExportable::printPdfFast($slug . '.pdf')`, une variante maison de `koolreport\printpdf\Handler` qui désactive `autoScriptToLang`/`autoLangToFont` (gain mesuré ~9%, cf. §4). Le temps de génération PDF reste néanmoins nettement supérieur au rendu web (mpdf doit calculer la pagination de l'intégralité des lignes, contrairement au widget web qui n'affiche/traite que la page courante côté client) — de l'ordre de 20 secondes pour ~2200 lignes.

### Excel

Basé sur `koolreport/excel`, widget `\koolreport\excel\Table`. Chaque rapport fournit une vue `<slug>.excel.view.php` qui définit les colonnes et, le cas échéant, un regroupement (`ExcelGroupedTable::rowGroup(...)`) avec sommes calculées nativement par Excel (pas de recalcul PHP, contrairement au PDF).

## 8. Dépendances et configuration KoolReport

Packages premium installés depuis le dépôt Composer privé `https://repo.koolreport.com` :

- `koolreport/amazing` — thème et widgets avancés.
- `koolreport/datagrid` — widget `DataTables` (tri/recherche/pagination/regroupement côté client) utilisé sur toutes les vues web.
- `koolreport/printpdf` — export PDF (mpdf).
- `koolreport/excel` — export Excel.

Configuration dans `composer.json` :

```json
"repositories": [
    { "type": "composer", "url": "https://repo.koolreport.com" }
]
```

Authentification via `auth.json` à la racine du projet (fichier **gitignored**, non commité) :

```json
{
    "http-basic": {
        "repo.koolreport.com": {
            "username": "<email associé à la licence KoolReport>",
            "password": "<token de licence KoolReport>"
        }
    }
}
```

## 9. Ajouter une nouvelle statistique

1. Créer `app/Reports/<slug>/<slug>.php`, classe `extends App\Reports\Support\BaseReport`, en s'appuyant sur `FilterableReport` pour les filtres.
2. Créer `<slug>.view.php` (widget `koolreport\datagrid\DataTables`), `<slug>.pdf.view.php` (via `PdfGroupedTable::render()`), `<slug>.excel.view.php` (via `\koolreport\excel\Table` + `ExcelGroupedTable::rowGroup()`).
3. Ajouter l'entrée `'slug' => \App\Reports\<slug>\<slug>::class` dans `statistique_laravel_controller::registry()`.
4. Si la statistique doit apparaître dans le formulaire maître : ajouter le bouton radio correspondant dans `resources/views/statistique/index.blade.php` et son entrée dans `criteriaMap` (JS) pour n'afficher que les filtres pertinents.
5. Vérifier via `php artisan tinker` : instancier la classe, `run()`, contrôler le nombre de lignes, puis tester `render()`, `printPdfFast(...)->pdf([...])->saveAs(...)` et `exportToExcel(...)->saveAs(...)` sur un fichier temporaire (à nettoyer ensuite).

## 10. Limites connues

- Les popups de recherche avancée client/chantier/formule du formulaire legacy n'ont pas été portées (remplacées par de simples listes déroulantes).
- L'export PDF `Chrome/Node headless` du rapport `bl_service` legacy (`koolreport/export`) n'a pas été repris : ni Chrome ni Node ne sont installés sur l'environnement de développement/déploiement cible. Tous les exports PDF utilisent désormais `koolreport/printpdf` (mpdf).
- Le temps de génération PDF reste sensiblement plus long que le rendu web sur les rapports à fort volume de lignes (nature même de la pagination mpdf, pas un défaut de configuration corrigible facilement).
