# Plan d'action SSTM — Modernisation, Multi-clients, Redesign

> Document de travail. On coche au fur et à mesure. Une phase = livrable testable.
> État des lieux fait le 13/07/2026.

## 📊 État des lieux

| Indicateur | Valeur | Constat |
|---|---|---|
| Code PHP | ~360 000 lignes | Gros projet, réécriture totale irréaliste → refonte progressive |
| Contrôleurs / Modèles / Vues | 119 / 174 / 459 | Beaucoup de copier-coller entre vues (modales, datatables) |
| Fichiers backup `x*/xx*/yy*` | 59 | Code mort à purger (c'est le rôle de git) |
| Tables MySQL | 128 | Mélange MyISAM/InnoDB, latin1/utf8/utf8mb4, pas d'index secondaires, pas de FK |
| Git | ❌ absent | Risque majeur : aucun historique, backup = copies de fichiers |
| Sécurité | ⚠️ | WHERE concaténés (injection SQL), credentials en dur dans `Model.php`, cookie de session = `id_UserAgent` (prévisible) |
| Multi-clients | 1 hébergement + 1 base **par client** | Chaque mise à jour = N uploads → c'est le problème n°1 à régler |
| Branding | `parametre_station` (logo, couleurs, entêtes) | ✅ Déjà par base, réutilisable tel quel |
| Droits | `users_space` + `user_profil` | ✅ Système d'accès par module déjà en place |

## 🏗️ Décision d'architecture multi-clients (à valider ensemble)

Deux options :

**Option A — Une seule base pour tous (ce que tu as demandé)** : ajouter `id_station` dans les 128 tables et modifier **des milliers de requêtes** concaténées à la main. Un seul `WHERE` oublié = le client A voit l'argent du client B. Risque énorme, des mois de travail.

**Option B — Une seule application + une base par client + une base "master" (recommandée)** :
- Un seul code hébergé une seule fois. Chaque client accède par sous-domaine : `client1.tonapp.ma`, `client2.tonapp.ma`.
- Une petite base `sstm_master` : table `tenant` (code, nom, sous-domaine, nom_base, logo, état, date_expiration...).
- `Model.php` lit le sous-domaine → se connecte à la base du client. **C'est le seul fichier du framework à modifier.**
- Mise à jour = 1 upload + 1 commande qui applique les migrations SQL à toutes les bases en boucle.
- Isolation des données garantie par construction (impossible de mélanger les clients), et ton code actuel fonctionne sans toucher aux requêtes.

→ L'option B te donne exactement le bénéfice recherché (une seule app, une seule mise à jour, chacun son logo) pour 5 % de l'effort et 1 % du risque. On peut viser l'option A plus tard, module par module, une fois le code assaini.

---

## Phase 0 — Fondations ✅ (faite le 13/07/2026)
- [x] `git init` + `.gitignore` + premier commit (tout le projet) — commit `498f085`.
- [x] Purge de 64 fichiers backup `x*/xx*/yy*` + dossier `xapp` (récupérables dans le commit initial) — commit `0b736f6`. App testée OK après purge.
- [x] `config/config.php` (hors git, `config.exemple.php` versionné) branché dans `Model.php` — commit `b6b5702`. Connexion PDO unique réutilisée (prépare le multi-tenant).
- [x] `database/schema.sql` (structure de référence) + `database/migrations/` (001 = table avoir) + `database/backups/` (dumps complets, non versionnés) — commit `142c6e7`.
- [x] Dépôt distant privé GitHub : **https://github.com/bebenhayoun/sstm** (compte `bebenhayoun`, gh CLI authentifié). Push initial fait le 14/07/2026. **PHASE 0 TERMINÉE ✅**

## Phase 1 — Multi-clients : une seule application (le gros gain)
- [x] Base `sstm_master` (`database/master.sql`) : tables `tenant` + `migration_log`.
- [x] Résolution du tenant par sous-domaine dans `Model::resolve_db()` (config `multi_tenant` + `base_domain`) ; domaine nu/CLI → base par défaut ; tenant inconnu/suspendu → 404.
- [x] `Model.php` : connexion dynamique selon le tenant (fait le 14/07).
- [x] Création d'un client : `php scripts/tenant_create.php <code> "<Nom>"` (base depuis `schema.sql`, auto-initialisation admin à la 1ère visite). Écran super-admin web = plus tard (Phase 1.5, optionnel).
- [x] Runner de migrations : `php scripts/migrate.php` (toutes les bases), `migrate.php <code>` (un client), `migrate.php --liste` (état).
- [x] Import d'une base existante : `php scripts/tenant_import.php <code> "<Nom>" <dump.sql> [--migrations-a-jour]`.
- [x] Testé en local : `demo.localhost` (base vierge → formulaire init), `sstm1.localhost` (import de la base station → login OK), `localhost` (base par défaut inchangée), `inconnu.localhost` (404).
- [ ] **Mise en production** (nécessite les accès OVH) : sous-domaine wildcard `*.domaine` → dossier de l'app, création de `sstm_master` sur le serveur, import des bases clients réelles, `config.php` prod avec `multi_tenant=true`. Checklist de déploiement à écrire à ce moment-là.

## Phase 2 — Sécurité (avant d'ouvrir à plus de clients)
- [ ] Requêtes préparées : helpers `find_safe()`/params liés dans `Model.php`, migration progressive des requêtes qui touchent `$_POST`/`$_GET` (38 occurrences directes dans les modèles → prioritaires).
- [ ] Cookie "se souvenir de moi" : token aléatoire stocké en base (remplacer `id_UserAgent`).
- [ ] Échappement des sorties dans les vues sensibles (XSS), en commençant par les champs saisis par les clients.
- [ ] HTTPS obligatoire en production + `session.cookie_httponly/secure`.

## Phase 3 — Base de données (via migrations versionnées)
- [ ] Uniformiser : tout en **InnoDB + utf8mb4** (migration générée automatiquement).
- [ ] Index sur toutes les colonnes de jointure/filtre : `id_client`, `id_compte`, `(source, id_source, zone)`, dates... → gains de vitesse immédiats sur `carburant_compte_bon` (68k lignes) et `client_bon` (41k).
- [ ] Colonnes d'audit partout : `id_creation`, `date_creation`, `id_modification`, `date_modification`.
- [ ] Nettoyage : tables mortes (`client_errachidia`, `*_ancien`...), doublons (`stocks_produit_ancien`/`stocks_produits`).
- [ ] (Optionnel, plus tard) clés étrangères sur les nouveaux modules.

## Phase 3.5 — Audit DataTables, table par table (préparation Phase 4/5, démarré le 17/07/2026)
> Règle : une table à la fois. Pour chaque table → (1) vérifier/corriger la recherche serveur (le `data()`/`datatable()` du modèle doit filtrer sur les colonnes réellement affichées dans la vue), (2) sortir le HTML actuellement généré dans le modèle (`$var .= '<div class="btn-group">...'`) vers une vue/partiel dédié, (3) optimiser JS (config DataTable dupliquée), HTML du tableau, requête SQL. On héberge/valide avant de passer à la suivante.

### Module Vente 🔄 en cours
- [x] `Reglements.php` / `ClientReglement::data()` — recherche vérifiée OK (nom client + réf. facture, testé via curl) ; HTML sorti du modèle vers le contrôleur (`ReglementsClientController::datatable()`) via le nouveau composant `dt_btn_group()`/`dt_btn_lien()`/`dt_btn_dropdown_date()` (`vendor/function.php`) ; JS uniformisé avec `init_datatable()` (`public/js/ajax.js`) ; suppression d'un badge `$statut` mort (calculé, jamais utilisé)
- [x] `Reglement.php` (factures/paiements du règlement) / `ClientReglement::factures_reglement()`, `paiement_reglement()` — recherche = désactivée intentionnellement (`"searching":false`), rien à corriger ; HTML sorti vers `ReglementClientController::datatable()` (`dt_btn`/`dt_btn_dropdown_confirm`) ; JS uniformisé avec `init_datatable()` ; `dt_btn()` étendu avec un paramètre `$attrs` (attributs HTML libres, ex: `data-toggle="modal"`) pour les boutons qui ouvrent une modale
- [x] `Impayees.php` / `ClientImpayes` — recherche vérifiée OK (client + référence) ; HTML sorti vers `ImpayeesClientController::datatable()` ; JS uniformisé avec `init_datatable()`
- [x] `Clients.php` / `Client::data()`, `ClientGroupe::data()` — recherche OK (nom) ; HTML sorti vers `ClientsController::data()`/`groupes()` ; JS uniformisé
- [x] `Factures.php`, `Facture.php` / `ClientFacture::data()`, `data_description()` — recherche OK (référence+client) ; HTML sorti vers `FacturesClientController`/`FactureClientController` ; JS uniformisé (badges `statut_facture()` laissés tels quels, logique métier isolée)
- [x] `Encaissements.php` / `Client::encaissement()` — **bug corrigé** : la recherche n'était pas du tout câblée (case cochée mais sans effet), ajoutée (client+référence). Pas de HTML dans le modèle. ⚠️ **Perf** : requête très lente (~105s même sur 5 jours, UNION de 6 sous-requêtes sans index sur source/id_source/zone) — pré-existant, signalé en Phase 3 (index), pas corrigé ici (ALTER TABLE en prod à faire hors heures d'ouverture)
- [x] `Cartes.php` / `ClientMatricule::cartes()` — **bug corrigé** : condition de recherche non protégée par `if($recherche!='')` (risque d'exclure des lignes à NULL même sans recherche) ; HTML sorti vers `CarteClientController::datatable()` ; JS uniformisé
- [x] `Avoirs.php` / `ClientAvance::data()` — **bug corrigé** : recherche absente (case cochée mais sans effet), ajoutée (client+référence) ; HTML sorti vers `AvoirClientController::datatable()` ; JS uniformisé. Note : `ClientAvoir::data()` (utilisée par `bons/Avoirs.php`, pas cette vue) traitée séparément dans le module Bons. **Fix régression** : `dt_btn_dropdown_confirm()` écrasait un `title` dynamique utilisé pour le routage JS (suppression de paiement dans `Reglement.php`) — paramètre `$title` ajouté au composant, corrigé et redéployé immédiatement
- [x] `Livre.php` / `Client::livre()` — **bug corrigé** : recherche absente, ajoutée (client+numéro) ; pas de HTML dans le modèle ; JS uniformisé ; perf OK (~0.4s)

### Module Bons
- [x] `Etats.php`, `ClientEtat.php` (+ `portion/Header.php`) / `ClientEtat::data()`, `ClientBon::data_etat()`, `ClientRemplacement::remplacement_etat()`, `ClientEtat::etat_produit()` — recherche OK sur `data()` (client+matricule) ; recherche ajoutée sur `data_etat()` (référence/intervenant/matricule, absente avant) et `etat_produit()` (produit, absente avant) ; HTML sorti des 4 méthodes vers `ClientEtatController::datatable()` ; JS des 3 tables uniformisé avec `init_datatable()`. Vigilance appliquée sur les `title` dynamiques utilisés pour le routage JS (`.supprimer`/`.modif` lisent `$(this).attr('title')`)
- [x] `ClientBon.php` / `ClientBon::data()` — recherche OK (intervenant/identifiant/client/matricule) ; HTML sorti vers `ClientBonController::datatable()` ; JS uniformisé
- [x] `ClientAvoir.php` / `ClientAvoir::data()` — **bug corrigé** : recherche absente (comme `ClientAvance::data()`), ajoutée (client+référence) ; HTML sorti vers `ClientAvoirController::datatable()` (attention aux `title` dynamiques, même pattern que `ClientAvance`) ; JS uniformisé
- [x] `Archive.php` / `ClientBon::data()` — **régression détectée et corrigée avant déploiement** : ce contrôleur (`ClientArchiveController`) réutilise `ClientBon::data()` déjà refactoré pour `ClientBon.php` mais ne construisait pas le HTML → corrigé avec la même logique. JS uniformisé
- [x] ~~`Avoirs.php`~~ — fichier mort, non routé par aucun contrôleur (vérifié), ignoré
- [x] `BonPassage.php` / `ClientBonPassage::data()` — recherche déjà OK (client/matricule/numéro) ; HTML sorti vers `BonPassageController::datatable()` ; JS uniformisé

### Module Achat
- [x] `Reglements.php`, `Reglement.php` / `FournisseurReglement` — miroir quasi exact de `ClientReglement` (vente), même pattern appliqué (recherche déjà OK, HTML sorti vers les contrôleurs, JS uniformisé)
- [x] `Avoirs.php` / `FournisseurAvance::data()` — **bug corrigé** : recherche absente (même bug que `ClientAvance`), ajoutée (fournisseur+référence) ; HTML sorti vers `AvoirFournisseurController::datatable()` ; JS uniformisé
- [x] `Factures.php` / `FournisseurFacture::data()` — recherche OK (référence+fournisseur) ; HTML sorti ; JS uniformisé
- [x] `Impayees.php` / `FournisseurImpayes::data()` — recherche OK ; HTML sorti (miroir `ClientImpayes`) ; JS uniformisé
- [x] `Fournisseurs.php` / `Fournisseur::data()` — recherche OK (nom) ; HTML **laissé dans le modèle** : utilise `Model::btn_group()`, un helper déjà partagé par 16 modèles différents (achat, carte, coffre, compte, compte_ps, employé, ont...) — le sortir proprement nécessite de retravailler les 16 appelants en une passe dédiée, noté en Phase 5 (datatable générique) plutôt que fait à la volée ici. JS uniformisé
- [x] `Depense.php` / `Depense::data()` — **bug corrigé** : recherche calculée mais jamais utilisée dans le WHERE (aucun effet), ajoutée (description/catégorie/fournisseur) ; HTML sorti (bouton `id=""` déjà mort/sans handler JS, préservé tel quel) ; JS uniformisé

### Module Coffre
- [x] `Remplacement.php`, `Remplacements.php` / `CoffreRemplacement::data()`, `CoffreRemplacementPiece::remplacement_piece()` — recherche OK (propriétaire/matricule/référence) ; HTML sorti vers `PieceRemplacementController::datatable()` (attention aux `title` dynamiques préservés) ; JS uniformisé
- [x] `Piece.php` / `Piece::data()` — recherche OK ; HTML sorti vers `PieceController::datatable()` ; JS uniformisé
- [x] `Caisse.php` / `CaissePsFlux::caisse_between()` — recherche désactivée intentionnellement, rien à corriger ; HTML sorti vers `CaisseController::datatable()` (attention : ce tableau utilise un attribut non-standard `titlle` (coquille dans le code d'origine, pas `title`) pour le routage JS des actions modifier/supprimer — préservé tel quel) ; JS uniformisé
- [x] `Envois.php`, `EnvoiBanque.php` / `EnvoiBanque::data()`, `piece_envoi()`, `remise_envoi()`, `attachement()`, `nouvelle()` — recherche déjà correcte partout ; HTML sorti des 5 méthodes vers `PieceEnvoiController` ; JS des 5 tables (piece_data, piece_envoi, remise_envoi, associe, libre, nouvelle) uniformisé. Note : `coffre/Envoi.php` identifié comme fichier mort (non routé, remplacé par `EnvoiBanque.php`) — édité par erreur avant de le découvrir, laissé tel quel (inoffensif car inaccessible)
- [x] `Archive.php` / `Piece::archive()` — recherche OK ; HTML sorti vers `ArchivePieceController::datatable()` ; JS uniformisé
- [x] `Vignette.php` / `OntBon::data()` — **bug corrigé** : comparaison `$recherche!=" "` (espace) au lieu de `!=''` (chaîne vide) ; pas de HTML (pas de colonne action dans cette table) ; JS uniformisé

### Module Banque
- [x] `CompteBancaire.php`, `OperationEnAttente.php`, `PieceImpayee.php` / `CompteBancaireFlux::data()`, `en_attente()`, `RemiseBanque::data()`, `EnvoiRemisePiece::data_impaye()` — **bug corrigé** : recherche absente sur `RemiseBanque::data()` (case visible mais sans effet), ajoutée (propriétaire+référence). Recherche déjà correcte partout ailleurs. HTML sorti des 4 méthodes vers les contrôleurs `CompteBancaireController`/`OperationEnAttenteController`/`PieceImpayeeController`. JS des 5 tables uniformisé

### Module Compte / Compte_ps / Point de vente
- [x] `compte/Comptes.php` / `Compte::data()` — recherche désactivée intentionnellement, rien à corriger ; HTML sorti vers `ComptesController::comptes()` ; JS uniformisé (`Compte::comptes()`, méthode du modèle, identifiée comme morte — aucun appelant — laissée telle quelle)
- [x] `compte/CompteZone.php` / `ClientCartePrepayeTicket::attachement()` — **bug corrigé** : recherche absente (case visible mais sans effet), ajoutée (numéro/client/matricule) ; HTML sorti vers `CompteZoneController::data_prepaye()` ; JS des 2 tables (data_bon, data_prepaye) uniformisé
- [x] `compte_ps/ComptesPs.php` (+ `datatable/Caisse.php`, `Depense.php`, `Stock.php`, `VenteOfficielle.php`) / `ComptePs::data()`, `StocksMouvementOfficiel::vente_between()`, `CaissePsFlux::caisse_zone_between()`, `Depense::depense_zone_between()`, `StocksProduitsQantite::stock()` — recherche désactivée intentionnellement partout ; HTML sorti des 5 méthodes vers `ComptesPsController`/`PointdeventeController` ; JS des 5 tables uniformisé
- [x] `point_vente/Compte.php`, `point_vente/Pv.php` / `Pv::data()`, `Atelier::data()`, `Colocataire::data()`, `ComptePv::paiements()`, `ComptePvTicket::data()`, `ComptePvTicketDescription::data()` — **bug corrigé** : recherche absente sur `Pv`, `Atelier`, `Colocataire` (case visible mais sans effet), ajoutée (nom) ; HTML sorti des 6 méthodes vers `PvController`/`ComptePvController` ; attributs de routage dynamiques préservés (`methode` sur `ComptePv`, `form`/`formulaire` sur `ComptePvTicketDescription`) ; JS des 5 tables uniformisé. Testé en local (php -l, page, 3 endpoints AJAX, recherche avec/sans résultat). Commit `700fab2`. **Déploiement en attente** : SSH vers `cluster128.hosting.ovh.net` timeout intermittent (probable rate-limit OVH après le grand nombre de connexions de cette session) — à réessayer, regrouper les déploiements restants en plus gros lots

### Module Comptabilité
- [ ] `Declarations.php`, `Factures.php`, `Reglements.php`

### Module Stocks
- [x] `Produits.php`, `Produit.php`, `Mouvements.php`, `BonCommandes.php`, `BonCommande.php`, `Transferts.php`, `Transfert.php`, `Comparaison.php`, `Alimentations.php`, `Alimentation.php` — **bugs corriges** : `BonCommande::data()`/`comparaison()` (recherche absente) ; `StocksAlimentation::alimentation_between()` (recherche conditionnee a tort au filtre fournisseur, + alias SQL `facture` invalide en WHERE une fois la recherche activee — corrige en `ali.note`). HTML sorti de tous les modeles (`StocksProduits`, `StocksMouvement`, `StocksTransfert`, `StocksAlimentation`, `BonCommande`, `StocksMouvementOfficiel`) vers les controleurs correspondants ; attributs de routage dynamiques preserves (`entete`, `nom`, `title="alimentation"/"transfert"`). JS des 10 tables uniformise. **Bug pre-existant repere, volontairement NON corrige (hors scope, a valider avec l'utilisateur)** : dans `stocks/Transfert.php` (`#flux`), le bouton "Info" porte la classe `supprimer` → il supprime la ligne au clic sans confirmation (pas de modale). Commit `a2079df`.

### Module Employé
- [x] `Employes.php`, `Salaires.php`, `Avances.php` — **bugs corriges** : compteur DataTables `Employes` (calcule sur la liste non filtree) ; fonctionnalite **Salaires totalement cassee** remise en etat (mauvaise classe instanciee dans `modal()` → erreur fatale systematique, variable `compact()` erronee, insertion declenchee des l'ouverture du formulaire vide, noms de champs jamais alignes avec le controleur, table `collaborateur_salaire` jamais creee en base). Migration `005_collaborateur_salaire.sql` ajoutee (SQL a executer par l'utilisateur). HTML sorti des modeles vers les controleurs ; JS des 3 tables uniformise. Commit `5e8b68c` + `4c41c8f` (schema.sql).
- [ ] `Salaire.php` (vue singuliere) : **code mort confirme**, non routee par aucun controleur, cible des endpoints `/Salaires/datatable|header|description|cloture|supprimer|annulation` qui n'existent pas — laissee telle quelle sauf si l'utilisateur souhaite reprendre cette fonctionnalite un jour.

### Modules restant (passe rapide, moins utilisés)
- [x] `restaurant/*`, `service/*`, `carte_fournisseur/*`, `cmi/*`, `fournisseur/*`, `ont/Administrations.php`, `parametre/*` (dont `Historique.php`), `statistique/Numerique.php`, `home/Home.php`, `home/Statistique.php`, `user/Users.php` — audit complet (agent) puis corrections des bugs reels : voir commit `e968177`. HTML-hors-modele et `init_datatable()` appliqués uniquement aux tables dont un bug a été corrigé (recherche/pagination/compteur) ; le reste de cette liste (services annexes, peu utilisés) a été audité mais pas systématiquement réécrit — cohérent avec le classement "passe rapide" déjà prévu.
- **Code mort / routes cassées repérées (non touchées, a decider avec l'utilisateur)** :
  - `ont/VignettesController.php` et `parametre/ParaProduitsController.php` sont des fichiers **vides (0 octet)** alors que leurs routes (`Vignettes`, `ParaProduits`) sont enregistrées dans `vendor/function.php` → erreur fatale si quelqu'un clique sur ces liens. `ParaProduits` orpheline aussi 3 vues (`datatable/Categorie.php`, `Service.php`, `Sous_categorie.php`) et `ParaProduits.php`.
  - Vues orphelines (aucun controleur ne les route) : `app/views/fournisseur/Fournisseurs.php`/`Facturation.php` (doublons de `achat/Fournisseurs.php`), `app/views/carte_fournisseur/Recharges.php`, `app/views/cmi/Recharges.php`, `app/views/statistique/yGraphique.php`, `app/views/home/Home_balance.php`, `app/views/home/Homex.php`.

## Phase 4 — Redesign professionnel
- [ ] Choisir le thème : rester AdminLTE modernisé **ou** passer à un thème pro Bootstrap 5 (ex. Tabler, gratuit et très propre) — à décider ensemble sur maquette.
- [ ] Créer des **composants réutilisables** (le cœur du redesign) : `composant/modale.php`, `composant/datatable.php`, `composant/champ.php`, `composant/filtre.php` → on ne copie plus 100 lignes par vue.
- [ ] Refonte du layout global : login, top bar, menu, dashboard (avec le logo du tenant partout, favicon, couleurs par client depuis `parametre_station`).
- [ ] Refonte **module par module** (ordre selon usage clients) : Compte journalier → Ventes/Clients → Coffre → Achats → Stocks → le reste.
- [ ] Impressions/factures : gabarit unique paramétré par le branding du tenant.

## Phase 5 — Productivité (faciliter les tâches)
- [ ] Générateur de module : une commande crée contrôleur + modèle + vue datatable + modale CRUD standard.
- [ ] Datatable générique côté serveur (une classe au lieu du copier-coller de `data()` dans 50 modèles). Inclut la migration de `Model::btn_group()` (helper HTML partagé par 16 modèles : achat, carte, coffre, compte, compte_ps, employé, fournisseur, ont — repéré le 17/07/2026 pendant la Phase 3.5) vers le nouveau composant `dt_btn_*`.
- [ ] Environnement de test séparé (base `station_test`) + recette curl documentée dans `CLAUDE.md`.
- [ ] Déploiement en 1 commande (script : pull + migrate + vider caches).

---

## 🤝 Méthode de travail à deux (pour chaque tâche)
1. Tu me décris le besoin → je lis `CLAUDE.md` + `PLAN.md` + le code concerné.
2. Je propose l'approche (et je te signale si ça impacte le multi-tenant ou la base).
3. J'implémente **en suivant ton style de framework**, je teste en local (compte de test + curl/navigateur).
4. Je te livre : liste des fichiers modifiés + migration SQL numérotée + commit git.
5. On met à jour `PLAN.md` (cocher) et `CLAUDE.md` (si l'architecture a changé).

**Règles d'or** : jamais deux chantiers en même temps ; toute modification de base passe par une migration numérotée ; commit avant chaque expérimentation.

## 📅 Ordre conseillé
Phase 0 (1 séance) → Phase 1 (le vrai soulagement : plus de N hébergements) → Phase 2 en parallèle du redesign → Phase 4 module par module → Phases 3/5 en continu.

---

## 📓 Journal des actions (tenir à jour APRÈS CHAQUE tâche — règle demandée par Bahaa le 14/07/2026)

### 13/07/2026
- ✅ Fonctionnalité Avoir dans PieceRemplacement (v1 création directe, remplacée le jour même par l'attachement) — puis **concept revu : attachement d'avoirs clients existants** (`client_avoir.destination='PieceRemplacement'`), résolution du client du chèque via `Piece::client_piece()`. Commits `0ba6707`, `20cb020` (bouton "forcer tous les clients").
- ✅ Phase 0 quasi complète : git init (`498f085`), purge 64 backups + `xapp` (`0b736f6`), config centralisée `config/config.php` (`b6b5702`), `database/schema.sql` + migrations (`142c6e7`).

### 14/07/2026
- ✅ Attachement des **avances clients** (facturation) au remplacement — migration **003** (colonnes `destination`/`id_destination` sur `client_avance`), anti double-emploi dans ReglementClient/Clients/StatistiqueClient. Commit `aa8063c`.
- ⚠️ **Hébergement : erreur 500 sur la modale Avance** = migration 003 jamais exécutée sur la base OVH (aucun SQL n'a été fait en prod !). SQL à exécuter : `ALTER TABLE client_avance ADD COLUMN destination varchar(50) NULL DEFAULT NULL, ADD COLUMN id_destination int(11) NULL DEFAULT NULL;` — Bahaa a proposé de donner les **accès SSH OVH** pour que Claude déploie lui-même (en attente des identifiants).
- ✅ Fix ComptePv : changement Station↔colocataire possible tant que pas d'encaissement/bon/clôture (avant : bloqué dès la 1ère ligne) + fix bascule silencieuse vers Station. Commit `422395e`. Fichiers à héberger : `ComptePvController.php` + `modal/nouveau_ticket.php`.
- ✅ GitHub CLI installé + `gh auth login` fait (compte `bebenhayoun`) → dépôt privé créé et poussé : **https://github.com/bebenhayoun/sstm**. **Phase 0 terminée à 100 %.**

- ✅ **Phase 1 développée et testée en local** : base `sstm_master` (tenant + migration_log), `Model::resolve_db()` (sous-domaine → base client, 404 si inconnu/suspendu), scripts CLI `tenant_create.php` / `tenant_import.php` / `migrate.php`. Tests OK : demo.localhost (vierge), sstm1.localhost (import base station + login), localhost (défaut), inconnu.localhost (404). Le runner a appliqué 001→003 sur sstm1 automatiquement. Bases locales de test : `sstm_demo`, `sstm_sstm1`.

- ℹ️ **Décision hébergement (14/07)** : l'hébergement actuel est **mutualisé** (pas de wildcard, bases limitées) → le multi-tenant partira sur un **VPS OVH à acheter plus tard** (Starter/Value suffit). D'ici là : tout se teste en local. Le mutualisé actuel reste en service pour les clients pendant la transition.

### 17/07/2026
- ✅ **Gratuité sur clôture de règlement client** (`ReglementClient/index/{id}`) : quand la différence facture/paiement est négligeable (ex: 0.20, 1 Dhs), possibilité de clôturer sans créer d'avance ni de reste, via une case à cocher dans la modale de clôture. Nouvelle table `client_gratuite` (`source`/`id_source` réutilisable, comme `client_avance`/`client_reste`) — migration **004**. Annulation de clôture prise en charge (supprime la ligne gratuité, toujours annulable). Affichage "Gratuité" dans le header du règlement une fois clôturé.
- ✅ **Fix bug ClientEtat** : "erreur de chargement du header" après clic sur Clôturer (nécessitait un rafraîchissement). Cause : dans `bons/ClientEtat.php`, le `load_header()` était appelé en parallèle de l'appel AJAX `/ClientEtat/cloture` (bug `callback()` invoqué immédiatement au lieu d'être passé en référence, + appel synchrone non attendu dans l'autre gestionnaire `#cloturer`) → requêtes concurrentes sur la même session PHP pouvant faire échouer le chargement du header. Corrigé en repoussant `load_header()` dans le callback de succès de `load_portion()`.

- ✅ **Accès SSH OVH obtenu et déploiement fait sur `sstm`** (hébergement mutualisé, cluster128, `/home/statioj/sstm/`) : 16 fichiers PHP/vues déployés (comparaison hash local/serveur avant upload, sauvegarde des anciennes versions dans le scratchpad de session, `php -l` vérifié sur le serveur après upload — tout OK). Couvre : fix ComptePv (`422395e`), attachement avances/avoirs (`aa8063c`, `20cb020`, `0ba6707`), Impayées (`cb4e7bf`), gratuité + fix ClientEtat (`525ee9b`). **Volontairement exclu** : `app/models/Model.php` et tout le multi-tenant (`d2e284e`, `3d465f0`) — reste réservé au futur VPS, pas déployé sur le mutualisé.
- ⚠️ **SQL restant à jouer sur la base OVH (Bahaa s'en charge)** : migrations `002_suppression_coffre_remplacement_avoir.sql`, `003_client_avance_destination.sql`, `004_client_gratuite.sql` (vérifier lesquelles sont déjà appliquées avant de rejouer — 001 semble déjà en place vu qu'aucune erreur signalée dessus).

### 21/07/2026
- ✅ **Phase 4, module Vente traité en entier** (Clients/Cartes/Encaissements/Grand Livre) : recette complète (composants, navigation AJAX, plein écran, impression/export génériques, pièges CSS rencontrés) documentée dans **`SKILL_MODULE_VENTE.md`** (nouveau fichier, à la racine) — à lire avant de reproduire sur un autre module. Détail jour par jour dans `DESIGN.md`.
- ✅ **Fix perf critique Encaissements** (hors design) : requête `UNION ALL` à 8 branches sans index, exécutée jusqu'à 3× par appel → bloquait le serveur. Migration `007_index_encaissements_performance.sql` + correctif `app/models/client/Client.php::encaissement()` (commit `ab0b6f1`).
- ✅ **Totaux Grand Livre** : optimisation SQL (migration `008`, index sur `client_avance`/`client_facture`/`compte_bancaire_flux`/`coffre_envoi_remise_piece`) + affichage repensé (`.ssm-stat-row`, plus léger que les cartes `.ssm-kpi`) — seuls les chiffres se rechargent au changement de filtre, plus tout le bloc (commit `b1d381b`).
- ✅ **Fix plein écran** : les modales (impression/export...) étaient invisibles tant qu'on ne quittait pas le plein écran (z-index du panel bien trop élevé, passait au-dessus des modales Bootstrap) — ramené à une valeur raisonnable (commit `48f2e57`).
- ✅ **Journal d'activité (audit trail)** — nouvelle table `journal_activite` (migration `009`), alimentée automatiquement par `Model::insert()/update()/delete()` (classe de base de tous les modèles, zéro changement requis ailleurs) : qui a créé/modifié/supprimé quoi, quand, avec l'état avant/après. Consultable dans un nouvel onglet de la page `/Historique` (filtrable employé/action/type de donnée/période, détail avant/après à la demande). Lecture seule côté appli (commit `249cdd0`).
- ✅ **Sauvegarde locale chiffrée** — nouveau module `/Sauvegarde` (réservé admin) : dump complet de la base en PHP pur (portable, pas de dépendance à `mysqldump`), chiffré en AES-256-GCM avec un mot de passe choisi par l'utilisateur (jamais stocké), détection native de toute corruption/altération/mauvais mot de passe au déchiffrement (refus propre, jamais de données invalides importées). Restauration avec confirmation explicite ("RESTAURER" à taper + popup navigateur). Testé de bout en bout (chiffrement, dump sur `station`, restauration réelle sur une base jetable, HTTP complet) sans jamais toucher aux données de `station` (commit `9a8462c`).
- ✅ **Bandeau du journal d'activité dans la top bar** : dernières actions du journal (qui/quoi/quand) affichées en défilement continu, pause au survol, clic → modale de détail avant/après (réutilise `/Historique/journal_detail`). Nouvelle méthode `JournalActivite::derniers()` + action `Home/journal_ticker` (fragment AJAX, même mécanisme que `Home/notifications`), remplace l'ancien spacer de la top bar. Testé en local (insertion/suppression d'une ligne journal temporaire pour vérifier le rendu et la modale, table repartie à 0 ligne après test).
- ✅ **Fix modale du bandeau journal "brouillée"/infermable** : la modale vivait dans le fragment AJAX du bandeau, lui-même dans `.ssm-topbar` (`position:sticky`+`z-index`, donc contexte d'empilement propre) — la modale plein écran de Bootstrap se retrouvait piégée derrière le reste de la page, et le rechargement AJAX toutes les 90s pouvait la détruire pendant qu'elle était ouverte (backdrop orphelin, obligeant à recharger la page). Déplacée dans `footer.php`, hors de `.ssm-shell` ; handler de clic délégué sur `document` (persiste aux rechargements du bandeau).
- ✅ **Réglages d'affichage par tableau + thème par module** (sous-module Client de Vente entièrement équipé — voir `SKILL_MODULE_VENTE.md` §9) : bouton engrenage (à gauche du plein écran) ouvrant une modale à presets **fermés** (jamais de couleur libre) — couleur d'entête, couleur de bordure de card (portée table entière), **alignement ET couleur de texte par colonne** (identifiée par le nom de colonne, pas son index) — enregistrés par utilisateur + par tableau (`parametre_affichage_table`, migrations 010+011). Composant générique réutilisable (`ssm_panel_settings()` dans `ajax.js` + `ReglageAffichageController`/`AffichageTable`), un seul appel JS + wrapper HTML par panel. Bouton "Thème" à l'extrême droite du sous-menu Vente (`menu_client.php`) : direction visuelle scopée au module (`parametre_style_module`, migration 010) sans toucher au thème global de l'utilisateur.
  - ⚠️ **Fix retour utilisateur** : l'alignement initial (par tableau entier, via classe sur le panel) était sans effet ("j'ai choisi droite mais rien n'est fait") — cause : les `columnDefs` DataTables posent déjà `className: 'text-center'`/`'text-right'` (Bootstrap, en `!important`) sur les cellules, qui gagnaient toujours face à une règle sans `!important`. Refonte : alignement passé **par colonne** (comme demandé) + toutes les règles générées (couleur ET alignement) scopées par l'id du panel + `!important` (migration 011 : suppression de la colonne `alignement` globale, déplacée dans le JSON `colonnes`).
  - Étendu à tout le sous-module Client : `Clients.php` (Clients + Groupes), `Cartes.php`, `Encaissements.php`, `Livre.php` (`vente_clients`/`vente_groupes`/`vente_cartes`/`vente_encaissements`/`vente_livre`).
  - Testé en local (roundtrip save/get avec la structure par colonne, whitelist serveur, bouton présent sur les 4 pages + les 2 tableaux de Clients.php, thème module appliqué sur Vente et absent ailleurs, aucune erreur PHP) ; données de test nettoyées après vérification.
  - **Prochain sous-module à équiper** : Gestion des bons (même recette, voir §9 du skill).
- ✅ **Fix critique navigation AJAX inter-pages** : ouvrir une modale (ex. "Ajouter" sur Clients) puis naviguer vers un autre onglet du sous-menu (ex. Cartes) sans la fermer bloquait TOUTE l'application (plus aucune modale/bouton nulle part, y compris la top bar) — le DOM de la modale (dans `#ssm_module_content`) était détruit par la navigation sans que Bootstrap nettoie son `.modal-backdrop` (en dehors de cette zone) ni les classes posées sur `<body>`. Fix dans `ssm_nav_ajax()` (`ajax.js`) : fermeture + nettoyage forcé du backdrop avant tout remplacement de contenu — corrige le problème pour tous les modules qui utilisent la navigation AJAX, pas seulement Vente. Documenté dans `SKILL_MODULE_VENTE.md`.
- ✅ **Filet de sécurité complémentaire** : sur retour utilisateur (Cartes → Clients → Ajouter → modale affichée mais inerte, sans focus), ajout d'un nettoyage automatique `shown.bs.modal` qui supprime tout `.modal-backdrop` en trop dès qu'une modale s'affiche avec succès (ne garde que le dernier) — filet générique en plus du fix ciblé ci-dessus. **Point important signalé à l'utilisateur** : `ajax.js` n'est jamais rechargé par la navigation AJAX (seul un rechargement complet de page, Ctrl+F5, recharge les `<script>`) — un onglet resté ouvert depuis avant un fix JS continue de tourner sur l'ancienne version tant qu'il n'est pas rechargé en dur, même si le fichier serveur est déjà corrigé.
- ✅ **Vraie cause trouvée et corrigée** (le problème persistait après les deux fixes ci-dessus) : reproduit avec un vrai Chromium headless (Playwright, installé à la volée dans le scratchpad de session, pas ajouté au projet) pour observer le DOM réel au moment du clic — `document.elementFromPoint()` sur le champ cliqué révélait que c'était `.modal-backdrop` qui recevait le clic, pas le champ. Cause : `#ssm_module_content.ssm-content-in-*` (animation d'entrée de la nav AJAX) utilise `animation-fill-mode: both`, qui laisse un `transform: translateX(0)` posé en permanence sur `#ssm_module_content` une fois l'animation finie (jamais retiré) — n'importe quel `transform`, même sans mouvement visible, crée un nouveau conteneur d'empilement pour tout `position:fixed` descendant, piégeant toute modale ouverte APRÈS au moins une navigation AJAX (rendue plus petite que l'écran, backdrop qui passe par-dessus). Même famille de piège que celui déjà documenté pour le plein écran, cette fois sur les modales. Fix : la classe d'animation est retirée (`ssm_module_nav_transition()`, `ajax.js`) une fois la durée de l'animation écoulée. Vérifié en rejouant le scénario exact avec Playwright : clic dans le champ, saisie, fermeture — tout fonctionne. Voir `SKILL_MODULE_VENTE.md` §2/§3 (piège détaillé + méthode de test navigateur).
  - ⚠️ **Bug pré-existant repéré au passage, non corrigé (hors scope)** : `app/views/home/Home.php` contient encore 3 configurations DataTable avec l'ancienne clé `oLanguage.sUrl` pointant vers `cdn.datatables.net` (bloqué par CORS en pratique) — provoque une erreur JS (`Cannot set properties of undefined (setting 'nTf')`) à chaque connexion, sur la page d'accueil. Le reste de l'appli a déjà été corrigé (traduction française en dur, voir `init_datatable()`), seul `Home.php` a été manqué. À corriger dans une prochaine tâche dédiée.
- ✅ **Notification + fermeture automatique après ajout/modification en modale** (pilote `Clients.php`/`#ajouter_client`, voir `SKILL_MODULE_VENTE.md` §10) : nouveau composant `ssm_notify()` (remplace `toastr` sur ce flux, pas partout) + `ssm_modal_auto_close()` (barre rouge qui se rétrécit, ferme la modale au bout de 5s sauf interaction de l'utilisateur entretemps). Comportement différencié : après un **ajout**, la modale se recharge vierge puis se referme toute seule (pratique pour enchaîner) ; après une **modification**, fermeture directe (pas de rechargement vierge, qui n'aurait pas de sens). Bug trouvé et corrigé pendant les tests navigateur : le rechargement du formulaire déclenche lui-même un `.trigger('change')` programmatique (peuplement du select2 des groupes) qui annulait la fermeture automatique instantanément — fix : ignorer tout événement sans `e.originalEvent` (jamais présent sur un `.trigger()` JS, toujours présent sur une vraie action utilisateur). Testé de bout en bout avec Playwright (3 scénarios : silence → fermeture à 5s ; interaction → annulation ; modification → fermeture directe), données de test nettoyées après vérification (`client`/`client_roles`/`journal_activite`).
  - ✅ **Détail de ce qui a été ajouté/modifié dans la notification** : `ssm_notify()` accepte un 4e paramètre `details` (`[{label,valeur}]`, entrées vides ignorées) construit à partir des champs pertinents du formulaire (Nom, Groupe, Plafond, Délai de paiement, Mode de paiement — pas les id techniques). Bug trouvé pendant le test navigateur : `Nombre()` (ajax.js) plantait silencieusement sur une valeur passée en chaîne (`.val()`) au lieu d'un nombre, empêchant l'envoi du formulaire (bouton restait caché, rien ne se passait) — fix : toujours `Nombre(parseFloat(valeur))`. Revérifié avec Playwright après correction, données de test nettoyées.
- ✅ **Dispositions de navigation alternatives (palette/rail/dock) + comptes ouverts séparés** (voir `SKILL_MODULE_VENTE.md` §11/§12, lu depuis `left et top .md` à la racine) :
  - Nouvelle préférence par utilisateur `parametre_layout_user` (migration 012) : `classique` (défaut, inchangé pour tous les comptes existants), `palette` (recherche Ctrl+K, sans menu latéral), `rail` (liseré caché + tiroir accordéon au survol), `dock` (dock magnétique flottant, magnification style macOS). Réglable dans le panneau Réglages ("Disposition de navigation").
  - Toutes les 3 réutilisent les **vraies données** de l'appli (pas les arbres/notifications fictifs des prompts) : `Layout::menu_config()` (déjà filtré par droits) via le nouvel arbre `Layout::menu_arbre()`, le journal d'activité déjà construit (`/Home/activite_json`), les notifications déjà calculées par domaine réel Ventes/Achats/Coffre/Banque (`/Home/notifications_groupees`, pas de "Messages/Système" fictifs).
  - Adaptées aux 4 directions visuelles existantes via les tokens CSS (jamais les couleurs en dur des prompts) — fichiers dédiés `public/css/design/layouts.css` + `public/js/ssm_layouts.js`, chargés **uniquement** si la disposition choisie n'est pas classique (zéro impact sur les comptes qui n'ont pas changé leur préférence).
  - **Comptes ouverts séparés** du panneau Réglages (mélangé avec le thème auparavant, retour utilisateur explicite) : nouvelle icône dédiée dans la top bar (porte ouverte, accent vert) avec son propre dropdown, style différent, présent dans les 4 dispositions.
  - Testé de bout en bout avec Playwright : palette (Ctrl+K, filtre clavier, navigation par Entrée), rail (survol, dépli accordéon, tiroir d'activité), dock (ouverture FAB, magnétisme au survol), notifications groupées et comptes ouverts présents dans chaque disposition, aucune erreur JS, non-régression vérifiée sur la disposition classique (modale Ajouter + comptes ouverts).
  - **Reste à faire** : la disposition palette/rail/dock n'a été testée que sur la direction visuelle Irisé — revérifier rapidement sur Majorelle/Pupitre/Aurore si un rendu détonne (peu probable, tout passe par les tokens CSS communs). Le panneau de notifications groupées est volontairement **identique** entre les 3 dispositions (seul le bouton déclencheur change de forme) — simplification assumée plutôt que reproduire un donut/cluster de compteurs bespoke pour chaque mode.
- ✅ **4 bugs remontés après livraison, corrigés et revérifiés** (voir `SKILL_MODULE_VENTE.md` §13) :
  1. Icônes de notification invisibles en thème clair (`text-white` en dur, hérité d'avant la refonte Phase 4) → couleurs adéquates par domaine (accent/warn/ok/bad).
  2. Dock : le volet de pages se refermait avant de pouvoir cliquer un lien (piège du survol à travers un espace vide, même famille que le rail) → affichage piloté en JS avec délai.
  3. Ordre du menu incohérent entre classique et les 3 autres dispositions (regroupement "Général"/"Exploitation" inventé) → `Layout::menu_arbre()` respecte désormais exactement l'ordre et les libellés de `menu_config()`.
  4. "Texte noir / texte à couleur de thème incohérent" → cause unique trouvée : règle CSS générique `body[data-design] a { color: var(--accent); }` plus spécifique que mes classes non préfixées, écrasait silencieusement la couleur prévue sur tous les liens (`<a>`) du rail/dock/palette/comptes-ouverts. Toutes re-préfixées `body[data-design] .ma-classe`.
  - Mode sombre testé à fond (Majorelle + Aurore, classique + dock) : fonctionne correctement dans tous les cas essayés — le ressenti venait très probablement du bug #4 (rendu incohérent), pas d'un vrai dysfonctionnement. À reconfirmer après un Ctrl+F5.
- ✅ **Confirmation avant d'écraser un thème de module + icônes de sous-menu + même piège de couleur dans le menu classique** (voir `SKILL_MODULE_VENTE.md` §15) :
  - Changer le thème global ne demande confirmation ("appliquer partout" / "garder leurs thèmes spécifiques") **que si** l'utilisateur a réellement une surcharge de module active (`Layout::donnees()['modules_surcharges']`) — sinon comportement immédiat inchangé.
  - Icônes ajoutées à chaque sous-entrée de menu (`Layout::menu_config()`), affichées dans le menu classique ET dans rail/dock/palette (mêmes données, `Layout::menu_arbre()`).
  - Même piège de spécificité CSS que §13 mais trouvé cette fois dans le **menu classique historique** (`.ssm-nav-link`/`.ssm-nav-sublink` non préfixés `body[data-design]`) — existait depuis le premier jour de la Phase 4, jamais remarqué avant. Corrigé.
  - Testé de bout en bout avec Playwright (sans surcharge → pas de modale ; avec surcharge → modale, "garder" préserve, "appliquer partout" uniformise et efface la surcharge). Préférences réelles de l'utilisateur restaurées après tests.
- ✅ **Retrait des thèmes historiques Sombre/Lightblue** (voir `SKILL_MODULE_VENTE.md` §20) : boutons retirés du panneau Réglages (ne restent que Majorelle/Pupitre/Irisé/Aurore) ; migration `013` réassigne à Majorelle toute préférence utilisateur/module qui pointait encore vers `dark`/`blue` (le fallback existait déjà côté affichage, cette migration remet juste les données en cohérence). Testé : plus que 4 cartes dans le sélecteur.
- ✅ **Fix menu classique : lien actif survolé = texte blanc sur fond blanc** (voir `SKILL_MODULE_VENTE.md` §19) : `.ssm-nav-link:hover` avait une spécificité supérieure à `.ssm-nav-link--active` — sans `!important` sur le `background` de l'état actif (seul `color` en avait), le survol faisait gagner un fond clair pendant que le texte restait blanc (pensé pour l'accent plein). Fix : `background` aussi en `!important`. Revérifié avec Playwright (couleurs identiques avant/pendant le survol).
- ✅ **Fix texte des notifications sur 2 lignes malgré l'élargissement** (voir `SKILL_MODULE_VENTE.md` §18) : `.ssm-menu-lg` (420px) et `.ssm-menu` (180px, autre fichier chargé après) avaient la même spécificité — la règle chargée en dernier gagnait, écrasant les 420px. Fix : `.ssm-menu.ssm-menu-lg` (classes chaînées, spécificité supérieure). Revérifié avec Playwright : dropdown à 420px, texte sur une seule ligne.
- ✅ **Top bar agrandie + fil d'ariane en pastilles + bandeau des prix carburant** (voir `SKILL_MODULE_VENTE.md` §17) : notifications élargies (320px→420-460px, contenu plus lisible) ; top bar agrandie (68px, icônes/marque plus grandes) ; fil d'ariane restylé en piste de pastilles (pastille pleine accent pour la page courante) ; logo/nom de station retiré du menu classique (ne vit plus que dans la top bar, qui a plus de place pour bien le présenter) ; journal décomposé en deux bandeaux empilés — actions (inchangé) + nouveau bandeau des derniers prix de vente des carburants (`HomeController::prix_carburants()`, réutilise `ProduitPrix::produit()` déjà existante, rafraîchi toutes les 3 minutes). Testé : tout s'affiche et fonctionne, aucune erreur JS.
- ✅ **Top bar unique pour toutes les dispositions + réglages en FAB + notifications refaites** (voir `SKILL_MODULE_VENTE.md` §16) : la top bar palette/rail/dock (bandeau d'activité/notifications groupées, distincte de la classique) a été supprimée — une seule top bar (classique) partout, seul le hamburger change de sens selon la disposition (replie le menu / ouvre la palette / bascule le tiroir rail / déclenche le dock). Ajouts : nom de la station affiché (`Station::nom()`, `parametre_station.nom`) avec marge propre autour du bandeau d'activité + fondu des bords au défilement ; contenu des dropdowns Ventes/Achats/Coffre/Banque entièrement refait (rangées propres, plus de `<ul>`/bootstrap) ; icônes de notification unifiées en une seule couleur (accent du thème) et agrandies ; bouton Réglages détaché de la top bar, sticky en bas à droite de l'écran. Code mort nettoyé (endpoints et fonctions JS de l'ancienne top bar alternative supprimés). Testé sur les 4 dispositions, aucune erreur JS, préférences réelles restaurées après tests.
- ✅ **Fix "le thème global revient à l'ancien"** (voir `SKILL_MODULE_VENTE.md` §14) : deux causes cumulées. (1) Une ligne de test oubliée dans `parametre_style_module` (créée en construisant la surcharge de thème par module, §9) figeait le module Vente sur Majorelle — supprimée (aucune ligne ne doit rester pour un compte qui n'a pas choisi lui-même un thème par module). (2) Vraie course critique dans `footer.php` : `location.reload()` appelé juste après avoir lancé la sauvegarde (asynchrone) sans attendre sa fin, pouvait recharger la page avant l'écriture en base — fix : reload déplacé dans le callback de `load_portion`. Revérifié avec Playwright (changement de thème sur une page Vente, persistant sur Home) : le thème choisi reste bien appliqué. Préférence réelle de l'utilisateur (Aurore/sombre) restaurée après test.
  - ⚠️ **Persistant après ce fix** : l'utilisateur a retesté et observé le même symptôme. Vérifié en se connectant (Playwright) : reproductible, pas du cache navigateur. Vraie cause : il avait entre-temps utilisé le bouton "Thème" du sous-menu Vente (surcharge par module, §9), qui gagne toujours sur le thème global tant qu'on reste dans ce module — un choix de thème global semblait donc "ne rien faire" sur les pages Vente (il s'appliquait bien ailleurs). **Fix de fond** : `HomeController::style()` (changement de thème global) supprime désormais toutes les surcharges par module de l'utilisateur — un choix dans le panneau Réglages est un geste explicite et global, il gagne partout. Revérifié : 3 changements de thème successifs depuis une page Vente, chacun s'applique immédiatement. Théme réel de l'utilisateur (Aurore/sombre) restauré, aucune surcharge résiduelle en base.

- ✅ **Fix chevauchement des FAB flottants + scrollbar du panneau Réglages** (voir `SKILL_MODULE_VENTE.md` §21) : `.ssm-container` avait un padding-bottom insuffisant (40px) pour dégager les FAB flottants (Réglages bas-droite, dock bas-gauche) → porté à 96px. Panneau Réglages sans scrollbar sur petit écran (Déconnexion inatteignable) → `.ssm-settings-body` avait `overflow-y:auto` mais pas `flex:1 1 auto; min-height:0`, donc ne se contraignait jamais en dessous de la hauteur de son contenu. Revérifié avec Playwright (viewport réduit) : scrollbar fonctionnelle, FAB ne chevauchent plus le contenu.
- ✅ **Style + alignement du sous-menu de module** (voir `SKILL_MODULE_VENTE.md` §22) : 3 styles pour la barre d'onglets Client/Carte/Encaissement/Grand Livre — Plein (défaut, thumb glissant, inchangé), Ligne (trait fin en bas + icône colorée, reprend l'ancienne "V3"), Contour (chips à bordure colorée, sans thumb) — plus un choix d'alignement (gauche/centre/droite). Réglable globalement (panneau Réglages) et par module (dropdown "Thème"), même architecture que la direction visuelle (migration `014` : colonnes `nav_style`/`nav_align` sur `parametre_style_user`/`parametre_style_module`). Bug trouvé et corrigé en cours de route : l'invalidation du cache session de l'endpoint global ne portait que sur les nouvelles colonnes, alors que `style()` ne relit la BDD que si `style_nom` n'est pas déjà en cache — les nouvelles valeurs n'étaient donc jamais relues malgré l'écriture SQL réussie. Testé via curl (login + endpoints + lecture du HTML rendu). Préférences réelles de l'utilisateur restaurées après tests.

- ✅ **Panneau Réglages en un seul "Enregistrer" + fix contour + style de texte par colonne** (voir `SKILL_MODULE_VENTE.md` §23) : le panneau Réglages global sauvegarde désormais tous les choix (direction, mode, disposition, style/alignement de sous-menu) en un seul clic sur un bouton "Enregistrer les modifications" sticky (hors de la zone qui défile) au lieu d'un reload à chaque clic. Style "Contour" du sous-menu corrigé : réutilise maintenant le même thumb glissant que "Plein"/"Ligne" (même mouvement animé) et les onglets gardent une boîte strictement identique entre les 3 styles (police/taille/padding, plus de différence). Ajout d'un style de texte par colonne (gras/normal/italique) dans le réglage d'affichage d'un tableau, en plus de la couleur et de l'alignement déjà existants (aucune migration nécessaire, simple extension du JSON par colonne). Préférences réelles restaurées après tests.

- ✅ **Police et taille de texte de toute l'application, global + par module** (voir `SKILL_MODULE_VENTE.md` §24) : nouveau choix de police (Thème/Système/Classique, migration `015`) et de taille de texte (Petit/Normal/Grand, via `zoom` sur `<body>` entier — modales comprises) dans le panneau Réglages global et le dropdown "Thème" par module, même mécanisme que le style de sous-menu. Endpoints existants (`/Home/nav_style`, `/Home/nav_style_module`) étendus plutôt que dupliqués. Testé, préférences réelles restaurées.

- ✅ **Totaux "Page"/"Filtré" de la DataTable Encaissements, optimisés + réaffichés proprement** (voir `SKILL_MODULE_VENTE.md` §25) : le total filtré était calculé par un appel AJAX séparé (4e exécution de la requête à chaque tirage) avec un affichage bricolé (`<script>` inline). Désormais calculé dans la MÊME requête SQL que le comptage filtré et renvoyé dans le MÊME JSON que la page de résultats (plus d'appel séparé), affiché avec le pattern `.ssm-stat-row` déjà utilisé pour le Grand Livre. Nouveaux helpers génériques `ssm_stat_set_montant()`/`ssm_table_totaux()` (ajax.js) posés comme modèle de référence pour généraliser aux ~30 autres tables à montants du même principe (pas encore fait, à la demande - un module à la fois).

- ✅ **Disposition "Rail caché" remplacée par "Roue" (menu radial)** (voir `SKILL_MODULE_VENTE.md` §27) : cahier des charges précis fourni par l'utilisateur (hub circulaire → anneau des sections → anneau des pages, géométrie trigonométrique) — implémenté et adapté à nos tokens (couleurs/police par thème, easing déjà utilisé ailleurs), données réelles (`SSM_MENU_ARBRE`), et rayon dynamique (jusqu'à ~13 sections pour un profil complet, pas seulement les 8 de l'exemple). Slug interne `rail` inchangé (pas de migration), seul le libellé ("Roue") et le mécanisme visuel changent. Testé de bout en bout avec Playwright, préférence réelle (disposition Palette) restaurée après test.

- ✅ **Fix thumb du sous-menu de module décalé de 6px** (voir `SKILL_MODULE_VENTE.md` §28) : `.ssm-module-nav-thumb` avait un `left: 6px` codé en dur qui s'additionnait au `translateX()` déjà complet calculé en JS - décalage constant de 6px vers la droite, peu visible sur "Plein" (masqué par le padding de l'onglet) mais net sur "Ligne"/"Contour" (trait/bordure précis) et proportionnellement plus visible sur les onglets les plus à droite (Grand Livre). Bug pré-existant à la refonte des styles, juste rendu visible par eux. Fix : `left: 0`. Revérifié avec Playwright sur les 4 onglets × 3 styles, correspondance exacte au pixel près.

- ✅ **Fix thumb du sous-menu — 2e cause, course de chargement pas du cache** (voir `SKILL_MODULE_VENTE.md` §29) : le décalage persistait malgré Ctrl+F5 après le fix du 6px (§28) - diagnostiqué comme une vraie course entre `ssm_module_nav_positionner_thumb()` (calculé une fois, synchrone) et le chargement asynchrone de la police Google Fonts + police d'icônes Font Awesome (texte/icônes reflow après coup, thumb jamais recalculé). Reproduit en bloquant volontairement la requête Google Fonts. Fix robuste : `ResizeObserver` sur chaque `.ssm-module-nav` qui repositionne le thumb à tout changement de taille, quelle qu'en soit la cause. Revérifié en conditions de police bloquée : correspondance exacte après le fix (vs décalage net avant).

- ✅ **Palette (Ctrl+K) groupée par section façon `<optgroup>`** (voir `SKILL_MODULE_VENTE.md` §30) : remplace la liste plate avec pastille de section par ligne par un vrai regroupement visuel (entête sticky par section, entrées isolées comme Accueil sans entête) — reprend une classe CSS déjà stylée mais jamais utilisée. Navigation clavier et filtre inchangés (déjà compatibles). Testé avec Playwright.

- ✅ **Raccourcis clavier globaux Ctrl+<lettre>** (voir `SKILL_MODULE_VENTE.md` §31) : Ctrl+<première lettre d'un bouton visible> déclenche ce bouton, un modal si plusieurs boutons partagent la lettre, choix des entrées d'un dropdown si le bouton en a un ; Ctrl+M ouvre le menu quelle que soit la disposition ; Ctrl+/ (bonus) liste les raccourcis disponibles sur la page courante. Global (toute page, tout module). Garde-fou : jamais actif si le focus est dans un champ de saisie. Testé avec Playwright, un bug trouvé et corrigé (filtre `:visible` excluait à tort les entrées d'un dropdown fermé).

- ✅ **Trois compléments raccourcis/UI** (voir `SKILL_MODULE_VENTE.md` §32) : Ctrl+flèches gauche/droite change de page sur la DataTable "en focus" (survolée/cliquée en dernier - gère le cas de plusieurs tables sur une même page) ; Échap ferme désormais n'importe quelle modale Bootstrap ouverte, y compris celles en `data-keyboard="false"` (override délibéré, demande explicite) ; le panneau Réglages global se ferme au clic en dehors, plus seulement via le bouton X. Testé avec Playwright.

- ✅ **Ctrl+Maj+flèches : onglet précédent/suivant du sous-menu de module** (voir `SKILL_MODULE_VENTE.md` §33) : choisi par l'utilisateur parmi 3 propositions (vs Alt+flèches, risque de collision navigateur ; Ctrl+, / Ctrl+., moins intuitif). Fonctionne sans avoir à focus l'onglet au préalable. Testé sur Clients→Cartes→Encaissements et retour, non-régression vérifiée sur les autres raccourcis.

- ✅ **Modal "Raccourcis clavier" dans le panneau Réglages** (voir `SKILL_MODULE_VENTE.md` §34) : bouton dédié ouvrant une référence complète de tous les raccourcis (badge + explication), source unique `SSM_RACCOURCIS_INFO` (`ajax.js`) à compléter à chaque futur raccourci ajouté — **règle permanente**. Bug de collision trouvé et corrigé au passage : le panneau Réglages (toujours en DOM, juste hors écran) polluait le scanner de raccourcis Ctrl+<lettre> sur toute page ; désormais exclu.

- ✅ **Modal de choix (flèches+défaut), focus auto formulaire d'ajout, Select2 dans une modale, Ctrl+↓/↑ entre champs** (voir `SKILL_MODULE_VENTE.md` §35) : premier choix sélectionné par défaut + navigation flèches dans le modal de choix ; focus auto sur le premier champ d'un formulaire d'AJOUT (ouvre + focus la recherche si Select2) ; **2 bugs Bootstrap/Select2 non-triviaux trouvés et corrigés** (`.modal('hide')` puis ouvrir une autre modale dans la même frame laissait la première bloquée "show" indéfiniment ; Select2 rendait son menu hors du DOM de la modale, empêchant tout focus réel sur sa recherche - fix global via un wrapper de `$.fn.select2` injectant `dropdownParent`) ; nouveau raccourci Ctrl+↓/↑ pour naviguer entre les champs d'un formulaire (alternative à Tab). Limite connue documentée : Select2 sans zone de recherche (peu d'options) pas totalement fiable avec ce raccourci.

### 22/07/2026
- ✅ **Phase 4, sous-module Bons (2e sous-module de Vente) traité en entier** (voir `SKILL_MODULE_VENTE.md` §36 pour le détail) : `menu_BonClient.php` refait sur le modèle `menu_client.php` (thumb, nav AJAX, `ssm_module_theme_menu_html('bons')`) ; les 6 pages (`BonPassage`, `ClientBon`, `ClientAvoir`, `Archive`, `Etats`, `ClientEtat` — cette dernière jamais auditée avant, page détail "état d'un client" avec 2 sous-tableaux Bon/Produit) équipées `.ssm-panel` + toolbelt (réglage+plein écran) + `.ssm-table-toolbar` + wrapper `#ssm_module_content`. **Totaux Page/Filtré** ajoutés/fusionnés en une seule requête SQL sur toutes les tables à montant trouvées (`ClientBon::data()`/`data_etat()`, `ClientBonPassage::data()`, `ClientAvoir::data()`, `ClientEtat::data()`/`etat_produit()`) — suppression des anciens appels AJAX séparés `total_total()` (5 actions contrôleur retirées, devenues inutiles). `Mensualite.php` (Statistique) équipée panel/plein écran mais sans totaux Page/Filtré ni réglage d'affichage : ses 2 tableaux (`mensualites.php`/`credits.php`) sont des fragments PHP non paginés déjà agrégés côté serveur (pas des DataTables), donc pas concernés par le pattern. Notification flash d'`Etats.php` (annulation) migrée vers `ssm_notify()` ; la notification "génération réussie + bouton Annuler" gardée en composant dédié (ssm_notify ne porte pas de bouton d'action). Testé par curl (login, chaque page normale + `X-Ssm-Nav: 1`, chaque endpoint datatable avec `montant_filtre` vérifié dans le JSON, `php -l` sur tous les fichiers touchés).

### 23/07/2026
- ✅ **4 bugs remontés après livraison du sous-module Bons, corrigés et documentés** (voir `SKILL_MODULE_VENTE.md` §37 pour le détail complet, méthode de test Playwright) :
  1. Thumb du sous-menu de module qui débordait sous le réglage "Taille de texte : Grand/Petit" (zoom CSS) — `ssm_module_nav_positionner_thumb()` (`ajax.js`) utilisait des mesures jQuery déjà "zoomées", re-multipliées par le zoom au rendu. Fix : `offsetLeft`/`offsetWidth` natifs (repère local, zoom-safe). Concerne tout `.ssm-module-nav` de tout module, corrigé une fois pour toutes.
  2. Menu déroulant Select2 mal placé dans les modales sous le même réglage de zoom (même famille de piège, côté librairie tierce) — fix global dans `ajax.js` (écouteur `select2:open`, repositionne via `getBoundingClientRect` divisé par le zoom). Un 1er essai (correction en `setTimeout`) laissait un flash visible (position erronée peinte avant la correction) ; un 2e essai (correction synchrone) supprimait le flash mais se faisait écraser par le repositionnement interne de Select2 (bug permanent, pire) ; fix retenu : `requestAnimationFrame` + `visibility:hidden` le temps de la correction.
  3. `iTotalRecords` (total non filtré) confondu avec `iTotalDisplayRecords` (total filtré) dans 2 méthodes du modèle Bons (`ClientBon::data_etat()`, `ClientEtat::etat_produit()`) — faussait l'info de pagination "Affichage de X à Y sur Z" dès qu'une recherche était active. Même bug repéré (non corrigé, hors périmètre) dans `Client::livre()` (Grand Livre, module Vente).
  4. Fond coloré (`bg-danger`/`bg-success`/etc) sur les `<thead>` de DataTable, retiré des 6 pages Bons — règle permanente désormais : `<thead>` neutre sans classe de couleur (le design system impose déjà un fond opaque sticky via CSS). Pas encore rétro-appliqué au module Vente (ex. `Encaissements.php`).
- ✅ **Recette modale (icônes/notify/auto-close/Ctrl+lettre) appliquée aux flux "ajout" du sous-module Bons, oubliée à la livraison initiale** (voir `SKILL_MODULE_VENTE.md` §38) : `modal/ajout_BonClient.php`, `modal/ajout_Avoir.php` (icône de modale, `data-mode`, field-icons, boutons `.ssm-btn`) ; boutons "Ajouter" de niveau page (`ClientBon.php`, `Etats.php`, `ClientAvoir.php`) convertis en `.ssm-btn.ssm-btn-primary` — cause directe du "Ctrl+A ne déclenche pas Ajouter" signalé (le scanner de raccourcis ne regarde que `.ssm-btn`) ; `ssm_notify()`/`ssm_modal_auto_close()` branchés sur les handlers de succès ajout/modification. Non traité : les modales d'action de `ClientEtat.php` (clôturer, attacher avance/avoir, facturer...), pas des formulaires "ajout" au même sens, à reprendre plus tard. Vérifié avec Playwright (Ctrl+A ouvre bien la modale/le choix, icônes et data-mode présents).

- ✅ **`ClientEtat.php` : header AJAX-ifié, plus de rechargement HTML complet à chaque action** (voir `SKILL_MODULE_VENTE.md` §39) : `ClientEtatController` refactoré (`header_donnees()`/`header_affichage()` privées, réutilisées par `header()` — rendu HTML réservé au tout premier chargement — et la nouvelle `header_json()` — JSON pur, utilisé par toutes les actions suivantes) ; `annuler_cloture()` extraite en action dédiée (mutation seule, sans rendu). `portion/Header.php` : tous les boutons conditionnels toujours rendus (classe `.d-none` au lieu d'un `if` PHP qui omettrait le HTML) pour être affichés/masqués en JS sans réinjection HTML ; chiffres avec `id` stables. Nouvelle fonction JS `rafraichir_header()` remplace tous les anciens `load_header()` (sauf le tout premier). 2 bugs pré-existants corrigés au passage : `dataTable_paiements` (3ᵉ DataTable du header) invisible depuis le script principal (variable locale, jamais accessible — `ReferenceError` silencieuse à chaque clic "Modifier" un paiement) ; `#data_bon` utilisait encore l'ancien `oLanguage.sUrl` cassé (CORS), pas seulement `Home.php` comme noté le 21/07. Testé : `header_json()` cohérent avec le rendu HTML initial (curl), `rafraichir_header()` reproduit un état identique sans mutation (Playwright), test de mutation réelle (détacher un bon sur l'état réel `#1341`) confirmé bout en bout puis **restauré manuellement en base** immédiatement après vérification. **Reste à faire (demande explicite, prochaine étape)** : séparer le dispatcher `ClientEtatController::datatable()` (4 DataTables aiguillées par un seul `if/elseif`) en une méthode dédiée par DataTable.

### 24/07/2026
- ✅ **Sous-module Facturation (Vente) traité en entier + fix global du rechargement de page en entrant sur un détail** (voir `SKILL_MODULE_VENTE.md` §40 pour le détail) : retour utilisateur — Facturation n'avait pas reçu le traitement Phase 4 fait sur Bons (toolbelt réglage/plein écran, barre imprimer/exporter, totaux `.ssm-stat-row`), et cliquer sur un règlement (comme un état) rechargeait toute la page. Cause racine trouvée : `dt_btn_lien()` (`vendor/function.php`) rendait un `<button onclick="window.location.href=...">` au lieu d'un `<a data-ssm-nav="1">` — un seul fix dans ce helper partagé corrige le rechargement PARTOUT où il est utilisé (bons/vente/achat/stocks/coffre), sans toucher un seul contrôleur. `Impayees.php`/`Factures.php`/`Reglements.php`/`Avoirs.php` équipés du pattern complet ; `Reglement.php` (détail) : cas nouveau à retenir — 2 DataTables simultanément visibles (pas commutées) → 2 sous-panels distincts au lieu d'un seul (sinon réglage/impression ciblerait toujours la même table) ; totaux Page/Filtré ajoutés à `ClientReglement::factures_reglement()`/`paiement_reglement()` (n'en avaient aucun) ; `portion/Header_reglement.php` restylé en `.ssm-panel-header`/`.ssm-situation-detail`. Testé : `php -l`, pages rendues par curl (200), JSON datatable vérifié (`data-ssm-nav="1"` présent sur le lien de détail).
- ✅ **`Reglement.php` : retouche demandée après livraison (marge Factures/Paiements, boutons réorganisés, tri des paiements)** (voir `SKILL_MODULE_VENTE.md` §40, complément) : les 2 colonnes Factures/Paiements sont maintenant de vraies cartes bordées (`.card`, `.h-100`, classes `.ssm-parallele-col` pour l'écart) — l'égalité de hauteur était déjà vraie (flex Bootstrap) mais invisible sans cadre. Boutons réglage/imprimer/exporter/ajouter regroupés proprement (2 lignes par carte au lieu de 3 emplacements dispersés), "Ajouter" avec texte en plus de l'icône. **Découverte en creusant la demande de tri** : `ClientReglement::paiement_reglement()` provoquait une vraie erreur SQL fatale dès qu'on essayait de trier par une colonne autre que "mode" (date/numéro résolus en PHP après coup, hors de la requête triée) — d'où le tri désactivé sur ces colonnes côté JS avant retouche. Nouvelle méthode `paiement_reglement_optimise()` : une seule requête `UNION ALL` (espèce/pièce/opération/carte + avance, avec repli sur l'ancienne résolution PHP **inchangée** pour le seul cas non reproductible en SQL — avance chaînée sur une avance antérieure) rend les 4 colonnes réellement triables en SQL. Ancienne méthode/`info_paiement()`/`paiement_modif()` non touchées (toujours utilisées ailleurs, ex. `cloture()`). **Vérifié par script de comparaison ligne à ligne sur les 2009 règlements réels ayant des paiements : 0 différence.**
- ✅ **Fix screenshot (capture d'écran utilisateur) : `.card` imbriquée dans `.card` invisible** — voir complément ajouté à `SKILL_MODULE_VENTE.md` §40 : `body[data-design] .card .card {...}` aplatit délibérément toute carte imbriquée (évite l'effet boîte-dans-la-boîte), donc `.card.card-outline` posé sur Factures/Paiements (eux-mêmes DANS le `.card` englobant) ne produisait aucun cadre visible. Remplacé par `.ssm-situation-detail` (même composant que `bons/portion/Header.php`), qui n'est pas une `.card` donc jamais aplati.
- ✅ **Fix retour utilisateur : filtres remis à zéro en revenant sur un règlement depuis un lien "Retour à la liste"** — cause : le cache d'onglet de sous-module (`ssm_tab_cache`, `ajax.js`) ne restaurait l'état mémorisé (bonne sous-page + mêmes filtres) que pour un clic sur l'onglet du sous-menu lui-même (`estOngletModule`), pas pour un lien quelconque menant à la même URL (ex. "Retour à la liste des règlements"). Le retour navigateur (popstate) le faisait déjà de façon générique - incohérence corrigée : tout lien `data-ssm-nav` restaure désormais le cache si une entrée existe pour l'URL visée, qu'il s'agisse de l'onglet ou d'un lien "Retour". Effet global immédiat sur tout module déjà équipé du sous-menu Phase 4 (Vente, Bons), aucune vue à modifier.
- ✅ **Audit des modales de Facturation + fix focus (retour utilisateur)** :
  - **Cause trouvée du "focus ne se fait pas pour Nouveau Lettrage"** : une vraie course - le bouton "Ajouter" porte `data-toggle="modal"` (Bootstrap affiche la modale IMMÉDIATEMENT) ET un `load_portion()` manuel sur le même clic (le formulaire arrive quelques dizaines de ms plus tard, par AJAX) - `shown.bs.modal` se déclenchait donc sur une modale encore VIDE. Fix : la logique de focus (déjà existante) est appelée aussi depuis `load_portion()` une fois le contenu réellement arrivé, pas seulement depuis `shown.bs.modal` - corrige ce point pour TOUS les boutons "Ajouter" de l'application (c'est le pattern le plus utilisé), pas seulement Lettrage.
  - **Nouvelle fonctionnalité demandée** : le focus avance maintenant automatiquement au champ suivant dès qu'on choisit une option dans un select (natif ou Select2), dans un formulaire de modale ouverte - réutilise exactement la même fonction que le raccourci manuel Ctrl+↓ déjà en place. Volontairement limité aux modales (pas les filtres d'une page de liste, où ce serait intrusif) ; une garde empêche un `.trigger('change')` programmatique (très utilisé dans l'appli pour synchroniser des champs entre eux) de déclencher un saut de focus inattendu.
  - **Modales non conformes trouvées et corrigées** : `Reporter_reglement.php` et `Nouvelle_avance.php` (aucune des conventions ssm-* - icône, `data-mode`, field-icons, `ssm-btn`). **Découverte au passage** : `Reporter_reglement.php` est en réalité du code mort - aucun contrôleur ne le charge (le vrai formulaire "Reporter l'échéance" est le formulaire intégré directement dans `Header_reglement.php`, pas une modale) ; corrigé quand même par cohérence mais actuellement inatteignable. `Nouvelle_avance.php` : titre mis à jour "Ajouter un Nouveau Règlement" (cohérence avec le renommage). Toutes les autres modales du module (Lettrage, Règlements, Factures, paiement/*) étaient déjà conformes.
- ✅ **Refonte complète de la page Facture (Header_facture.php + Article.php + Facture.php)** (retour utilisateur, capture d'écran : "vraiment n'est pas professionnelle") : même traitement que les autres pages redesignées cette session - `Header_facture.php` (Client/N°/Date/Paiement/Statut/HT-TVA-TTC empilés en `<input readonly>` bruts) devient `.ssm-panel-toolbar` + `.ssm-etat-titre-actions` + `.ssm-kpi-value` (badge Payée/Impayée coloré) + `.ssm-stat-row` (même pattern que `Header_reglement.php`) ; `Article.php` (formulaire d'ajout d'article, bandes bleues `bg-info` illisibles) devient un encadré `.ssm-situation-detail` avec labels+icônes de champ, plus un vrai bouton Fermer (`#dimiss`, géré par un handler déjà existant dans `Facture.php` mais jamais câblé à un bouton avant) ; `Facture.php` : carte convertie en panel avec toolbelt (réglage/plein écran) + barre imprimer/exporter au-dessus du tableau, styles bricolés (`.border-class`, jamais utilisé) retirés. Vérifié : le mécanisme AJAX était déjà entièrement en place (aucun rechargement de page caché trouvé) - le problème était uniquement visuel. Rendu vérifié par curl (facture directe ouverte, facture état clôturée), création réelle d'une facture de test pour confirmer l'affichage, supprimée après.
- ✅ **Refonte complète de la modale "Nouvelle Facture" (retour utilisateur, capture d'écran : "vraiment n'importe quoi")** : la modale était construite en Bootstrap/AdminLTE brut avec une feuille de style locale bricolée par-dessus (au lieu du design system `ssm-*` utilisé partout ailleurs), causant 2 bugs distincts :
  1. Le "y" de "Payé" invisible - le select "Statut" était un `.form-control.form-control-sm` avec une hauteur/line-height forcée par le style local, en dehors des métriques de police globales de l'appli, qui clippait la descendante du "y". Fix : passage à `.custom-select` (même composant que partout ailleurs dans l'appli, jamais eu ce problème).
  2. Le menu déroulant du client s'affichait loin du champ, dans un coin de l'écran - la modale forçait `dropdownParent: $('.modal-content')`, un sélecteur qui vise TOUT le DOM (pas seulement cette modale) et pouvait tomber sur un tout autre `.modal-content` déjà présent sur la page (panneau Réglages, export, raccourcis clavier...). Un fix global existant dans `ajax.js` (wrapper sur `$.fn.select2`) injecte déjà automatiquement le bon parent via `this.closest('.modal')` dès qu'aucun `dropdownParent` n'est précisé - il suffisait de ne PAS le forcer manuellement pour que ce fix s'applique correctement. Même correctif appliqué au `$('.modal-content')` utilisé après l'envoi du formulaire (`$btn.closest('.modal-content')` au lieu d'un sélecteur global).
  Bannière "Tous les champs marqués d'un * sont obligatoires" retirée (demande explicite). Toute la logique JS (génération du numéro en debounce, auto-remplissage mode/délai au changement de client, calcul de la date d'échéance, validation, sauvegarde AJAX) conservée à l'identique. **Vérifié** : rendu de la modale par curl (plus aucune trace de l'ancien style/bannière/dropdownParent forcé), et création réelle d'une facture de bout en bout via le vrai formulaire - fonctionne, facture de test supprimée après.
- ✅ **Fix filtre Du/Au (retour utilisateur) : un règlement du jour même n'apparaissait pas sans mettre "Au" au lendemain** — cause réelle : `date_flux`/`date_operation`/`date_piece` sont des `DATETIME` (avec heure), alors que "Au" n'était qu'une `DATE` (minuit) - un règlement fait l'après-midi (`14:33:00`) était donc hors de l'intervalle `BETWEEN ... AND '2026-07-24'` (= `2026-07-24 00:00:00`). Fix retenu, plus large que juste corriger la comparaison : suppression complète du filtre Du/Au pour Lettré/Partiel (demande explicite) - même comportement que Non Lettré, tout s'affiche sans restriction de date. Champs Du/Au retirés du filtre `Avoirs.php`.
- ✅ **Ajout du filtre "Partiel" oublié dans le select (retour utilisateur, après le fix précédent)** : le select `#id_reglement_data` d'`Avoirs.php` n'avait que Non Lettré/Lettré - ajout de l'option "Partiel" (valeur 2), qui filtre désormais spécifiquement les paiements avec `montant_lettre < montant` ; le filtre "Lettré" exclut maintenant ces partiels (avant, il les incluait mélangés). Vérifié : Partiel = exactement les 29 lignes réelles trouvées précédemment, Lettré = 2172 (2172+29=2201, rien perdu).
- ✅ **Fix logique "Partiel" pour un paiement (retour utilisateur, après coup)** : le "Partiel" n'était en fait PAS inatteignable comme documenté juste après - l'utilisateur a identifié le vrai cas : un paiement (`client_paiement`) qui a généré une avance excédentaire à la clôture de SON lettrage (`ClientReglement::cloture()`, `client_avance.id_paiement_source`) et dont cette avance n'est pas encore rattachée ailleurs (`id_paiement=0`) n'est que PARTIELLEMENT lettré (montant_lettre = montant - excédent), pas 100%. Dès que l'avance excédentaire est attachée à un autre lettrage (ou qu'il n'y a pas d'excédent), le paiement redevient 100% lettré. Requête corrigée avec une jointure sur `client_avance` par branche de paiement. **Vérifié sur la base réelle : 29 paiements sont effectivement "Partiel"** (petits excédents historiques jamais rattachés) - le total "Lettré" a baissé de ~6252 Dhs en conséquence (montant réellement toujours en circulation). Revérifié aussi par un scénario de test complet (paiement 80/facture 52,50 → Partiel, puis attachement de l'excédent ailleurs → repasse à Lettré), données de test supprimées après.
- ✅ **Renommage métier Règlement→Lettrage / Avance→Règlement + vue unifiée + cards Factures ouvertes + Facture.php en AJAX inline** (voir `SKILL_MODULE_VENTE.md` §41 pour le détail complet) : demande large, plan écrit et approuvé avant exécution. "Règlement" (rapprochement factures/paiements) renommé "Lettrage" ; "Avance" (`client_avance`) renommée "Règlement" - les 2 onglets du sous-menu échangent leurs libellés (URLs inchangées). Nouvelle vue unifiée "Règlements" (`ClientAvance::data_reglement()`) : règlements créés directement + tous les paiements de lettrage, JAMAIS le même règlement affiché deux fois (exclusion des avances générées par une clôture ET des paiements de type "attachement d'avance" - même argent, double affichage évité). 2 bugs trouvés et corrigés en cours de route (alias SQL `date`/`date_document` cassant le tri ; 27 paiements "opération" avec `date_operation` NULL exclus à tort du filtre "Lettré", fixé via `COALESCE`). Factures ouvertes affichées en cards (montants calculés en direct depuis les articles, une facture ouverte n'a pas encore de montant figé) ; auto-bascule vers "Fermée" si 0 facture ouverte (cas réel de la base actuelle). `Facture.php` : fin du `window.open()`/fenêtre séparée, navigation AJAX inline comme le reste de l'appli, bouton Retour. **Vérifié bout en bout** : comparaison SQL sur toute la base réelle (2201 lignes, 0 divergence) + scénario complet créé de A à Z (client, facture, lettrage, attachement, paiement, clôture avec excédent) confirmant l'absence de doublon, données de test supprimées après vérification.
- ✅ **Fix affichage bizarre ~1s en changeant d'onglet de sous-module (dernière ligne "en face" de l'entête)** : cause - `table.ssm-table thead th { position:sticky }` (règle globale, toutes les DataTables de l'appli) + l'animation d'entrée de `#ssm_module_content` (`ssm-content-in-*`) applique un `transform` pendant toute sa durée (~350-600ms, `animation-fill-mode:both`) - un `transform` sur un ancêtre devient le "containing block" de tout descendant `position:sticky` (même piège déjà documenté dans ce fichier pour `position:fixed`/modales), le `<thead>` se recalcule alors par rapport au mauvais conteneur le temps de la transition, d'où le saut visuel qui se corrige seul une fois le transform retiré. Fix : `position:static !important` sur les `<thead> th` tant que la classe d'animation (entrée OU sortie) est posée - inutile de rester "collé" pendant une transition de page de toute façon. Aucun changement JS, uniquement CSS - à confirmer par l'utilisateur en conditions réelles (pas de navigateur/Playwright disponible dans cet environnement pour reproduire visuellement).
- ✅ **Fix header du règlement trop haut pour peu d'info + réordonnancement des totaux** (retour utilisateur, capture d'écran) : cause - `Header_reglement.php` s'enveloppait dans `.ssm-panel-header` (fond/padding/bordure propres), alors qu'il est injecté DANS un `.card-header` déjà stylé (`vente/Reglement.php`, `#header`) - chrome doublé (2 fonds/paddings empilés). Wrapper retiré, titre+statut+actions fusionnés sur une seule ligne (au lieu de 2), formulaire de report toujours caché par défaut. Ordre des totaux demandé : Factures → Paiements → Résultat (au lieu de Factures → Résultat → Paiements), le Résultat coloré vert/rouge selon Avance/Reste (classes `.ssm-stat-positive`/`.ssm-stat-negative`, déjà existantes - pas de nouveau CSS). Vérifié par curl sur un règlement Néant (résultat neutre) et un règlement avec avance réelle (résultat vert confirmé).
  - 🐛 **Régression introduite par ce même fix, corrigée immédiatement** (retour utilisateur : "je clique sur n'importe quel règlement, j'entre toujours sur le même") : `ssm_tab_cache_cle()` ne garde que le 1er segment de chemin (`/ReglementClient`, identique pour `/ReglementClient/index/1255` ET `/ReglementClient/index/1985`) - sans comparer l'URL COMPLETE de l'entrée en cache à celle visée, cliquer sur n'importe quelle ligne de la liste restaurait le premier règlement déjà visité cette session, peu importe l'id cliqué. Fix : ajout de `entreeCache.url === href` (même garde-fou déjà présent dans le handler `popstate` juste en dessous, qui lui n'avait pas cette faille). Les onglets de sous-menu et les liens "Retour à la liste" ne sont pas affectés (leur `href` EST l'URL canonique mémorisée) ; seuls les liens vers un enregistrement précis (`dt_btn_lien()`) en profitent correctement maintenant (jamais de faux-match entre 2 ids différents).

- 📖 **Lecture exhaustive préparatoire du module Achat (Fournisseur + Alimentation de stock + Dépense + Facturation Fournisseur)** — lecture seule, aucune modification, en préparation du futur chantier Phase 4 sur ce module (voir `SKILL_MODULE_VENTE.md` comme référence de méthode). Constats principaux :
  - **Aucune vue du module Achat n'est en `ssm-*`** (tout en Bootstrap/AdminLTE brut, comme Facturation/Bons avant leur refonte) et **aucune navigation AJAX** (`window.location.href` partout pour changer d'onglet ; 2 formulaires en vrai POST plein-page : `Nouveau_reglement.php`, `Reporter_reglement.php`) — bénéficient déjà du fix `dt_btn_lien()` (§40) mais rien d'autre.
  - **Toutes les tables à montant violent la règle CLAUDE.md des totaux** : total "Page" calculé en JS, total "Filtré" via un appel AJAX séparé (`total_total()` sur pratiquement chaque contrôleur : Depense, Alimentations, BonCommandes, ComparaisonAlimentation, FacturesFournisseur, ImpayeesFournisseur, ReglementsFournisseur, AvoirFournisseur) → double exécution de la requête à chaque `draw`, à fusionner en COUNT+SUM unique (pattern `Client::encaissement()`/`ClientAvance::data_reglement()`).
  - **Fournisseur** : table minimale (`fournisseur`: id+nom), tout le reste dans `fournisseur_roles` (facturation/alimentation/depense/avoir) ; activer `facturation` crée une `Entreprise` liée, activer `avoir` crée un `Client`+`ClientRole.avoir` fictif (fournisseur utilisable comme client pour avoir/bon carburant — mécanisme croisé à documenter si retouché).
  - **Alimentation de stock** (routée sous `stocks` mais dans le menu Achat) : une alimentation "brouillon" n'a pas de montant TTC figé tant qu'elle n'est pas clôturée (`AlimentationsController::cloture()`) ; peut générer un `ClientBon` de refacturation automatique à un client si `id_client` fourni à la clôture (prix d'achat majoré) — logique peu visible, à creuser si retouchée. Bug de nommage trouvé : `FournisseurFactureDescription::description_facture()` renvoie des alias totalement décalés (`produit`⇒TVA réelle, `prix`⇒quantité réelle, `entre`⇒prix réel) — fonctionne aujourd'hui par coïncidence positionnelle côté JS, piège pour qui retouche sans le savoir.
  - **Dépense** : pas de formulaire de saisie directe d'une dépense "sans facture" trouvé dans ce périmètre (seulement CRUD catégories) — les dépenses "avec facture" sont créées en miroir par `FacturesFournisseurController::modal()` (écrit aussi dans `achat_depense`).
  - **Facturation Fournisseur** = pendant quasi-exact du module Facturation Vente déjà refondu (`fournisseur_reglement`+`fournisseur_paiement` = lettrage, `fournisseur_avance` = paiement direct/règlement, `fournisseur_avoir` type Avoir/Déduction) — **jamais eu l'inversion de nommage Avance/Règlement que la Vente a eue**, donc aucun renommage requis ici. Le concept ATTACHEMENT existe déjà côté Achat mais réimplémenté indépendamment avec un schéma **moins générique** (colonne dédiée `id_paiement`/`id_reglement` par table plutôt que le couple `destination`/`id_destination` réutilisable côté Client) — à harmoniser si on veut un mécanisme d'attachement unique partagé vente/achat. `ReglementFournisseurController` (575 lignes) reproduit indépendamment `PieceRemplacementController` (paiement espèce/pièce/opération) et la logique de clôture avec excédent→avance / manque→reste (même principe que côté vente, code dupliqué).
  - Rapport complet (contrôleurs/modèles/vues ligne par ligne, bizarreries de code) produit et conservé dans cette session — à redemander/refaire si besoin au moment d'attaquer réellement le chantier (pas re-sauvegardé tel quel dans ce fichier, trop long ; les constats actionnables ci-dessus suffisent pour démarrer).

- ✅ **Sous-module Facturation Fournisseur (module Achat) traité en entier** (voir `SKILL_MODULE_VENTE.md` §42 pour le détail complet) : premier module hors Vente à recevoir le traitement Phase 4, sur la base de l'audit préparatoire du 24/07 (ci-dessus). Périmètre : Impayées/Factures/Règlements/Avoirs-Avances + détail d'un Règlement — équivalent quasi-exact du sous-module Facturation Vente (§40-41), copié-adapté (mêmes classes ssm-*, mêmes composants). Aucun renommage métier requis (Achat n'a jamais eu l'inversion Avance/Règlement de la Vente). Totaux Page/Filtré fusionnés en SQL sur les 6 tables à montant (violaient tous la règle CLAUDE.md jusque-là), 4 méthodes `total_total()` devenues inutiles supprimées. Nouveau `achat/portion/menu_achat.php` (sous-nav AJAX, thème `facturation_achat`) remplace l'ancien `Header.php`. 9 modales redessinées (icône/data-mode/field-icon/ssm-btn), dont 3 avec un piège CDN (`oLanguage.sUrl`) corrigé au passage. `Nouveau_reglement.php` gardé en soumission plein-page classique (restylé seulement, conversion AJAX jugée hors scope). Bug pré-existant repéré mais non corrigé (non exploitable en pratique, colonne non triable côté UI) : `FournisseurReglement::paiement_reglement()` plante en tri SQL par date, même défaut que l'ancien `ClientReglement::paiement_reglement()` avant sa refonte. Vérifié par `php -l` (24 fichiers) + curl (pages, fragments AJAX, tous les endpoints datatable avec les nouvelles clés de total, modales) — pas de test de mutation réelle (aucune logique métier touchée).

- ✅ **Renommage métier Facturation Fournisseur (module Achat) : Règlement→Lettrage, nouvel onglet Règlement (ex-Avance), onglet Avoirs/Déductions séparé** (voir `SKILL_MODULE_VENTE.md` §43 pour le détail complet) : même principe que le renommage Vente (§41), avec une différence propre à l'Achat — l'ancien onglet unique "Avoirs/Avances" mélangeait 2 concepts (`fournisseur_avance` = paiement direct, `fournisseur_avoir` = avoir/déduction reçu du fournisseur), désormais séparés en 2 onglets. `/ReglementsFournisseur`/`/ReglementFournisseur` relabellés "Lettrage" (URLs inchangées). Nouvelle route `/AvancesFournisseur` (nouveau contrôleur `AvancesFournisseurController`) = vue unifiée "Règlement" (`FournisseurAvance::data_reglement()`, copie fidèle de `ClientAvance::data_reglement()` adaptée — pas de source "carte" côté fournisseur, exclusion supplémentaire des paiements sourcés "avoir" en plus de "avance"). `/AvoirFournisseur` (URL inchangée, contrôleur simplifié) devient l'onglet "Avoirs / Déductions" pur (`FournisseurAvoir::data()`, nouvelle méthode — préserve un détail métier facile à rater : un Avoir peut aussi être "utilisé" via `id_remplacement`, pas seulement `id_reglement`/`id_paiement`). Ancienne `FournisseurAvance::data()` (combinait les deux) supprimée après vérification qu'aucun autre appelant n'existait. **Vérifié par comparaison SQL directe sur la base réelle** : total avoirs (81 lignes, 583 418,26 Dhs) = Non Utilisé (43 200,00) + Utilisé (540 218,26) exactement ; total paiements espèce/pièce/opération attachés (545 lignes, 100 335 170,85 Dhs) = intégralement retrouvé en "Lettré" côté Règlement (Non Lettré et Partiel à 0, cohérent avec l'état réel de la base — aucune avance directe non générée par clôture n'existe encore). Un bug trouvé et corrigé en cours de route : alias SQL de `FournisseurAvoir::data()` (`date_avoir`/`type`) ne correspondaient pas aux noms de colonnes attendus par le JS DataTable (`date_document`/`type_document`), cassant le tri — même piège que celui déjà rencontré plusieurs fois cette session avec `$this->datatable()`.

- ✅ **"Compte Fournisseur" (renommage) + nouvel onglet Grand Livre Fournisseur** (voir `SKILL_MODULE_VENTE.md` §44 pour le détail complet) : section menu principal "Fournisseurs" renommée "Compte Fournisseur" (`Layout.php`, même modèle que "Compte Client" côté vente), nouveau sous-menu 2 onglets (`achat/portion/menu_fournisseur.php` : Fournisseurs + Grand Livre). Nouveau `Fournisseur::livre()`/`solde()` (copie fidèle de `Client::livre()`/`solde()`) : Crédit = `fournisseur_facture` (toutes les lignes, pas de notion brouillon/id_cloture côté fournisseur contrairement au client), Débit = espèce/pièce/opération (mêmes 3 sources que `FournisseurAvance::data_reglement()`, pas de carte) **+ une branche "Avoir" propre à l'achat** (absente côté vente, décision utilisateur confirmée : un avoir attaché à un règlement réduit réellement la dette, daté par `COALESCE(date_decaissement, date_avoir)`). Nouveau contrôleur `GrandLivreFournisseurController`, nouvelle route, `Fournisseurs.php`/`Nouveau_fournisseur.php` redessinés en ssm-*. Bonus fix trouvé en relisant `Layout.php` : `AvancesFournisseur` (créé en §43) manquait des `actifs` du bloc Facturation, ajouté au passage. **Vérifié par comparaison SQL directe sur la base réelle** : crédit total (100 557 466,37 Dhs) = somme exacte des 677 factures valides (une facture anomalie avec `date_facture='0024-08-22'`, année 24 au lieu de 2024, correctement exclue par le filtre de date — donnée à corriger côté saisie, pas un bug) ; débit total (102 281 636,48 Dhs) = somme exacte espèce+pièce+opération (comptant aussi bien les paiements attachés à un règlement QUE les avances directes, ces dernières absentes de `fournisseur_paiement`) + avoirs attachés. `php -l` sur les 10 fichiers touchés/créés, chaque page/fragment/endpoint testé par curl.

- ✅ **Achat : Alimentation de stock (Alimentations/Bons de Commande/Comparaison) + Dépense traités** (voir `SKILL_MODULE_VENTE.md` §45 pour le détail complet) : dernier volet du module Achat (avec Fournisseur/Grand Livre §44 et Facturation §42-43, tout le module Achat est maintenant en Phase 4). Demande explicite : supprimer tout l'ancien pattern `total_total()` (HTML/`<script>` renvoyé en AJAX séparé) au profit du JSON fusionné partout, et liberté totale sur l'agencement visuel/boutons (pas d'obligation de respecter l'ancien design, seule la logique métier devait rester identique). Totaux Page/Filtré fusionnés en SQL sur les 5 méthodes concernées (`StocksAlimentation::alimentation_between()`, `BonCommande::data()`/`comparaison()` (7 colonnes chiffrées, la plus complexe du lot)/`data_description()`, `Depense::data()`) — les 4 `total_total()` correspondants supprimés. Nouveau `stocks/portion/menu_alimentation.php` (sous-nav AJAX, remplace les 3 boutons `window.location.href`). Un vrai bug pré-existant trouvé et corrigé au passage (indépendant de la demande, repéré en touchant la ligne juste à côté) : `BonCommande::data_description()` faisait `$sql . $this->datatable();` (concaténation sans affectation, le tri/la pagination du tableau produits d'un bon de commande n'avaient jamais été appliqués) — corrigé en `$sql .= ...`. Bug déjà documenté dans l'audit du 24/07 (`montant_ttc_hors` dans `alimentation_between()`, TVA calculée puis jetée) **non corrigé** — hors périmètre de cette demande, laissé tel quel. **Vérifié par comparaison SQL directe sur la base réelle** : `montant_demande_filtre` (75 695 608,928278 Dhs, 624 lignes) = somme exacte de `stocks_bc_description` ; total du détail d'un bon de commande réel (id 382 : 369 302,40 Dhs) = somme exacte de ses 2 lignes. `php -l` sur les 13 fichiers touchés/créés, les 5 pages + leurs fragments AJAX + endpoints datatable/modal testés par curl (200, pas de Fatal).

- ✅ **3 retours utilisateur après §45 (Alimentation/Dépense) + fix global focus auto-avance** (voir `SKILL_MODULE_VENTE.md` §46 pour le détail) :
  1. **Bug JS trouvé et corrigé dans la fonction PARTAGÉE `ssm_champ_est_actif()`** (`ajax.js`) : choisir une option Select2 (constaté sur "Stock" dans la modale "Ajouter Article de Stock") renvoyait le focus en arrière sur le tout premier champ du formulaire au lieu d'avancer au suivant. Cause : au moment où Select2 émet `select2:select`, son menu vient déjà de se refermer (`isOpen()` faux) et le focus réel navigateur est reparti sur le conteneur visible du widget (`.select2-selection`), pas sur le `<select>` caché — plus aucun champ ne passait alors pour "actif", et `ssm_champ_naviguer()` retombait par défaut sur l'index 0. Corrigé à la source unique déjà en place (demande explicite : "un principe dans une fonction JavaScript pour que corriger ça devienne facile") — bénéficie automatiquement à toute modale de toute l'appli (vente comprise), aucune modification par module nécessaire.
  2. **Dépense : bouton "Ajouter Dépense" manquant** — jusque-là `DepenseController` ne gérait que le CRUD des catégories, aucun formulaire n'existait pour créer une ligne `achat_depense` directe. Nouvelle action `DepenseController::depense()` + modale `Nouvelle_depense.php` : crée une dépense `type_depense='sans_facture'`, `source='Direct'`, `id_compte=0` (distinct des dépenses `avec_facture` créées en miroir par la Facturation Fournisseur, jamais modifiables/supprimables depuis cet écran pour ne pas désynchroniser `fournisseur_facture` — `Depense::data()` expose un flag `modifiable` basé sur `type_depense`). CRUD complet testé (insert/update/fetch/delete) sur la base réelle, ligne de test supprimée après.
  3. **Alimentation (détail) : boutons "Ajouter Article de Stock"/"Ajouter Hors Stock" réorganisés** sur une ligne dédiée, le premier à l'extrémité gauche, le second à l'extrémité droite (au lieu d'être mélangés aux affichages en lecture seule "Articles de Stock : X"/"Articles hors Stock : Y") — demande explicite, logique/routes JS inchangées (`#modal`/`#description`).

- ✅ **`Header_alimentation.php` compacté** (retour utilisateur : header trop haut - 6 informations + 2 boutons d'ajout + 1 action + un titre, tout étalé sur plusieurs lignes) : titre + les 2 boutons "Ajouter" + Modifier + l'action (Clôturer/Annuler/Supprimer) regroupés sur une seule ligne (`.ssm-panel-toolbar`) ; les 6 informations (Date/Référence/Livreur/Montant HT/Montant TVA/Montant TTC pour une alimentation avec facture, Date/Référence/Livreur/Montant sinon) alignées sur une seule `.ssm-stat-row`. Nouveau : **Montant HT = Montant TTC − Dont TVA** (demande explicite). Supprimés : les 2 champs en lecture seule "Articles de Stock"/"Articles hors Stock" avec leur valeur chiffrée (redondants avec le contenu du tableau juste en dessous) — les boutons de bascule entre les 2 tableaux (`#alimentations`/`#descriptions`) conservés, seulement débarrassés de leur valeur affichée. Bouton "Retour" (flèche à gauche du titre) supprimé — redondant avec le lien "Retour à la liste des alimentations" déjà présent en haut de la page (ajouté au chantier §45). Vérifié par curl sur une alimentation réelle avec facture (id 545 : 369 302,40 + 36 930,24 = 406 232,64 ✓) et une sans facture (id 1).

- ✅ **Alimentation (détail) : tableau Stock/Hors Stock unifié + retouches header + 2 bugs corrigés** (voir `SKILL_MODULE_VENTE.md` §47) : demande explicite de ne plus faire basculer entre les 2 vues ("on affiche les deux à la fois") — nouvelle `StocksAlimentation::articles()` (UNION ALL `stocks_mouvement`/`fournisseur_facture_description`, colonne `stock` à `---` pour une ligne hors stock, `tva` à `---` pour une ligne stock, discriminant `type_ligne` préservant le routage JS existant via le paramètre `title` de `dt_btn()`) remplace l'ancien double-affichage de `AlimentationsController::datatable()`. `Header_alimentation.php` : Clôturer redevenu un bouton direct (pas de dropdown-confirm, puisqu'il ouvre déjà une modale — Annuler/Supprimer gardent le dropdown-confirm), icône Modifier corrigée (`fa fa-pencil-square-o` invisible → `fas fa-edit`), date de l'alimentation déplacée sur la ligne du titre ("Fournisseur - Référence / DateTime"), les 2 boutons "Ajouter" déplacés en bas de header (gauche/droite), lignes de bascule supprimées. **2 bugs pré-existants trouvés et corrigés au passage** (indépendants de la demande) : (1) `FournisseurFactureDescription::description_facture($id)` avait ses alias SQL complètement décalés (`produit as stock, prix as entre, qte as prix, tva as produit`) — Prix Unitaire et Quantité s'affichaient inversés dans l'ancien tableau "Articles hors Stock", ne fonctionnait que par coïncidence positionnelle côté JS ; méthode corrigée bien que devenue dead code (plus appelée) après l'unification, gardée fixée par prudence. (2) `DepenseController::depense()` : cliquer sur "Ajouter Dépense" pour simplement OUVRIR le formulaire vide envoyait `id=0`, et le code insérait immédiatement une ligne avec des champs non définis (`Warning: Undefined array key` + `Fatal error` sur la contrainte NOT NULL de `id_categorie`) — corrigé pour suivre le pattern standard à 4 cas du projet (ajout = `id==0` **ET** `montant` présent, pas `id==0` seul). Bonus : tag HTML mal fermé dans `stocks/modal/alimentation.php` (`<h3>` au lieu de `</h3>` après le lien "Accéder" affiché après un ajout réussi). **Vérifié sur la base réelle** : `articles()` testé sur une alimentation avec facture réelle (id 545, 2 lignes stock) + une ligne hors-stock de test insérée/supprimée (confirmé `stock:"---"` + `tva` réelle affichés ensemble) ; `Depense::depense()` testé avec un vrai insert (id 184, supprimé après) confirmant qu'ouvrir le formulaire vide n'insère plus rien alors qu'une vraie soumission fonctionne toujours. `php -l` sur les 6 fichiers touchés.

- ✅ **Renommage "Alimentation de stock" → "Gestion des Bons (Dépotage)" + intégration de Dépense en 4e onglet** (voir `SKILL_MODULE_VENTE.md` §50) : demande explicite - libellé du menu principal (`Layout.php`) renommé, le mot "Alimentation" remplacé par "Bon de Livraison" partout côté UI (titres, boutons, messages `ssm_notify`, breadcrumbs) dans `AlimentationsController.php`, `Alimentations.php`, `Alimentation.php`, `modal/alimentation.php`, `modal/cloture.php`, `Comparaison.php`, `Mouvements.php` - **URLs/routes/noms de classes/valeur `source` en base (`stocks_mouvement.source='Alimentation'`) volontairement INCHANGÉS** (règle établie tout au long de cette session pour tout renommage métier). Onglet "Dépense" retiré du menu principal et intégré comme 4e onglet du sous-menu module (`menu_alimentation.php`), séparé visuellement des 3 premiers onglets (Bon de Commande/Bon de Livraison/Comparaison) par un nouveau séparateur `.ssm-module-nav-sep` (components.css, ignoré par le positionnement du thumb glissant et la navigation clavier car ne portant pas la classe `.ssm-module-nav-item`). Ordre final : Bon de Commande - Bon de Livraison - Comparaison | Dépense. `Depense.php` (module achat) inclut désormais ce même menu partagé (`$type='depense'`) - fonctionne malgré le dossier `achat/` différent de `stocks/` car `include()` résout par rapport au cwd (`public/`), pas au dossier du fichier appelant, comme les 3 autres vues du module. **Vérifié en profondeur par curl sur le serveur réel** : les 5 pages (Alimentations/BonCommandes/ComparaisonAlimentation/Depense/détail d'une alimentation) répondent 200 sans Fatal/Warning, les 4 onglets + séparateur apparaissent dans le bon ordre sur chacune avec le bon onglet actif, la navigation AJAX (header `X-Ssm-Nav`) fonctionne sur les 4 pages, les 4 endpoints datatable répondent correctement, les titres de modales (ajout/modification/clôture) affichent bien "Bon de Livraison", le menu principal n'a plus qu'une seule entrée "Gestion des Bons (Dépotage)" (aucun résidu de l'ancien "Alimentations Stock"/"Dépenses" séparé).

- ✅ **`ssm_modal_auto_close()` : fermeture automatique qui ne s'armait jamais sur une modale dont le 1er champ est un Select2** (retour utilisateur, modale "Ajouter Article de Stock") : la barre de fermeture automatique s'armait immédiatement dans le même tick que `ssm_focus_ajout_si_besoin()` (ouverture + réessais de focus du 1er champ Select2, cf. commentaire existant "Bootstrap reprend le focus sur la modale a plusieurs reprises... parfois pres de 2 secondes") - ces reprises de focus sont de VRAIS événements navigateur (contrairement aux `.trigger()` déjà filtrés), non détectés par le test `e.originalEvent` existant, et annulaient la fermeture avant même que l'utilisateur touche à quoi que ce soit. Fix : armement des écouteurs d'annulation différé de 400ms (la barre/le minuteur de fermeture démarrent, eux, toujours immédiatement) - laisse la séquence d'ouverture automatique se stabiliser avant de commencer à détecter une vraie interaction utilisateur. Fonction partagée (`ajax.js`) : bénéficie à tous les appels existants (`Clients.php`, `Nouvelle_facture.php`, `ClientBon.php`...) sans les modifier.

- ✅ **Alimentation : select Stock vide après choix du Produit + auto-fermeture de la modale "Ajouter Article de Stock"** (voir `SKILL_MODULE_VENTE.md` §49) : (1) choisir un Produit avance automatiquement le focus (auto-navigation partagée) sur le select Stock, mais ses options sont peuplées en AJAX (cascade Produit→Stock) et arrivent APRÈS l'ouverture automatique du menu par le focus - l'utilisateur voyait un menu vide et devait recliquer. Nouvelle fonction partagée `ssm_select2_rouvrir_si_ouvert($champ)` (`ajax.js`) : si le select2 ciblé est déjà ouvert au moment où les options arrivent, ferme/rouvre pour forcer le re-rendu - appelée dans `remplir_stock()` de `stocks/modal/mouvement_alimentation.php`. (2) Après "Ajouter" dans cette modale, le formulaire revenait vierge (prêt pour un nouvel article) sans jamais se refermer tout seul - ajout de la barre de fermeture automatique déjà standard ailleurs (`ssm_modal_auto_close`, `stocks/Alimentation.php`), annulée dès que l'utilisateur retouche la modale ; en modification (pas ajout), fermeture directe sans barre (rien à ré-afficher). Vérifié par curl : insertion réelle d'un mouvement de test sur l'alimentation 545 confirmée en base (id 13774, supprimé après), réponse re-render bien un formulaire vierge (le cas qui justifie l'auto-fermeture).

- ✅ **Alimentation : inversion des boutons "Ajouter Article de Stock"/"Ajouter Hors Stock"** (retour utilisateur) : ordre inversé sur la ligne du bas du header, "Hors Stock" à gauche, "Article de Stock" à droite (`Header_alimentation.php`) - vérifié par curl sur l'alimentation facture réelle id 545.

- ✅ **Alimentation : suppression du clic "Accéder"/message de succès, fermeture directe de la modale** (voir `SKILL_MODULE_VENTE.md` §47) : après §47, l'utilisateur a signalé ne pas vouloir du message "ajoutée avec succès" + lien "Accéder" à cliquer (qui en plus rechargeait la page en dur) - même chose pour "Modifier". Désormais : dès "Enregistrer", la modale se ferme automatiquement et (a) pour un ajout, navigation AJAX directe (`ssm_nav_ajax`, pas de rechargement dur) vers la page de l'alimentation créée ; (b) pour une modification, le header se rafraîchit sur place (`load_header()`), sans quitter la page. Implémenté via un marqueur caché commun `#alimentation_resultat` (`data-modifie`/`data-id`) renvoyé par `stocks/modal/alimentation.php` à la place de l'ancien texte visible - `AlimentationsController::modal()` renseigne désormais `$ajouter` (l'id) aussi bien à l'ajout qu'à la modification (avant : seulement à l'ajout). Vérifié par curl sur la base réelle (ajout id 548 confirmé `data-modifie="0"`, puis modification du même id confirmée `data-modifie="1"` + valeurs bien mises à jour en base) - ligne de test supprimée après.

- ✅ **Bug critique corrigé : DataTable Caisse plantait (Fatal error PDO)** : `CaissePsFlux::libelle()` (`case 'Reglement:client'`/`'Reglement:fournisseur'`) indexait le résultat de `findone()` sans vérifier qu'il existait - une ligne orpheline dans `compte_ps_caisse_flux` (paiement client source supprimé sans passer par `delete_flux()`) faisait planter TOUT le datatable dès que la période filtrée l'incluait (`findone(null)` → `SELECT * from client_reglement where id=` → erreur SQL fatale, non rattrapée). Garde-fous ajoutés (retour `"(paiement/règlement supprimé)"` au lieu de planter) dans `CaissePsFlux::libelle()` **et** `CompteBancaireFlux::libelle()` (même schéma non protégé trouvé au passage, zone client + cas `Impaye`, pas encore déclenché faute d'orphelins côté banque mais latent). 2 lignes orphelines réelles trouvées (id 9845/9846, `compte_ps_caisse_flux`, paiements client 2300/2301 supprimés le 24/07) et supprimées via `CaissePsFlux::delete_flux()` (pas de SQL brut, pour recalculer correctement le solde de caisse en cache : 41 239,17 → 41 079,17). Vérifié par curl : `/Caisse/datatable` renvoie un JSON valide (99 lignes) sans Fatal/Warning après le fix.

- ✅ **Module Trésorerie : Caisse redessiné en Phase 4** (voir `SKILL_MODULE_VENTE.md` §51) : demande explicite ("à toi l'honneur"). `Caisse.php` entièrement converti ssm-* (`ssm-panel-toolbelt` réglage+plein écran, `ssm-panel-toolbar` filtres+dropdown Ajouter, `ssm-table-toolbar` imprimer/exporter génériques - remplace le formulaire caché dédié `/Excel/caisse`), `header_caisse.php` en `.ssm-stat-row` (solde coloré en rouge si négatif - logique déjà présente mais jamais utilisée dans l'ancien code), les 3 modales (`operation_caisse.php`, `transfert_caisse.php`, `nouvelle_operation.php`) avec icône/`data-mode`/field-icons/`ssm-btn`. Total Page/Filtré fusionné en SQL dans `CaissePsFlux::caisse_between()` (`debit_filtre`/`credit_filtre` dans le même JSON que la DataTable, `ssm_table_totaux_multi()`) - remplace l'ancien round-trip séparé `/Caisse/total_total` (contrôleur + vue supprimés) ; au passage, l'ancien `count($query->fetchAll())` (rapatriait toutes les colonnes de toutes les lignes juste pour compter) remplacé par un vrai `COUNT(*)` SQL - gain de perf réel en plus de la fusion demandée. Code mort supprimé : `CaisseController::export()`/`telechargement()` (données factices codées en dur "Dupont/Martin/Durand", sans rapport avec le vrai export `/Excel/caisse`). **2 bugs trouvés et corrigés en testant** : (1) titres de modale "Ajouter"/"Modifier" inversés dans `operation_caisse.php`/`transfert_caisse.php` - comparaison `$type==0`/`$id_destination==0` avec une valeur par défaut `""`, qui donnait `true` en PHP7 mais **`false` en PHP8** (régression de compat PHP8, le serveur tourne en 8.0.30) - corrigé en comparant `$id==0` comme le reste de l'appli ; empêchait aussi le focus auto du 1er champ (`ssm_focus_ajout_si_besoin()` cherche "ajout/nouveau" dans le titre). **Vérifié en profondeur par curl sur le serveur réel** : page + header + datatable (JSON avec `debit_filtre`/`credit_filtre`) sans Fatal/Warning ; les 3 modales testées avec un cycle complet ajout→vérification en base (ligne + flux miroir)→suppression→vérification du nettoyage, pour Opération Caisse (id 51) et Transfert (id 1371) - tout supprimé après test.

- ✅ **Module Trésorerie : Banque redessiné en Phase 4** (`CompteBancaire`/`OperationEnAttente`/`PieceImpayee`, voir `SKILL_MODULE_VENTE.md` §51) : les 3 pages passées en ssm-* (`ssm-panel-toolbelt` réglage+plein écran, `ssm-panel-toolbar`, `ssm-table-toolbar` génériques - remplace les formulaires cachés dédiés vers `/Excel/banque`), `portion/menu.php` (compte bancaire + solde + les 3 boutons de navigation) restylé en `ssm-panel-toolbar`/`ssm-stat-row`/`ssm-btn` - **décision assumée : la navigation entre les 3 pages reste en POST de page complète (mécanisme existant, fonctionnel) plutôt que convertie en AJAX `ssm-module-nav`** ; ces 3 pages sont fortement couplées (sélection compte bancaire + solde partagés, sous-tableau "pièces de remise", routes croisées comme `#nouveau` qui pointe toujours vers `/OperationEnAttente/modal` depuis les 3 pages) et une conversion de la navigation aurait exigé de faire persister cet état hors de la zone remplacée par l'AJAX - un chantier distinct, plus risqué à valider sans navigateur, volontairement pas traité dans cette passe pour ne pas fragiliser un module financier en production. Total Page/Filtré fusionné en SQL dans les 3 méthodes concernées (`CompteBancaireFlux::data()`/`en_attente()`, `EnvoiRemisePiece::data_impaye()`) - remplace les anciens `count($query->fetchAll())` par un vrai `COUNT(*)` (gain de perf) et les round-trips séparés `/CompteBancaire/total_total` (supprimé) et `/OperationEnAttente/total_total` (supprimé, jamais réellement fonctionnel côté `PieceImpayee.php` : appelait `load_total_total()`, une fonction qui n'y était jamais définie - "Total Filtré" y était déjà cassé silencieusement avant ce chantier). **4 bugs trouvés et corrigés en testant** (même famille que le bug Caisse du même jour) : (1) `CompteBancaireFlux::libelle()` (cas `Reglement`/zone `client`, et cas `Impaye`) indexait `findone()`/`$piece` sans vérifier leur existence - orphelins possibles si un paiement/une pièce liée a été supprimé ; (2) `EnvoiRemisePiece::data_impaye()` : `$id_piece` pouvait être `NULL` (jointure orpheline vers une pièce supprimée), interpolé tel quel dans une requête SQL → erreur fatale ("id_piece= and statut=-1"), reproduite et corrigée avec un vrai orphelin trouvé en base ; (3)/(4) `CaisseController::operation_banque()` et `CompteBancaireController::annuler()` et `OperationEnAttenteController::modal()` (branches suppression) : `find_attribut(...)[0]['id']` sans vérifier que le tableau n'était pas vide (cas normal si l'opération n'a pas de ligne caisse liée) - warning "Undefined array key 0" à chaque suppression d'une opération banque pure, corrigé aux 3 endroits avec le même garde-fou. **Vérifié en profondeur par curl** : les 4 pages (Caisse/CompteBancaire/OperationEnAttente/PieceImpayee) + leurs datatables (avec les vrais cas `id_remise=0` et `id_remise=1`, ce dernier révélant l'orphelin réel) + `/CompteBancaire/solde` répondent sans Fatal/Warning ; cycle complet ajout→vérification en base→suppression testé pour une opération banque directe (id 8626 puis 8627, montant 100, supprimé après) confirmant l'absence de warning après le fix.

- ✅ **Module Coffre : sous-groupe Chèque/LCN traité en Phase 4** (Piece/PieceEnvoi/PieceRemplacement/ArchivePiece, voir `SKILL_MODULE_VENTE.md` §52) : demande explicite ("n'importe quel principe déjà fait doit être appliqué... on fait tout cumulé"), avec un exemple précis donné par l'utilisateur - le bouton "Remplacer" (Piece.php) qui rechargeait toute la page. **Bug corrigé exactement comme demandé** : `.remplacer`/`.envoyer` (Piece.php) utilisaient un vrai `<form>`+`.submit()` vers `PieceController::nouveau_remplacement()`/`nouveau_envoi()`, qui faisaient un `$this->redirect(...)` serveur (rechargement complet) - remplacé par un POST AJAX (`$.post`) qui renvoie du JSON (`{"id":...}`), suivi d'un `ssm_nav_ajax()` vers la page créée. **Même principe étendu à toutes les redirections serveur du même sous-groupe trouvées en auditant** : `PieceRemplacementController::cloture()`/`delete()`/`report()` (déclenchés par de vrais `<a href>`/`<form>` dans `header_remplacement.php`) - convertis en actions AJAX pures (`$this->param` au lieu de `$GLOBALS['paths']`), pilotées par des handlers JS déjà à moitié présents mais jamais branchés (`#cloturer`/`#annuler_cloture` existaient déjà dans `Remplacement.php` mais ciblaient des ids absents du HTML). Sous-menu `menu_piece.php` converti en `ssm-module-nav` (AJAX, remplace les 4 boutons `window.location.href`). `Piece.php`/`Remplacements.php`/`Remplacement.php`/`Archive.php`/`Envois.php` entièrement redessinés en ssm-* (toolbelt, toolbar, table-toolbar) ; `EnvoiBanque.php` (détail d'un envoi, 945 lignes, logique remise/pièces complexe) traité de façon plus prudente : coquille extérieure ssm-* appliquée (toolbelt, wrapper `#ssm_module_content`) **sans restructurer la logique interne** (même arbitrage que Banque §51, pas de navigateur pour valider visuellement une zone aussi dense). Total Page/Filtré fusionné en SQL dans `Piece::data()`/`archive()`, `CoffreRemplacement::data()`, `EnvoiBanque::data()` (remplace un `count($fetchAll())` + une somme calculée en PHP par un vrai `SUM()` SQL) - 3 anciens `total_total()` supprimés. Modales `nouvelle_piece.php`/`nouveau_envoi.php`/`nouvelle_remise.php`/`info_envoi_nouvelle.php` avec icône/`data-mode`/field-icons/`ssm-btn`. **Bugs trouvés au passage** (même famille "findone() non gardé sur ligne possiblement supprimée" que Caisse/Banque le même jour) : `Piece::source()` (cases Remplacement/PieceRemplacement/Reglement/Carburant) + variable `$id_mode`/`$selected` non définie dans 3 fichiers (warning PHP8, pré-sélection qui ne s'appliquait jamais). **Vérifié en profondeur par curl** : les 5 pages + `EnvoiBanque.php` détail répondent sans Fatal/Warning ; les 4 datatables renvoient un JSON valide avec totaux fusionnés ; cycle complet testé pour Remplacer (id 18, 19), Clôturer/Annuler la clôture (id 15, remis à l'état initial), Reporter (id 15, date remise à NULL après test), Annuler le remplacement (créé puis supprimé, `id_remise` de la pièce bien remis à 0) - toutes les données de test nettoyées après vérification.

- ✅ **Module Coffre : sous-groupes CMI + Carte Fournisseur traités en Phase 4** (voir `SKILL_MODULE_VENTE.md` §53), suite directe de §52. Insight clé : CMI et Carte Fournisseur partagent le même modèle `Telecollecte` (discriminé par `id_fournisseur`) - une seule passe de fusion Total Page/Filtré sur `data_cmi()`/`data_comparaison()`/`data_ticket()` profite aux deux sous-groupes, mais la suppression des 6 `total_total()` (3 CMI + 3 Carte Fournisseur) a quand même été faite contrôleur par contrôleur. `CompteBancaireFlux::tom_card()` (Recouvrement/Suivi, propre à Carte Fournisseur) fusionné à part. Sous-menus `cmi/portion/menu.php`/`carte_fournisseur/portion/menu.php` convertis en `ssm-module-nav` (coquille "Tikcets"→"Tickets" corrigée au passage). Les 6 pages Remises/Comparaisons/Ticket (CMI + Carte Fournisseur) + `carte_fournisseur/suivi.php` (jamais retouché avant) redessinées en ssm-*, 4 modales avec icône/`data-mode`/field-icons/`ssm-btn` (logique JS interne préservée telle quelle, notamment la bascule Recharge/Location TPE). Bug trouvé au passage : `CmiRemisesController` (2 branches) indexait `find_attribut(...)[0]['id']` sans garde - corrigé. **Vérifié par curl** : 7 pages + 7 datatables (avec params DataTables server-side complets) sans Fatal/Warning, totaux fusionnés confirmés. Aucune donnée de test créée (vérification en lecture seule). Le module Coffre est désormais entièrement traité en Phase 4 (Chèque/LCN, CMI, Carte Fournisseur, Vignette).

- ✅ **Module Stock traité en Phase 4 + citernes en_stock + Service renommé "Base Article" sous Réglages** (voir `SKILL_MODULE_VENTE.md` §54). Stock : `menu_stock.php` en `ssm-module-nav`, `Produits.php`/`Mouvements.php`/`Transferts.php`/`Transfert.php` (détail) redessinés en ssm-*, Total Page/Filtré fusionné (`StocksProduits::data()`/`data_mouvement()`/`mouvement_between()` - le "Total Filtré" n'existait pas du tout avant côté Stock). Ajout demandé : bandeau `.ssm-kpi-row` au-dessus du tableau produits montrant l'`en_stock` de chaque citerne carburant (`carburant_citerne.stock`), datatable produits lubrifiants inchangé comme demandé. Bouton "Ajouter" retiré de Stock (création déplacée vers Base Article). Bug de navigation en dur trouvé et corrigé au passage (`Transfert.php`/`Header_transfert.php`, même famille que tout le chantier Phase 4) + `toastr.error` utilisé par erreur pour des messages de succès. Service → "Base Article" : relocalisé sous Réglages (Layout.php), nouveau contrôleur minimal `BaseArticleProduitController` qui réutilise directement `/Produits/datatable`+`/Produits/modal` (pas de duplication de la logique CRUD), 2 registres de routing à mettre à jour (`namespace_resolve()` ET `left_bar()`, indépendants). Vérifié par curl (pages + nav AJAX + datatables), aucune donnée de test créée.

- ✅ **Module Statistique > Numérique traité en Phase 4** (voir `SKILL_MODULE_VENTE.md` §55). `Numerique.php` reconstruit sur `.ssm-tabs`/`.ssm-tabs-link` (3 onglets internes Résultat/Ventes/Achats, PAS `ssm-module-nav` car même page/mêmes URL), `ssm-panel`/`ssm-kpi-row`/`ssm-stat-row--fin`, CDN externe `oLanguage.sUrl` retiré via `init_datatable()`. **Fusion Total Page/Filtré** (le module n'en avait AUCUNE avant) : `Statistiques::vente()`/`achat()` renvoient désormais `qte_filtre`/`ht_filtre`/`ttc_filtre` dans leur propre JSON, `vente_total()`/`achat_total()` supprimées (ancien mécanisme séparé et fragile, couplé à l'onglet Résultat). Bug métier trouvé en fusionnant : `achat_total()` utilisait le WHERE de `vente_total()` par erreur - le "Total Filtré" achat ne portait pas sur le même jeu de données que le tableau, corrigé de facto par la fusion. Autres bugs corrigés : variable variable `$$type_vente`, ~10 `find_attribut()[0]['montant']` non gardés contre NULL, colonne "Action" morte (bouton jamais câblé) supprimée, code mort nettoyé dans le contrôleur. Hors périmètre (signalé, non traité) : doublon mort `home/StatistiqueController.php`, `Graphique.php` (design ad hoc séparé, pas touché car non nommé par l'utilisateur). Vérifié par curl (page + 4 endpoints + recherche), aucune donnée de test créée.

- ✅ **Module Statistique > Graphique : vraie refonte + nouveaux onglets Produit/Service** (voir `SKILL_MODULE_VENTE.md` §56). Déclencheur : les règles CSS globales de cette session avaient cassé le `<style>` ad-hoc de cette page (premier cas où le CSS global casse un module pas encore traité, plutôt que l'inverse). 4 onglets `.ssm-tabs` (Carburant/Comparaison/Produit/Service, chacun avec ses propres filtres). Tous les graphiques unifiés sur Chart.js (retrait de JustGage + du pie fait main `afficher_cercle()`), fix Chart.js v2→v3 (`scales.yAxes`→`scales.y`), palette couleur unique lue dynamiquement via `getComputedStyle(--accent/--ok/--warn/--bad)` (theme-aware, au lieu de 3+ palettes hex fixes), retrait des blocs `plugins.datalabels` inertes (plugin jamais chargé). Backend : `mensualite()` (12 requêtes) supprimée au profit de `statistique_annee_optimisee()` généralisée par `type_vente` ; ~110 lignes de code mort supprimées ; bugs corrigés (`$colors[$i]` non gardé, divisions par zéro, `find_attribut()[0]` non gardés ×12, `findone()` non gardé). Nouveau backend Produit/Service : `Statistiques::par_produit()` (top 10 + "Autres", achat/marge seulement pour Produit puisque les services n'ont pas de prix d'achat), 2 actions minces réutilisant le modèle existant - décision explicite de ne PAS ajouter d'"encaissement par mode de paiement" pour Produit/Service (le circuit `carburant_compte`/`Bon` est lié à une session de caisse, pas à une ligne produit, mélanger les catégories n'a pas de sens métier). Vérifié par curl (page + 6 endpoints) + `node --check` sur le JS extrait ; **limite assumée : pas de navigateur, rendu visuel des charts non vérifiable à l'œil**. Aucune donnée de test créée.

- ✅ **Statistique > Graphique : réorganisation en 2 onglets principaux × 3 sous-onglets, puis correction en 3 onglets principaux** (voir `SKILL_MODULE_VENTE.md` addendums §56 bis + §56 ter). 1er retour utilisateur (§56 bis, depuis corrigé) : Carburant/Lubrifiant & Service en 3 sous-onglets Général(doughnut %)/Évolution/Comparaison. 2e retour (§56 ter, structure définitive) : en réalité **3 onglets PRINCIPAUX** - **Général** (nouveau, séparé - 1 seul graphique ligne Achat/Vente toute activité, **données 100% aléatoires** pour l'instant, méthode de calcul à définir plus tard par l'utilisateur, note "provisoire" affichée) / **Carburant** / **Lubrifiant & Service** (fusion Produit+Service, catégorie `'ps'`). Carburant et Lubrifiant & Service gardent chacun 3 sous-onglets mais avec un sens différent : **Général** = filtre propre (du/au/fixateur/taxe pour Carburant, du/au/taxe pour L&S) + KPI + les graphiques détaillés de la période (pas de doughnut %, idée abandonnée) ; **Évolution** = filtre `année` seul, découplé, nouveau endpoint dédié `evolution()` (délègue à `statistique_annee_optimisee()` déjà généralisée) ; **Comparaison** = logique inchangée, juste restylée (toolbar encadrée dans `.ssm-chart-card` au lieu de flotter). Bug attrapé par curl en cours de route : `statistique_ps()` plantait (`Undefined array key "annee"` puis SQL invalide) car son formulaire "Général" n'envoie plus d'année - corrigé en retirant le calcul de mensualités de `statistique_type()` (devenu inutile, `evolution()` s'en charge). CSS `.ssm-chart-zone--attente` du 1er essai retirée (plus utilisée). Vérifié par curl (page + tous les endpoints, y compris le bug puis sa correction) + `node --check`, aucune donnée de test créée.

- ✅ **Statistique > Graphique : bug systémique tooltip/légende décalés sur tous les charts + Select2 multiple hors thème** (voir `SKILL_MODULE_VENTE.md` addendum §56 quinquies). Root cause : les 9 onglets `.ssm-tabs` imbriqués chargent/rendent leurs graphiques dès `document.ready()` alors qu'un seul est visible à la fois - Chart.js construit sur un canvas caché (`display:none`) calcule une géométrie fausse qui reste périmée même une fois l'onglet affiché (tooltip sur le mauvais point, clic légende togglant le mauvais dataset) - pas un bug de données/ordre de tableaux (déjà vérifiés alignés). Fix : handler délégué `shown.bs.tab` global appelant `chart.resize()` sur tout le registre `charts{}` à chaque changement d'onglet. Bonus : Vente/Achat d'une même année en légende partageaient la même couleur (masquer l'un rendait l'autre quasi identique) - nouvelle fonction `avec_alpha()`, Achat éclairci. Select2 **multiple** (filtre "Années") n'était couvert par aucune règle de thème (seul `--single` l'était) - hauteur non alignée + tags bleu par défaut - nouveau bloc CSS dédié.

- ✅ **Nettoyage `header.php`/`footer.php` : retrait des plugins AdminLTE morts + CDN à jour** (voir `SKILL_MODULE_VENTE.md`, section dédiée après §56 quinquies). Sauvegarde faite en premier (`backup_layout_25072026/*.bak`). Audit via agent Explore (grep systématique de chaque plugin contre tout `app/views`+`public/js`) avant toute suppression : 12 plugins confirmés morts retirés (ekko-lightbox, bs-custom-file-input, bootstrap-switch, bs-stepper, dropzone, bootstrap4-duallistbox, bootstrap-colorpicker, tempusdominus-bootstrap-4, daterangepicker, inputmask+moment, filterizr, gaugeJS, `charts.css` CDN, jquery-ui CDN) ; conservés car confirmés utilisés : `adminlte.min.css/js` (small-box/callout, ~9+ vues), select2bs4, icheck-bootstrap, sweetalert2. CDN mis à jour **sans saut de version majeure** (vérifié via `data.jsdelivr.com`/`api.cdnjs.com`, jamais deviné) : jQuery 3.6.0→3.7.1 (pas 4.0.0, breaking changes), Font Awesome 6.5.2→6.7.2 (pas FA7), Chart.js `"latest"` non pinné → **pin explicite 4.5.1**. Doublon jQuery (header CDN + footer local) laissé tel quel : nécessaire au fonctionnement des scripts inline de vue (voir détail dans le fichier skill), retirer l'un sans audit exhaustif aurait été le risque "tout gâcher" explicitement signalé. Vérifié : `php -l`, 4 modules variés testés par curl sans Fatal, 3 nouvelles URLs CDN gagnées en HEAD 200 avant intégration.

- ✅ **Module Users (Réglages) traité en Phase 4** (voir `SKILL_MODULE_VENTE.md`, entrée après §56 terdecies/quaterdecies). `Users.php` reconstruit sur `ssm-panel`/`ssm-panel-toolbelt` (réglage/plein écran)/`ssm-table-toolbar` (imprimer/exporter) - pas de `ssm-stat-row` Total Page/Filtré (aucune colonne montant, la règle CLAUDE.md ne s'applique pas ici). Modale `Nouveau_user.php` : `data-mode`/`.ssm-modal-icon`/champs `.select2-error` obligatoires (même pattern que les modales déjà reprises cette session), boutons `ssm-btn`, icône ajoutée devant chaque case à cocher de la grille de permissions (Statistique/Trésorerie/Coffre/Compte Carburant-PS/Ventes/Achats/Stock/Services/Restaurant/Employés/Comptabilité/Réglage) - logique métier de la grille (calcul de `#compte` à partir de `#compte_carburant`/`#compte_ps`) non touchée. Vérifié par curl (page + `/Users/data` avec un payload DataTables complet + `/Users/modal`) sans Fatal/Warning, `node --check` sur les 2 blocs `<script>` extraits. Aucune donnée de test créée. Prochaine étape demandée par l'utilisateur : module **Paramètres** (Réglages).

- ✅ **Module Paramètre : navigation AJAX (ssm-module-nav 2 niveaux) + CRUD sans rechargement** (voir `SKILL_MODULE_VENTE.md` §57). Retour utilisateur : presque tous les liens rechargeaient la page. Passé par plan approuvé (étude via agent Explore, 14 contrôleurs cartographiés) avant execution. Navigation : `menu_parametre.php` (niveau 1, 6 items) + `menu_ventes.php`/`menu_para_stock.php` (niveau 2), même pattern que `stocks/portion/menu_stock.php`, `$type` défini localement par vue (comme `Produits.php`). CRUD AJAX : 4 contrôleurs convertis (`ParaPistolet`/`ParaCiterne`/`ParaCarburants`/`ParaScenarios`, ce dernier avec 2 fragments liste+zones) - `redirect()` remplacé par un rendu de fragment (`view_modal()`), logique métier d'insert/update/delete strictement inchangée (bugs préexistants non touchés, pas de leur ressort). Nouveaux fichiers `app/views/parametre/datatable/{Pistolets,Citernes,Carburants,Scenarios,ScenarioZones}.php`. Vérifié par curl (12 pages + 4 endpoints liste, cycle CRUD complet testé/nettoyé pour Pistolet et Scénarios+Zones ; Citerne/Carburants non testés en écriture live - mutations multi-tables avec gaps de nettoyage déjà présents avant ce chantier, seule la logique de rendu a changé). Hors périmètre signalé : fil d'Ariane (`top_bar.php`, composant partagé site-wide) et `ParaProduitsController` (fichier vide préexistant, vue orpheline).

- ✅ **Paramètre : 3 correctifs après 1er usage réel** (voir `SKILL_MODULE_VENTE.md` §58). Bug de fond trouvé : les vues n'avaient pas `#ssm_module_content` (convention qui fait persister le menu de sous-module entre navigations AJAX au lieu de le re-render à chaque fois, cf `stocks/portion/menu_stock.php`) - sans lui, `ssm_nav_ajax()` retombait sur un remplacement complet de page qui ne rejouait jamais le positionnement JS du curseur glissant (d'où "couleur pleine seulement au 1er chargement"). Ajouté sur les 10 vues. Menu aplati en 1 seul niveau (9 items, plus de hub Ventes/Stock intermédiaire) - `ParaVentes`/`ParaStock` réduits à un `redirect()` vers leur ancien 1er enfant ("entrer directement au premier"), vérifié que `X-Ssm-Nav` survit à ce 302. Onglets internes de `Historique.php` (Bootstrap `.nav-tabs`) convertis en `.ssm-tabs` pour cohérence visuelle. Les 5 tableaux CRUD passés en vraies DataTables (`init_datatable()` mode client, pas d'endpoint JSON dédié - listes courtes déjà rendues serveur). Effet de bord corrigé : les handlers `$(document).on(...)` des 4 pages CRUD (§57) auraient été réempilés à chaque revisite maintenant que leur script re-exécute en AJAX - namespace `.off().on()` ajouté partout. Cycle CRUD complet rejoué et vérifié propre après restructuration.

- ✅ **Paramètre : retour à 6 items menu + sous-onglets internes** (voir `SKILL_MODULE_VENTE.md` §59). Le menu à 9 items à plat (§58) allait trop loin - retour à 6 (Banque/Ventes/Télécollecte/Stock/Historique/Sauvegarde), Ventes (Scénarios+Points de vente) et Stock (Carburants+Citernes+Pistolets) redeviennent des pages à sous-onglets internes `.ssm-tabs` (même URL) plutôt que des entrées de menu séparées. Mémorisation du dernier onglet actif via `sessionStorage` (défaut Scénarios/Carburants), `location.hash` en repli pour les anciens liens directs (redirigés désormais vers `/ParaVentes`/`/ParaStock` avec ancre). 5 anciennes pages dédiées supprimées, absorbées dans les 2 pages à onglets.

- ✅ **Paramètre : 2 correctifs sur les sous-onglets internes** (voir `SKILL_MODULE_VENTE.md` §60). (1) Les sous-tabs de `ParaVentes.php`/`ParaStock.php`/`Historique.php` ne réagissaient pas au clic (`data-toggle="pill"` retiré, remplacé par une bascule JS 100% manuelle et explicite - un seul point de vérité sur qui est actif, plus de dépendance au délégué global Bootstrap). (2) Icônes d'action harmonisées sur les 5 tableaux (`Scenarios.php`/`ScenarioZones.php` avaient des boutons texte colorés au lieu d'icônes comme les 3 autres) - tous alignés sur icône seule + `title` en tooltip.

- ✅ **Paramétrage de la Facture (`/Station`) : refonte complète avec aperçu en direct** (voir `SKILL_MODULE_VENTE.md` §61). Migration `018` : 6 nouveaux réglages (`afficher_ice`/`afficher_adresse`/`afficher_tel`/`afficher_mail`, `mentions_legales`, `signature_active`). Cœur de la demande : `StationController::apercu()` rend la VRAIE vue `Invoice.php` (même chaîne `inc/impression/*` que `/FacturesClient/invoice`) avec des données d'exemple + les réglages en cours de saisie injectés via `$GLOBALS['ssm_station_apercu']` dans `style.php`, **sans jamais écrire en base** - `Station.php` poste vers un `<iframe>` cible (form caché + `tableau()`, débounce 400ms) à chaque modification, remplaçant l'ancienne maquette factice. `Invoice.php` consomme les nouveaux réglages (champs client conditionnels, bloc mentions légales, signature - `$GLOBALS['signature']` historique conservé en OU, pas remplacé). Bug latent corrigé : `logo_facture` par tenant (`inc/svg/logos/{code_station}_Logo_facture.png`, persisté en base - avant, `insertion_facture()` le remettait à `""` à chaque sauvegarde et 2 tenants auraient partagé le même fichier). 5 onglets `.ssm-tabs` + 5 palettes de couleurs rapides. **Incident évité en cours de test** : un `insertion_facture()` de test a écrasé la vraie config de la station (double-encodage `--data-urlencode` sur des couleurs déjà préfixées `#`) - repéré et restauré immédiatement depuis la ligne `id=2` (jamais touchée, config d'origine intacte).

- ✅ **"Type d'encaissement" déplacé dans Paramètre (1er onglet) + `/Parametre` va direct au 1er onglet** (voir `SKILL_MODULE_VENTE.md` §62). Nouveau `ParaEncaissementController`/`ParaEncaissement.php` (repris de la section admin-only en bas de `Station.php`, logique inchangée), ajouté en 1er item de `menu_parametre.php` + routing (`vendor/function.php`, `Layout.php`). `ParametreController::index()` redirige désormais directement vers `ParaEncaissement` au lieu d'afficher `Options.php` (page supprimée, contenait uniquement le bouton "Initialiser" = reset complet de la base - supprimée à la demande explicite de l'utilisateur plutôt que relocalisée ; `HomeController::initialiser_tout()` backend intact mais plus aucune UI ne pointe dessus).

- ✅ **Bug réel corrigé : modale "Attacher pistolet" vide + modale "Ajouter Zone" bloquée** (voir `SKILL_MODULE_VENTE.md` §63). Reproduit et confirmé par curl : le bouton `.attachement` lisait `e.target.id` au lieu de `$(this).attr('id')` - cliquer sur son icône (ajoutée en §60) envoyait un `id_zone` vide, causant un Fatal PHP silencieux côté serveur (réponse JSON invalide, callback `error` jamais géré) - d'où "les pistolets ne s'affichent pas, il faut fermer 3-4 fois" (dépendait du hasard de l'endroit cliqué sur le bouton). Fix + même correction préventive sur `.attacher`. 2e bug : `#exampleModal` ("Ajouter Zone") vit dans le fragment `#zones_liste`, détruit/recréé sans nettoyage du `.modal-backdrop`/classes `<body>` si encore ouverte au moment du remplacement (même classe de bug déjà fixée dans `ssm_nav_ajax()`) - nouvelle fonction `remplacer_zones_liste()` reprenant ce fix. Bascule "tout en ajax" demandée : la modale Attacher n'est plus ouverte par `data-toggle` puis remplie en différé, mais seulement après réception des données.

- ✅ **Achat > Bons de Livraison : facturation différée à la clôture + nouveau module "État Fournisseur"** (voir `SKILL_MODULE_VENTE.md` §64). Migrations `019`+`020`. Création d'un Bon simplifiée (fournisseur/n° de bon/date/livreur seulement, `type` reste NULL) ; à la clôture, choix à 3 voies : Facture (mêmes champs qu'avant, réunis en une étape) / Suivre (nouveau - regroupement "État Fournisseur", réplique allégée du principe État déjà en place côté Vente/Client) / Bon d'entrée simple (comportement non-facture actuel inchangé). Bons pré-migration (`type` déjà posé) : **aucune régression**, clôture identique à avant, vérifié par curl. Nouveau module complet : `FournisseurEtatController` + 3 modèles (`FournisseurEtat`/`FournisseurBon`/`FournisseurRemplacement`) + vues, réutilisant la couche Règlement déjà en place (`zone='fournisseur'` sur les tables partagées) pour 5 méthodes de règlement (espèce/pièce/opération/avoir/avance). Garde-fou en cascade de l'annulation de clôture répliqué et vérifié par un cycle complet (reste généré → attaché ailleurs → annulation refusée → libéré → annulation acceptée). Bug réel trouvé et corrigé en testant : `fournisseur_avoir` n'avait pas les colonnes `source`/`id_source` (contrairement à `client_avoir` et à `fournisseur_avance`) - migration `020` ajoutée.

- ✅ **Réorganisations de navigation : États Fournisseur / Réglage PV / Comparaison** (voir `SKILL_MODULE_VENTE.md` §65). (1) "États Fournisseur" (§64) retiré du menu Achats comme entrée séparée, intégré comme onglet du sous-menu "Gestion des Bons (Dépotage)" (`stocks/portion/menu_alimentation.php`) - même principe que ClientEtat côté Vente. (2) "Réglage PV" (Compte Journalier, gestion des PV/Ateliers/Co-locataires) déplacé depuis un bouton isolé sur `/Comptes` (aucune entrée menu) vers un nouvel onglet de Paramètre (`ParaPvController`/`ParaPv.php`), `/Pv` redirige vers `/ParaPv`. Bug latent corrigé au passage : les 3 onglets internes utilisaient `data-toggle="tab"`, non fiable une fois intégré au système de navigation AJAX de Paramètre - converti en bascule JS manuelle (même fix que §60/§63). (3) "Comparaison" fusionnée comme 2e onglet interne de "Bon de Commande" (au lieu d'une entrée séparée du sous-menu Dépotage), `/ComparaisonAlimentation` redirige avec ancre.

- ✅ **Compte Journalier : perf ComptePv (tickets fermés + détail ticket + `/Comptes`) + redesign `/Comptes`** (voir `SKILL_MODULE_VENTE.md` §69). Retour utilisateur : liste lente, détail de ticket lent, DataTable `/Comptes` lente. N+1 éliminé sur `Compte::data()`/`ComptePs::data()`/`ComptePv::data()` (extraction `formater_periode()` statique, pur formatage, appelé avec les dates déjà en mémoire au lieu d'un `findone()` par ligne) ; `ComptePvTicket::data()` : COUNT en sous-requête au lieu de `fetchAll()`+count() ; `ComptePvController::ticket()` : 5 requêtes séquentielles (espèce/banque/carte/pièce/gratuité) fusionnées en une seule `UNION ALL` (`ComptePvTicket::total_paiements()`). Migrations `021`+`022` (index manquants sur `compte_pv_ticket`/`compte_pv_ticket_description`/`client_bon`/`compte_pv_gratuite`). `/Comptes` restructuré : comptes ouverts en cartes stylisées (JS, réutilise l'endpoint DataTable existant via un payload synthétique — pas de nouvel endpoint), comptes clôturés dans une carte "Archive" repliée par défaut (ancienne DataTable+filtres inchangés à l'intérieur). Vérifié par curl (page + 2 endpoints + détail ticket réel) sans erreur PHP.

- ✅ **ComptePv : checklist modale Phase 4 sur les 7 modales de paiement/ticket** (voir `SKILL_MODULE_VENTE.md` §70). Retour utilisateur "n'oublie pas de styliser comptepv". Icône ronde + `data-mode` + `ssm-field-icon` + `ssm-btn` appliqués aux modales `point_vente/modal/paiement/{espece,operation,piece,carte,gratuite,client}.php` + `nouveau_ticket.php`, dégradés CSS inline retirés au profit de l'icône uniforme. Le tableau de bord ComptePv lui-même (`Compte.php`, ~2600 lignes, temps réel par polling) volontairement **non repris** cette passe (déjà stylé à la main, risque jugé trop élevé sans navigateur pour valider un composant live). Vérifié par curl (7 endpoints sur un ticket réel), aucune erreur PHP.

- ✅ **Compte Journalier : les 6 dernières modales stylisées, module entièrement traité** (voir `SKILL_MODULE_VENTE.md` §71). `Pompiste/Vente/Validation_compte/Validation_zone/sortie_caisse/info_attachement.php` passées à la checklist Phase 4 (dernier reste signalé en fin de §68). Boutons Valider/Annuler à couleur dynamique conservés. Vérifié par curl (7 endpoints sur un compte réellement ouvert), aucune erreur PHP. Reste hors périmètre volontaire : tableau de bord ComptePv (§70, temps réel, risque jugé trop élevé sans navigateur).

- ✅ **Statistique > Graphique : onglet Général réel, reclassement Adblue, bug "Transfert Bon" corrigé** (voir `SKILL_MODULE_VENTE.md` §72). Bug réel trouvé et corrigé : `AlimentationsController::cloture()` ne rattachait jamais la vente directe "Transfert Bon" à `stocks_mouvement_officiel` (76 lignes/37 bons historiquement invisibles dans toutes les stats) — migration `023` (backfill idempotent) **à rejouer sur toutes les bases clients existantes**. Adblue (vendu par index comme du carburant, TVA 20%) reclassé de "Carburant" vers "Produit" partout (règle générique sur la TVA). Onglet Général branché sur de vraies données (réutilise `/StatistiqueGraphique/evolution` avec `type_vente='tous'`, aucun nouvel endpoint). Nouvelle page d'explication des calculs (`/StatistiqueRapport`, lien depuis Graphique, pas d'entrée menu). Vérifié par curl (evolution carburant/ps/tous, cohérence des sommes) + non-régression Carburant confirmée.

- ✅ **Page d'accueil (`/Home`) : refonte générale du design** (voir `SKILL_MODULE_VENTE.md` §73). Module jamais touché Phase 4. Nouveau composant `.ssm-kpi-tile` (remplace les `small-box` AdminLTE des 8 tuiles chiffres-clés Achats/Ventes/Caisse/Banque, mécanisme JS de bascule d'affichage conservé à l'identique). 7 cartes principales converties en `.ssm-widget-card`, 3 paires synchronisées par `data-sync-group` (Achats/Ventes↔Caisse/Banque, À Traiter↔Utilisateurs, Stock Citerne↔Alerte Produits) — bug de course évité en amont (`collapsed-card` statique en HTML au lieu d'un `.click()` en boucle qui aurait doublé-basculé les paires synchronisées). DataTables passées en `ssm-table`, état vide des notifications en `ssm-empty-state`. Vérifié par curl (page complète + 8 endpoints AJAX), aucune erreur PHP.

- ✅ **Nouveau module "Shop & Café Restaurant" — Phase 1 (fondations)** (voir `SKILL_MODULE_VENTE.md` §74). Refonte complète du module Restaurant à partir de zéro (conception validée via une page HTML dédiée avant tout code). Ancien module (`restaurant_compte`/`restaurant_vente`, aucune donnée) supprimé entièrement. Principe central : un point de vente = une zone d'activité existante (`compte_ps_zone_activite`, flag `restaurant` réutilisé + `caisse=1`) → apparaît automatiquement dans Trésorerie > Caisse, sans écran à reconstruire. Migration `024` : extension `stocks_produits` (code-barre/photo/catégorie/mode de vente direct-préparé), nouvelles tables `shop_*` (catégories, points de vente, appareils autorisés, recettes, comptes/tickets/dépenses pour les phases suivantes). Livré et vérifié par curl (cycle complet) : gestion des points de vente + appareils TPE, catégories, catalogue produit avec code-barre/photo/éditeur de recette. Reste : achats, compte journalier + écran de vente + encaissement (espèce/carte/crédit client), dépenses/résultat/clôture.

- ✅ **Nouveau module "Shop & Café Restaurant" — module complet (Phases 1 à 5)** (voir `SKILL_MODULE_VENTE.md` §74-76). Refonte totale à partir de zéro, conception validée avec l'utilisateur avant tout code (page HTML dédiée). Ancien module Restaurant supprimé. Livré et vérifié de bout en bout : points de vente (= zone d'activité existante, apparaît nativement dans Trésorerie > Caisse), catégories, catalogue produit (code-barre/photo/recette), achats (0 code — réutilise `/Alimentations` tel quel), compte journalier (un seul actif à la fois, employés rattachés, résultat en direct, dépenses, clôture avec écart), écran de vente (grille photo + recherche/scan douchette + panier), 3 moyens de paiement tous intégrés à l'existant (espèce→Trésorerie, carte→CMI, crédit client→Crédit Client, chacun tagué `source='Shop'`). 2 bugs réels trouvés et corrigés en cours de route (attribution du ticket au mauvais "employé", clé de menu manquante causant un plantage total de l'app). Style `ssm-*` appliqué dès la création. Cycle complet rejoué et nettoyé par curl à chaque phase, `php -l` propre sur les ~20 fichiers du module, non-régression confirmée sur toutes les pages existantes touchées.

- ✅ **Shop & Café Restaurant : module Achat dédié, séparé de Gestion des Bons station** (voir `SKILL_MODULE_VENTE.md` §77). Retour utilisateur : refus de la réutilisation de `/Alimentations` (§75) — un écran d'achat totalement indépendant était voulu. Nouvelles tables `shop_alimentation`/`shop_alimentation_description` + `ShopAlimentationController` dédié, DataTable avec Total Page/Filtré, clôture alimentant quand même les tables partagées (stock/mouvements/statistiques restent cohérents). Vérifié : cycle complet rejoué et nettoyé, confirmé que le bon Shop n'apparaît pas dans `/Alimentations` (vraie séparation).

- ✅ **Shop & Café : nettoyage test + icônes catégorie + seeder démo** (voir `SKILL_MODULE_VENTE.md` §78). Point de vente/catégorie de test de l'utilisateur supprimés. Champ texte "icône" remplacé par une grille d'icônes cliquables (un utilisateur ne connaît pas les classes FontAwesome). Nouveau `scripts/shop_seed_demo.php` : crée 2 points de vente complets avec catalogue, achats, et par PV 2 comptes archivés + 1 en cours, tous moyens de paiement — rejouable, vérifié par curl sur toutes les pages concernées.

- ✅ **Shop & Café : retour utilisateur design/navigation/tickets en cours** (voir `SKILL_MODULE_VENTE.md` §79). Bouton "Ajouter" dépenses réaligné à droite (`ssm-btn`). Navigation inversée : entrer dans un point de vente ouvre directement la caisse (`/ShopVente/index/{id}`), avec un bouton clair "Voir le résultat du compte" pour y accéder ; `ShopCompteController::ouvrir()` renvoie du JSON pour permettre la redirection. Design retravaillé (bandeau d'en-tête partagé Compte/Vente, tuiles KPI pour le résultat). Nouveau concept **ticket en cours** : un ticket non encaissé reste persisté et reprenable (même principe que `compte_pv_ticket`, sans migration - réutilisation de la colonne `statut` existante), avec bande dédiée + boutons Reprendre/Supprimer sur l'écran de vente. Vérifié par `php -l`/`node --check`/curl sur les comptes du seeder (§78), aucune donnée de test laissée en base.

- ✅ **Shop & Café : historique des tickets + "Nouveau ticket" explicite** (voir `SKILL_MODULE_VENTE.md` §80). Nouvelle modale "Historique des tickets" dans la caisse (liste des tickets déjà encaissés du compte + détail des lignes au clic) - manquait totalement. Bouton "Mettre en attente" (bas de carte, peu visible) remplacé par "Nouveau ticket" en haut de la carte panier : parque automatiquement le ticket en cours (visible dans la bande "Tickets en cours") et en commence un nouveau, geste central demandé par l'utilisateur rendu explicite. Vérifié par curl sur un compte réel (3 tickets historiques, détail correct).

- ✅ **Shop & Café : historique en page (DataTable + annulation) et tickets en attente en cards** (voir `SKILL_MODULE_VENTE.md` §81). L'historique n'était plus en modale : affiché en page (masque catalogue/panier), en DataTable avec Total Page/Filtré, action "Voir le détail" + nouvelle **"Annuler la clôture"** d'un ticket (`ShopTicket::annuler_cloture()`, garde-fous compte non clôturé + crédit client pas déjà facturé, reverse mouvements de stock + paiement, ticket repasse en_cours). Tickets en attente affichés en page également, en cards façon reçu imprimé (toutes les lignes, boutons Reprendre/Supprimer) - remplace l'ancienne bande de chips. Vérifié par curl sur le compte réel du seeder, y compris un cycle complet annulation/ré-encaissement.

- ✅ **Assistant de configuration initiale — Étape 1 (compte administrateur)** : `HomeController::initialisation()` (jouée automatiquement par `Controller::auth()` tant qu'aucun utilisateur n'existe) transformée en premier écran d'un assistant multi-étapes. Restylée en plein-page `ssm-*` (nouveau `home/portion/initialisation.php` + composants CSS `.ssm-wizard-*`), soumission en AJAX (plus un POST pleine page), connexion automatique de l'administrateur créé (mêmes clés de session que `User::verify()`) pour enchaîner sans repasser par l'écran de connexion. Bug pré-existant découvert et corrigé au passage (`$GLOBALS['access']` non défini quand `Controller::auth()` force directement l'écran Station sans jamais repasser par `public/index.php` - warnings PHP inoffensifs mais bruyants, `top_bar.php`/`left_bar.php` gardés par `?? 0`). Vérifié en réel sur le tenant `demo` (vierge) : création complète (utilisateur/profils/banques/postes/modes/styles), connexion automatique confirmée (redirection vers l'écran Station déjà existant - `TypeEncaissement` vide -, aucun retour au login), puis tenant restauré à l'état vierge après test. **Reste à faire, étape par étape selon les instructions de l'utilisateur** : les étapes suivantes de l'assistant (contenu à définir).

### À reprendre à la prochaine session
0. **Chantier Banque en suspens (volontairement pas fait le 25/07/2026)** : convertir la navigation entre `CompteBancaire`/`OperationEnAttente`/`PieceImpayee` (actuellement un POST de page complète via `portion/menu.php`/`#form_voir_plus`) en AJAX `ssm-module-nav` comme le reste de l'appli - nécessite de faire vivre le sélecteur de compte bancaire + le solde HORS de la zone remplacée par la nav AJAX (persistant entre les 3 onglets), avec binding namespaced (`.off().on()`) pour éviter l'accumulation de handlers. Décision prise de ne pas le faire dans la même passe que le redesign visuel (§51) : module financier, 3 pages fortement couplées (sous-tableau remise/pièces, routes croisées), pas de navigateur disponible pour valider visuellement - risque jugé trop élevé sans confirmation utilisateur d'abord.
0. **Prochain chantier immédiat (demandé explicitement)** : séparer `ClientEtatController::datatable()` en 4 actions dédiées (une par DataTable : liste des états, bons d'un état, produits d'un état, paiements/remplacements d'un état) — voir `SKILL_MODULE_VENTE.md` §39, dernier paragraphe.
1. **Prochain chantier Phase 4** : **module Achat entièrement traité** (Fournisseur/Facturation/Compte Fournisseur+Grand Livre/Alimentation de stock/Dépense, voir §42-45) ainsi que Bons et Facturation côté Vente, **module Coffre entièrement traité** (Chèque/LCN, CMI, Carte Fournisseur, Vignette, voir §52-53), et **module Stock + Base Article (ex-Service) entièrement traités** (voir §54). Reste : **Compte** (Compte Journalier) avec `SKILL_MODULE_VENTE.md` comme référence de méthode — prochain module candidat. Bug connu non corrigé (hors périmètre à chaque passage, faible priorité) : alias décalés dans `FournisseurFactureDescription::description_facture()`, coquille sur `montant_ttc_hors` dans `StocksAlimentation::alimentation_between()`. Au passage, penser à corriger dans le module Vente les 2 points signalés en §37 non retro-appliqués : `Client::livre()` (iTotalRecords) et les `<thead>` colorés (`Encaissements.php` etc).
1. **Lire `SKILL_MODULE_VENTE.md` en premier** si on continue le design (Phase 4) — le module Vente est désormais entièrement traité (Client/Carte/Encaissement/Grand Livre, Bons, Facturation) ; le prochain chantier est un nouveau module (Compte Journalier, Restaurant) avec la même recette.
2. **Sauvegarde/Journal d'activité** : penser à jouer les migrations `007`/`008`/`009` sur les autres bases (sstm1, tenants, et à terme la base OVH) avant d'utiliser ces fonctionnalités ailleurs qu'en local.
3. Continuer les tests locaux du multi-tenant avec Bahaa (navigateur : demo.localhost:3000, sstm1.localhost:3000 ; créer d'autres tenants avec `tenant_create.php`).
4. Toujours en attente côté mutualisé : fichiers fix ComptePv + migration 003 en prod (erreur 500 modale Avance) — déploiement manuel classique ou via SSH si fournis.
5. Quand le VPS est acheté → installation (Linux/Apache/MariaDB/PHP, DNS wildcard `*.domaine`, Let's Encrypt), `sstm_master`, import des bases clients réelles, checklist de déploiement.
6. Phase 1.5 optionnelle : écran web super-admin des tenants. Ensuite Phase 2 (sécurité) / reste de la Phase 4 (redesign, module par module).
7. **Généraliser le pattern "Total Page"/"Total Filtré"** (voir `SKILL_MODULE_VENTE.md` §25-26, fait sur Encaissements et Grand Livre — devenu une **règle permanente** dans `CLAUDE.md` : toute DataTable à montants doit l'avoir par défaut) aux ~28 autres tables à montants du même principe (Achat/Depense, Achat/Impayees, Bons/*, Coffre/*, Stocks/*, Vente/Avoirs, Vente/Impayees, Banque/*...) — un module à la fois, à la demande ou dès qu'on retouche une de ces vues.

- ✅ **Rapport d'audit "Compte Journalier" ajouté à l'application** (page `/Rapport`, menu "Rapport d'Audit" en tout premier, icône rouge pour être très visible). Contenu source : `AUDIT_COMPTE_JOURNALIER.md` (racine), mis en forme HTML dans `app/views/home/Rapport.php` (nouveau `RapportController`). Accessible à tout utilisateur authentifié (namespace `rapport` forcé à 1 dans `public/index.php`, comme `home`).

- 🟡 **Compte Journalier — les 4 phases de l'audit toutes démarrées, en continu** (voir `SKILL_MODULE_VENTE.md` §66). Backup fait en premier (`backup_compte_journalier_26072026/`, 79 fichiers). **Phase 0** : dispatch dynamique sans garde (4 sites) → `method_exists()` ; division par zéro répartition pompiste ; incohérence `==`/`===` sur `id_client` ; garde `modif` manquant ; `<form>` mort d'ancien routage (`Pompiste.php`) ; branding station en dur dans le ticket PV imprimé ; paramètre mort (`ComptePvTicketDescription::descriptions()`). **Phase 2** : création/modification de compte Carburant convertie en AJAX/JSON (`Comptes::ajouter()`). **Phase 3** : N+1 corrigés sur les 2 endpoints PV polled toutes les 5s (`ComptePvController::employes()`/`encours()`, requêtes groupées à la place de boucles PHP) ; violation de la règle permanente "Total Filtré" trouvée et corrigée sur `/Comptes` (appel AJAX séparé `total_total` qui rejouait toute la requête → fusionné en SQL dans la réponse du DataTable). **Phase 4** : `/Comptes` équipée du chrome ssm-* standard (toolbelt réglage/plein écran + toolbar imprimer/exporter). Poursuite immédiate (3e passage) : entrée de routage fantôme `ComptePvResultatController` retirée, variable morte (`PistoletIndex::pistolet()`), `$date_min` non-initialisé (`marge()`), ordre de suppression non-sûr (`CompteZoneController::secondaire()`), N+1 `PistoletIndex::pistolet()`/`pistolet_compte()` corrigé (4 requêtes fixes au lieu d'1 par pistolet). Tout vérifié par `php -l`/`node --check`/curl sur données réelles, sans donnée de test créée. Reste à traiter : les bugs Phase 0 restants du catalogue (accès `find()[0]['x']` non gardés, pattern édition=suppression+réinsertion PV, tickets à montant net nul jamais clôturables), les autres N+1 (Vente/CiterneFlux/bons_compte*), et la suite du design Phase 4 (Compte/CompteZone/PV) - poursuite par petits blocs vérifiés.

- 🟡 **Compte Journalier — 4e passage** : `Vente::vente_zone_total()`/`vente_zone()`/`vente_compte()` avaient le pire N+1 du catalogue (boucle produits × pistolets, 1 `findone()` par itération) — corrigé en exposant `melange` directement sur les pistolets déjà batch-fetchés ; **vérifié par comparaison avant/après rigoureuse** (`git stash` sur les fichiers touchés, même compte réel, valeur strictement identique au centime près). `ClientBon::bons_compte_zone()`/`bons_compte()` également corrigées (2 des 6 lookups par ligne batch-fetchés). OntBon et CarteBon également corrigées (5e passage). Bug Phase 0 corrigé : ticket PV à montant net nul jamais clôturable (`ComptePvController::cloture()`, condition `montant!=0` parasite retirée, seule `reste==0` compte). Les 7 méthodes `bons_compte*` du catalogue sont maintenant TOUTES traitées (BonBaf/Piece/ClientAvoir/Espece ajoutées, toutes vérifiées sur données réelles). `/CompteZone` équipée du bouton plein écran (Phase 4, choix conservateur - pas de réglage colonnes/toolbar sur cette page à dashboard dynamique). 6 accès `find()[0]['id']` non gardés dans `EncaissementController` (branches modification) sécurisés (`?? 0`). **Décision assumée de ne pas toucher** : `point_vente/Compte.php` (Phase 4 - page trop dynamique/complexe pour redesign sans navigateur) et le pattern "édition=suppression+réinsertion" des méthodes de paiement PV (observation de code, pas un bug prouvé - changer le comportement de ré-écriture de lignes financières sans bénéfice net démontré est jugé plus risqué qu'utile). N+1 CiterneFlux laissé (tables très petites, impact négligeable). `/ComptePv/resultat` et `/Compte` équipées du bouton plein écran (choix conservateur). `compte/portion/Secondaire.php` (fragment partagé Compte/CompteZone) migré du vieux `$("#example1").DataTable({oLanguage:{sUrl:cdn}})` vers `init_datatable()` (moteur/style ssm-* commun, plus de dépendance CDN externe). Bouton "Supprimer le compte" converti de navigation brute vers AJAX+confirmation. Vérifié : le `echo+print_r+die` sur erreur SQL (cité par l'audit) est en fait LE pattern d'erreur de tout le framework (`Model::insert/update/delete`, 16 occurrences) - pas une négligence locale, laissé tel quel (chantier transverse séparé si un jour souhaité, hors périmètre Compte Journalier).

- ✅ **Correctif design : cartes Pompistes/Encaissements/Citernes/Ventes/Sortie espèce mal designées** (voir `SKILL_MODULE_VENTE.md` §67). Root cause : ces 5 cartes vivent dans le panel principal, aplaties visuellement (plus de bordure/ombre) par la règle Phase 4 "carte dans une carte" (voulue pour d'autres pages type Clients.php, effet de bord ici). Nouvelle classe `.ssm-widget-card` (+ 5 couleurs) qui y échappe, garde le comportement `collapse` AdminLTE. Icônes rafraîchies pour mieux coller au sens (gas-pump pour citernes, cash-register pour encaissements, etc). Vérifié par curl sur données réelles.
