# Spec — Statistiques

> Statut : `done`

## Intention

Fournir aux admins une vue sur l'activité de leur association : ventes, performance des vendeurs, produits populaires, écarts de caisse — et, depuis le lot 4, ce qui a **manqué**.

Les ventes qu'on a faites se comptent toutes seules. Celles qu'on n'a pas faites, parce qu'un produit était en rupture, ne se voient nulle part. C'est pourtant là que se perd l'argent.

## Contraintes

- Accessible uniquement aux admins (back-office).
- Données scopées à l'association de l'admin.
- **Pas de graphiques JavaScript.** Les mêmes écrans doivent pouvoir partir en PDF, et un canvas ne s'y rend pas. Les représentations sont donc en CSS pur : barres, largeurs en pourcentage.

## Comportement attendu

### Ventes par période
- Filtre par date (jour, semaine, mois, plage personnalisée).
- Total des ventes, nombre de transactions, répartition espèces/SumUp.

### Évolution
- Le chiffre de la période, celui de la **période comparable précédente**, et l'écart entre les deux.
- « Comparable » veut dire de même nature : la semaine dernière contre cette semaine, pas les sept derniers jours. Un lundi ne se compare pas à un jeudi.
- Une progression depuis zéro n'affiche **aucun pourcentage** : un démarrage n'est pas une hausse de 100 %, et publier un chiffre qui n'existe pas est pire que ne rien dire.

### Ventes par vendeur et par produit
- Vendeurs avec nombre de ventes et total généré.
- Produits triés par quantité, et séparément par chiffre d'affaires : ce ne sont pas les mêmes classements, et ils ne racontent pas la même chose.
- Regroupement par rayon quand des rayons existent.

### 20 / 80
- Produits classés par chiffre d'affaires, avec la **part cumulée** et le seuil des 80 %.
- L'intérêt n'est pas le classement mais la coupure : au-dessus, la poignée de produits qui fait le chiffre et qu'il ne faut jamais laisser manquer.
- La ligne qui fait franchir les 80 % en fait partie — c'est elle qui les atteint.

### Ruptures

Le stock enregistré est un instantané : il dit où on en est, pas depuis quand. L'historique, lui, existe déjà — `StockMovement` est un journal daté. On **remonte le temps** à partir du stock actuel en défaisant les mouvements. Aucune table nouvelle, aucune tâche planifiée.

Trois garde-fous, chacun couvert par un test :

1. **La série ne remonte pas avant la première écriture du produit.** Avant elle, on ne sait rien — le produit n'existait pas, ou son journal n'était pas tenu. Un stock inconnu ne doit pas se lire comme une rupture : « on ne sait pas » n'est pas « il n'y en avait plus ».
2. **La série ne va pas au-delà d'aujourd'hui.** Une période nommée court jusqu'à la fin du mois ; compter les jours à venir comme manquants doublerait la rupture en début de mois.
3. **Jours à zéro et jours en négatif sont comptés séparément.** Une rupture dit qu'il n'y a plus rien à vendre ; un stock négatif dit que le chiffre affiché ne veut plus rien dire (voir [ADR-0007](../decisions/0007-vente-jamais-bloquee-par-stock.md)). Le second appelle un inventaire, pas une commande.

### Ventes perdues

Les jours sans stock multipliés par ce que le produit se vend un jour ordinaire.

C'est une **estimation**, et elle ne peut pas être autre chose : on ne saura jamais combien de clients sont repartis les mains vides. Tous les écrans qui l'affichent le disent, à l'endroit même où on la lit.

Le « jour ordinaire » est un jour où le produit s'est vendu, pas un jour du calendrier : diviser par tous les jours écraserait la moyenne d'un stand qui n'ouvre que deux récrés par semaine.

### Fiche produit

Une page de **consultation**, qui n'existait pas — on ne pouvait que modifier un produit, jamais le regarder. Or la question « celui-là, on le garde ? » ne se tranche pas dans un formulaire d'édition.

Elle porte : stock actuel, CA de la semaine et de la période, évolution sur deux semaines, jours de rupture, ventes perdues estimées, l'historique du stock jour par jour, et le journal des mouvements avec leur auteur.

### Écarts de caisse
- Sessions clôturées avec leurs écarts espèces **et** carte, en colonnes séparées et avec deux totaux distincts.
- Filtrable par caisse et par période.

### Exports

- Export PDF des statistiques générales et d'une fiche produit.
- L'export **reprend les filtres en cours** : exporter autre chose que ce qu'on regarde serait pire qu'inutile.
- Rendu par Dompdf, sans aucune ressource distante : le PDF doit se produire à l'identique sur un serveur sans accès réseau.

## Critères de « fait »

- [x] Page stats dans le back-office admin, filtres de période fonctionnels.
- [x] Tableaux ventes par période, par vendeur, par produit.
- [x] Écarts de caisse par session, espèces et carte séparés.
- [x] Toutes les données filtrées par association.
- [x] Évolution contre la période comparable précédente.
- [x] 20/80 avec part cumulée et seuil.
- [x] Regroupement par rayon.
- [x] Reconstruction du stock jour par jour depuis le journal des mouvements.
- [x] Jours à zéro et jours en négatif comptés séparément.
- [x] Estimation des ventes perdues, signalée comme estimation partout où elle apparaît.
- [x] Fiche produit consultable, avec historique et statistiques.
- [x] Export PDF des statistiques générales et d'une fiche produit.

## Correction du 2026-09-11 — les ruptures se comptent en jours d'ouverture

Les jours à zéro, les jours en négatif et les ventes perdues se comptaient en **jours
calendaires**. Un produit en rupture le dimanche comptait une journée de rupture et une journée de
ventes perdues, alors que la boutique était fermée : un stand ouvert deux récrés par semaine
récoltait cinq jours de rupture pour chaque semaine passée à attendre sa livraison.

Ils suivent maintenant les **jours d'ouverture** déclarés par l'association
([cadencier.md](cadencier.md)). Une association qui n'en déclare aucun les ouvre tous, et retrouve
le comptage d'avant.

Le **rythme** qui sert à estimer les pertes change aussi de dénominateur. Il valait « les jours où
le produit s'est vendu » — un contournement qui datait d'avant les jours d'ouverture et qui
surestimait le rythme d'un produit vendu par à-coups. Il se mesure désormais sur les jours où le
produit **pouvait** se vendre : boutique ouverte, stock présent, après sa première vente. Les
jours sans stock sont exclus du dénominateur — ce sont précisément ceux dont on estime la perte,
les compter reviendrait à diviser par ce qu'on cherche.

**Une nuance conservée telle quelle** : le jour où le dernier article part compte à la fois comme
jour de vente et comme jour de rupture, puisqu'une rupture se lit sur le stock de **fin** de
journée. C'est la sémantique du lot 4, et la changer serait une autre décision.

## Ajout du 2026-09-29 — les lectures annuelles

Quatre blocs, sur une période **année scolaire** (septembre → août) ajoutée à `StatsPeriod`. Une
année civile couperait la seule année qui existe pour une boutique de lycée.

- **CA par mois**, en bâtons. Les mois sans vente gardent leur colonne à zéro et les mois à venir
  portent un tiret : resserrer le graphique sur les mois travaillés ferait disparaître les
  vacances, c'est-à-dire l'information qu'on cherche.
- **CA par jour d'ouverture**, en bâtons. Seuls les jours déclarés par l'association remontent —
  un lundi à zéro sur une boutique fermée le lundi n'est pas une contre-performance.
- **Camembert du 20/80**. Il encode un **rang**, pas des identités : une seule teinte du foncé au
  clair, la traîne qui s'efface. La palette Organic est chaude et peu saturée ; sept couleurs
  catégorielles n'y passent ni le seuil de saturation, ni la séparation daltonienne, ni la
  distinction en vision normale. Et le 20/80 ne demande pas « qui est qui » mais « lesquels font
  le chiffre ».
- **Saisonnalité en petits multiples** : une courbe par produit, chacune dans sa case, toutes à la
  même échelle. Sept courbes superposées demanderaient les sept couleurs qui n'existent pas, et
  resteraient emmêlées.

Les trois graphiques vivent dans `templates/_graphiques.html.twig`, partagé par le back-office et
le front — mêmes noms de classes, une feuille de style de chaque côté, comme `.couverture` depuis
le lot 5.

**Ce qui part en PDF** : les bâtons seulement, en **barres horizontales**. Dompdf ne fait pas de
flexbox, donc une colonne verticale ne survit pas à l'export. Le camembert et les courbes restent
à l'écran : un camembert imprimé en noir et blanc ne dit rien de toute façon.

**Côté vendeur**, deux blocs seulement : le camembert de l'année et les jours d'ouverture. Les
courbes de saisonnalité sont un outil de gestion, elles restent au back-office.

**Sur la fiche produit** : son année en bâtons, ses jours d'ouverture, et sa part du chiffre de
l'association — c'est cette part qui dit si une rupture se verrait ou passerait inaperçue.

## Notes

Le cadencier (`docs/chantiers.md`, lot 5) reprendra ces mêmes calculs sous une autre forme : ventes par produit × période, avec la rotation et les jours de rupture sur la même ligne. Il n'y a rien de nouveau à mesurer, juste une autre façon de lire.
