# Guide de modification — Lambec Trans Express

Site vitrine + CMS de gestion pour une activité de conduite PL/SPL indépendante.
PHP 8.3, SQLite, **aucun framework, aucune dépendance externe, pas de Composer**.

L'architecture des dossiers est décrite dans [README.md](README.md).
Ce fichier-ci décrit **comment modifier le code sans rien casser**.

---

## La règle numéro un

```bash
php bin/check.php
```

**Après chaque modification.** 154 vérifications : calculs de facturation,
numérotation, chevauchements de planning, validation, rendu de toutes les pages.
Le script travaille sur une base jetable — les vraies données ne sont jamais
ouvertes. Il sort en code 1 si quoi que ce soit échoue.

Version complète, quand le serveur tourne :

```bash
php bin/check.php --url=http://localhost:8123 --email=contact@lambec.fr --password=VOTRE_MOT_DE_PASSE
```

Ajoutez une vérification dans `bin/check.php` chaque fois que vous corrigez un
bug : c'est ce qui empêche qu'il revienne.

### Environnement local

`php` n'est pas dans le PATH. Le binaire à utiliser :

```bash
C:/laragon/bin/php/php-8.3.28-Win32-vs16-x64/php.exe
```

C'est la seule version installée qui possède un `php.ini`, donc les extensions
(`pdo_sqlite`, `mbstring`…). Les dossiers `php-8.5.*` existent mais sont nus :
tout script y échoue sur « could not find driver ».

Serveur de développement :

```bash
php -S localhost:8123 -t public bin/dev-router.php
```

---

## Les huit pièges

Chacun a une raison d'être. Les contourner casse quelque chose de réel.

### 1. Toute valeur affichée passe par `e()`

```php
<?= e($client['company']) ?>          <!-- oui -->
<?= $client['company'] ?>             <!-- faille XSS -->
<?= enl($mission['notes']) ?>         <!-- échappe + garde les retours à la ligne -->
```

Sans exception, y compris pour les messages `Flash` : ils contiennent des noms
de clients, donc de la saisie utilisateur.

### 2. Tout formulaire POST : `csrf_field()` dans la vue, `Csrf::verify()` dans le contrôleur

```php
<form method="post" action="/admin/clients/nouveau">
    <?= csrf_field() ?>
```

```php
if (Request::isPost()) {
    Csrf::verify();      // première ligne, avant toute écriture
```

Oublier `verify()` ouvre la porte à la falsification de requête. Oublier
`csrf_field()` rend le formulaire inutilisable (erreur 419).

### 3. Les boutons de la barre d'en-tête passent par `View::share()`

```php
View::share('page_actions', '<a class="btn btn--primary" href="…">+ Nouveau</a>');
```

**Pas** `$page['actions']`. Le layout est rendu **après** la vue
(`View::capture()`), et `extract()` en fait une copie locale : une écriture
dans `$page` depuis la vue n'atteint jamais le layout. `View::share()` écrit
dans un tableau statique, lu à chaque rendu — c'est le seul canal qui remonte.

### 4. L'ordre des routes compte

Dans `app/routes.php`, le premier motif qui correspond gagne :

```php
$router->any('/admin/clients/nouveau', …);   // AVANT
$router->get('/admin/clients/{id}',    …);   // sinon « nouveau » est pris pour un id
```

### 5. Les données de l'entreprise vivent dans `settings`, jamais en dur

Téléphone, SIRET, code APE, adresse, mentions de TVA : **toujours**
`Setting::get('company_phone')`.

C'est le correctif d'un vrai problème : l'ancien site recopiait le téléphone
dans 8 fichiers, et l'un d'eux affichait un numéro périmé sur les factures.

**Ajouter un réglage réclame deux gestes** — sinon il ne s'enregistrera pas,
en silence :

1. la valeur par défaut dans `database/seed.php` ;
2. la clé dans `SettingController::EDITABLE` (liste blanche des champs que le
   formulaire a le droit d'écrire).

### 6. Une facture émise ne se modifie pas et ne se supprime pas

Trois protections délibérées, à ne pas « simplifier » :

- **Numérotation continue** — `Document::destroy()` annule au lieu de supprimer
  dès qu'une facture n'est plus au brouillon. L'administration fiscale exige
  une séquence sans trou.
- **Coordonnées figées** — `documents.client_snapshot` conserve les coordonnées
  du client au jour de l'émission. Ne le remplacez pas par une lecture de la
  fiche client : déménager un client réécrirait ses anciennes factures.
- **Suppression client bloquée** — un client rattaché à un document comptable
  n'est pas supprimable (10 ans de conservation légale). On le passe en
  « inactif ».

Et sur le document imprimé : **aucune ligne de TVA chiffrée.** L'activité est
en franchise de base (art. 293 B du CGI) ; afficher « TVA (20 %) : 0,00 € »
contredit la mention légale juste en dessous.

### 7. Ajouter une colonne exige une migration

`database/schema.sql` n'utilise que des `CREATE TABLE IF NOT EXISTS` : sur une
base déjà installée, y ajouter une colonne **ne fait rien**. Tout marcherait
sur un poste neuf et planterait en production.

Marche à suivre dans [database/migrations/README.md](database/migrations/README.md).

### 8. Les couleurs et le gabarit ne se codent pas en dur

La charte — bleu `#0F2F5F`, orange `#F97316`, Poppins — est la valeur **par
défaut** des feuilles de style. Les valeurs réellement appliquées viennent des
réglages et sont injectées dans chaque page sous forme de variables CSS, par
`App\Models\Theme::css()`.

Dans une feuille de style, écrivez donc `color: var(--accent)` et jamais
`color: #F97316` : une couleur en dur devient insensible à l'écran Apparence.

Les trois gabarits de document (`charte`, `classique`, `epure`) partagent le
**même balisage PHP**. Seule une classe posée sur `.sheet` les distingue, tout
le reste vit dans `print.css`. Ajouter un gabarit ne demande donc aucune
modification des vues : une entrée dans `Theme::TEMPLATES`, un bloc
`.sheet--<clé>` dans `print.css`, et c'est fini.

Le logo est du SVG en ligne — la fonction `logo()` dans les vues — et jamais
un `<img>`. Il hérite ainsi des couleurs du thème et s'imprime sans dépendre
d'une requête réseau : un logo manquant sur une facture, c'est une facture à
refaire.

---

## Où ajouter quoi

| Besoin | Fichiers à toucher |
|---|---|
| Une page publique | `routes.php` → `SiteController` → `Views/site/` |
| Un écran d'administration | `routes.php` (avec `$admin`) → contrôleur → `Views/admin/` |
| Un champ sur une fiche | migration → `schema.sql` → modèle → `Views/…/formulaire.php` + `fiche.php` |
| Une règle de validation | `Core/Validator.php` (méthode chaînable) |
| Un type de document | `Document::TYPES` + `PREFIXES` → `Views/docs/` → `DocumentController::print()` |
| Un texte modifiable | `Content::SECTIONS` + blocs dans `seed.php` |
| Un réglage | `seed.php` **et** `SettingController::EDITABLE` |
| Un gabarit de document | `Theme::TEMPLATES` + un bloc `.sheet--<clé>` dans `print.css` |
| Un réglage d'apparence | `seed.php` **et** `ThemeController::EDITABLE` |

Un contrôleur suit toujours la même forme : lecture, validation, écriture,
`Flash`, redirection. En cas d'erreur : `Flash::setErrors()` +
`Flash::keepInput()` + redirection vers le formulaire, qui se repeuple via
`old()` et `error_for()`.

---

## Conventions

**Dates** — stockées en `Y-m-d H:i:s`, heure locale (Europe/Paris). Affichées
via `date_fr()` / `date_long()`. Jamais de `date()` brut dans une vue.

**Montants** — `float`, arrondis à 2 décimales à l'enregistrement, affichés via
`money()`. Tout est **HT** : la TVA n'est pas applicable.

**Saisie de nombres** — `Request::decimal()` accepte « 1 250,50 » comme
« 1250.50 ». Ne lisez pas `$_POST` directement pour un montant.

**SQL** — toujours des requêtes préparées, jamais de concaténation de variable.
Les modèles exposent des méthodes nommées ; les contrôleurs n'écrivent pas de SQL
(sauf `Database::insert()` sur une table sans modèle, comme `client_contacts`).

**Vues** — PHP nu, pas de moteur de gabarits. `$config` est partagé partout ;
les fragments s'incluent par
`include $config['paths']['views'] . '/docs/_pied.php';`.

**Caches par requête** — `Setting::all()`, `Flash::pull()` et `Flash::errors()`
mémorisent leur résultat. `Flash::pull()` peut donc être appelé depuis la vue
*et* le layout sans perdre les messages.

**Transactions** — `Database::transaction()` est réentrante : imbriquer deux
appels ne lève pas d'erreur, la transaction externe garde la main.

---

## Deux points en attente de décision

Signalés à l'utilisateur, modifiables dans Réglages sans toucher au code :

- **Code APE** — `5229Z` retenu (présent dans 6 des 7 anciens fichiers) contre
  `82.99Z` dans l'ancien `index.html`. À confirmer sur l'avis INSEE.
- **Mention RCS** — les anciennes mentions légales disaient « immatriculée au
  RCS de Boulogne-sur-Mer », l'ancienne facture disait « dispensé
  d'immatriculation au RCS ». Le RCS a été conservé.

Sont aussi à compléter : **IBAN et BIC**, sans lesquels les factures affichent
« à compléter dans les réglages ».

---

## Avant de mettre en ligne

```bash
php bin/check.php --url=…   # tout doit passer
```

- `config/local.php` **ne doit pas** partir sur le serveur : il force le mode
  débogage, qui affiche les erreurs PHP aux visiteurs.
- Racine web sur `public/` (voir README).
- `_legacy/` contient les anciennes pages HTML, gardées pour référence
  historique. Rien n'en dépend.
