# Projet SSTM — Gestion de station-service (framework maison PHP)

> **Feuille de route** : voir `PLAN.md` (modernisation, multi-clients/tenant, redesign). Le consulter avant tout gros chantier et cocher les étapes faites.
> **Redesign (Phase 4)** : voir `DESIGN.md` (journal détaillé) et surtout **`SKILL_MODULE_VENTE.md`** — recette complète (composants, navigation AJAX, plein écran, impression/export, pièges CSS déjà rencontrés) à lire avant de retoucher le module vente ou d'en attaquer un nouveau avec la même approche.
> **Règle permanente — tableaux à montants** : toute DataTable (n'importe quel module) qui affiche une colonne de montant doit avoir un total "Page" + un total "Filtré" par colonne de montant, selon le pattern de `SKILL_MODULE_VENTE.md` §25 (`Client::encaissement()`/`Client::livre()` comme modèles) : total filtré calculé en SQL (COUNT+SUM fusionnés en une requête) et renvoyé **dans le même JSON** que la DataTable — jamais un appel AJAX séparé — affiché via `.ssm-stat-row.ssm-stat-row--fin` + `ssm_table_totaux()`/`ssm_table_totaux_multi()` (`ajax.js`). À appliquer par défaut dès qu'on touche une table à montants, même sans demande explicite.

Serveur de dev : `http://localhost:3000` (déjà lancé par l'utilisateur, racine = `public/`).
Base de données : MySQL XAMPP (`C:\xampp\mysql\bin\mysql.exe`), base `station`, user `root`, sans mot de passe — credentials dans `config/config.php` (hors git, modèle : `config/config.exemple.php`), chargés par `app/models/Model.php`.
Compte de test applicatif : `b.e.benhayoun@gmail.com` / `BENbahaa@02`.
Git : dépôt privé **https://github.com/bebenhayoun/sstm**. Commiter à chaque étape et pousser. **Toute modification de structure de base = fichier numéroté dans `database/migrations/`** (voir `database/README.md`), puis régénérer `database/schema.sql` (⚠️ sans BOM de préférence ; les scripts tolèrent le BOM).

## Multi-clients (Phase 1, 14/07/2026)
- Résolution par sous-domaine dans `Model::resolve_db()` : `code.base_domain` → base du tenant via `sstm_master` (tables `tenant`, `migration_log` — voir `database/master.sql`). Domaine nu ou CLI → base `db` de la config. Tenant inconnu/suspendu → 404.
- Config : `multi_tenant` (bool) + `base_domain` + credentials `master` dans `config/config.php`. En local : `multi_tenant=true`, `base_domain=localhost` → tester avec `curl -H "Host: demo.localhost:3000"` ou http://demo.localhost:3000.
- Scripts (PHP CLI XAMPP : `C:\xampp\php\php.exe`) : `scripts/tenant_create.php <code> "<Nom>"` (base vierge depuis schema.sql, auto-init admin à la 1ère visite) ; `scripts/tenant_import.php <code> "<Nom>" <dump.sql> [--migrations-a-jour]` ; `scripts/migrate.php [<code>|--liste]` (applique les migrations non jouées, journal dans `sstm_master.migration_log`).
- Tenants de test locaux : `demo` (vierge), `sstm1` (copie de station). Le branding par client reste `parametre_station` dans chaque base.

## Routage (public/index.php)
- URL `/{Controller}/{action}/{param3}/{param4}` → `App\controllers\{namespace}\{Controller}Controller::{action}()`.
- Le namespace du contrôleur est résolu par `namespace_resolve()` dans `vendor/function.php` (tableau statique route → namespace). **Tout nouveau contrôleur doit être ajouté dans ce tableau** (et dans `left_bar()` pour le menu).
- Les segments d'URL sont dans `$GLOBALS['paths']` (`paths[1]`=controller, `paths[2]`=action, `paths[3]`, `paths[4]`=params).
- Action par défaut : `index`.
- Les fichiers `xxx*.php` / `x*.php` / `yy*.php` sont d'anciennes versions gardées en backup — ignorer.

## Contrôleurs (app/controllers/Controller.php)
- `Controller::view('dossier.Vue', compact(...))` → inclut `app/views/dossier/Vue.php` avec header/top_bar/left_bar/footer. Les variables passées sont accessibles dans la vue via `$param['...']`.
- `Controller::view_modal('paiement.espece', ...)` → rend seulement le fragment (modales AJAX).
- Les POST AJAX passent par `$_POST['param']` au format `cle=.=valeur/./cle2=.=valeur2`, décodé par `Controller::to_array()` dans le constructeur du contrôleur → `$this->param`.
- Côté JS : `load_portion('/Controller/action', 'id_element_html', param)` (défini dans `public/js/ajax.js`) fait le POST AJAX et injecte le HTML.

## Modèles (app/models/Model.php)
- CRUD maison : `find($where)`, `findone($id)`, `find_attribut($attr,$where)`, `insert($tab)`, `update($id,$tab)`, `delete($id)`, `delete_where($where)`.
- Chaque modèle définit `public $table='nom_table';`.
- `datatable()` génère order/limit pour les DataTables server-side.

## Concept PieceRemplacement (app/controllers/coffre/PieceRemplacementController.php)
Remplacement d'une pièce bancaire impayée (`coffre_piece`) par des paiements.
- Table pivot : `coffre_remplacement_piece` (`source`, `id_remplacement`, `montant`). Chaque méthode de paiement (espece/piece/operation/carte/**avoir**) insère une ligne pivot + une ligne dans sa table de destination avec `source='PieceRemplacement'`, `id_source`=id du pivot, `zone='client'`.
- Destinations : espece→`compte_ps_caisse_flux`, piece→`coffre_piece`, operation→`compte_bancaire_flux` (table exacte via modèles), carte→`carte_bon`, avoir→`coffre_remplacement_avoir`.
- Affichage de la liste : `CoffreRemplacementPiece::remplacement_piece()` + `info_paiement($source,$id_source)` (un `case` par méthode).
- Chaque action de paiement du contrôleur gère 4 cas selon `$this->param` : ajout (`id==0` + `montant`), modification (`id` + `montant`), suppression (`id` + `suppression`), ouverture du formulaire pré-rempli (`id` seul). Elle calcule `$max` = montant pièce − somme déjà remplacée, et rend la modale `app/views/paiement/{methode}.php`.
- Clôture : `cloture()` (URL `/PieceRemplacement/cloture/{id}` ; annulation avec `/annulation` en paths[4]). Possible si `sum(coffre_remplacement_piece.montant) >= montant pièce` ; l'excédent part dans `coffre_remplacement_avance`.
- Vue principale : `app/views/coffre/Remplacement.php` (dropdown des méthodes + DataTable + JS). Header : `app/views/coffre/portion/header_remplacement.php` (boutons Clôturer/Annuler).

## Méthode Avoir = ATTACHEMENT (concept revu le 13/07/2026)
- On n'y crée PAS d'avoir : on **attache un avoir client existant non utilisé** (`client_avoir` avec `id_destination is null`). Création des avoirs : `ClientAvoirController` (bons) / `AvoirClientController` (vente).
- Attacher = insérer le pivot (`source='avoir'`, montant de l'avoir) + `client_avoir.destination='PieceRemplacement'`, `id_destination`=id pivot. Détacher = supprimer le pivot + remettre destination/id_destination à NULL. Pattern identique : `ClientEtatController::avoir()` (destination='Etat') et `ReglementFournisseurController::avoir()`.
- Un avoir consommé ainsi ne peut plus être attaché à un compte journalier (les vues Compte ne listent que `destination='Carburant'`) — exigence utilisateur.
- Le client du chèque : `Piece::client_piece($piece)` — `id_client` direct, sinon remonte via `source` : Reglement→`client_paiement`→`client_reglement` ; Avance→`client_avance` ; Remplacement→`client_remplacement`→`client_etat`. Si aucun client résolu → modale avec message, pas d'avoirs proposés.
- Pas de bouton "modifier" pour un avoir attaché (comme `avance`) : détacher/rattacher seulement. Vue modale : `app/views/paiement/avoir.php` (tableau + bouton `.attacher_avoir`, handler JS dans `coffre/Remplacement.php`).
- Mode forcé : bouton "Afficher tous les avoirs" dans la modale (`param['tous']`) → liste les avoirs libres de tous les clients avec colonne Client + avertissement ; les handlers `.forcer_avoirs`/`.retour_avoirs_client` vivent dans la modale elle-même (pas dans `Remplacement.php`).
- **Avances (facturation) attachables aussi** (14/07/2026) : `avance()` du contrôleur + modale `paiement/avance.php` (handlers JS internes à la modale), même concept que l'avoir mais sur `client_avance`. Migration 003 a ajouté `destination`/`id_destination` à `client_avance` (`id_paiement` reste réservé aux règlements — ne PAS le réutiliser, collision d'ids). Disponible = `id_paiement=0 AND id_destination is null` — ce filtre a été ajouté aussi dans `ReglementClientController::avance()`, `ClientsController` et `StatistiqueClientController` (anti double-emploi). Affichage type/référence via `ClientAvance::info_avance()` (fallback "Antérieure").
- La clôture prend l'avoir en compte automatiquement via la somme du pivot.

## Recette de test rapide (curl)
> **CSRF (28/07/2026)** : toute requête POST (y compris le login) exige désormais un jeton valide — voir `public/index.php`. Récupérer le jeton depuis la balise `<meta name="csrf-token">` d'une page chargée dans la même session (GET), puis l'envoyer soit en en-tête `X-CSRF-Token` (AJAX), soit en champ `csrf_token` du corps POST (formulaire classique). Sans lui : `403` + `{"erreur":"session_expiree",...}`.
```bash
# 1. GET initial pour obtenir le cookie de session ET le jeton CSRF
curl -s -c cookies.txt -o page.html http://localhost:3000/
TOKEN=$(grep -oE 'name="csrf-token" content="[^"]+"' page.html | sed -E 's/.*content="([^"]+)".*/\1/')
# login (cookie de session) - le jeton est obligatoire ici aussi
curl -s -b cookies.txt -c cookies.txt -H "X-CSRF-Token: $TOKEN" -d "Authentification_login=1" -d "email=b.e.benhayoun@gmail.com" -d "password=BENbahaa@02" http://localhost:3000/
# action AJAX (toujours envoyer le header X-Requested-With ET X-CSRF-Token)
curl -s -b cookies.txt -H "X-Requested-With: XMLHttpRequest" -H "X-CSRF-Token: $TOKEN" \
  --data-urlencode "param=id_remplacement=.=15/./id=.=0/./montant=.=50/./..." \
  http://localhost:3000/PieceRemplacement/avoir
```

## Conventions
- Interface en français, Bootstrap 4 / AdminLTE, jQuery, Select2, DataTables server-side, toastr.
- Pas de requêtes préparées sur les WHERE (concaténation) — suivre le style existant du projet.
- L'utilisateur héberge ensuite les fichiers modifiés : **toujours lui donner la liste complète des fichiers modifiés/créés + le SQL exécuté** à la fin d'une tâche.
