DOCS/v2/stats-screen-plan.md
Thomas Fransolet a5a8ecdb20 Documentation interne MyInfoMate / Unov
Import initial de la documentation : statut, roadmap, plans V1/V2,
specs verticales (creche, sport), audits securite, plan de test,
analyse concurrentielle et maquettes de design.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 11:17:01 +02:00

182 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Écran Statistiques — refonte
> Audit du code réel + décisions du 2026-08-07. **Refonte implémentée le 2026-08-07** — voir « État d'implémentation » en fin de document.
> Maquette : **https://claude.ai/code/artifact/052fe8f3-0288-4847-b63f-4c7de06068ec**
> Fichier concerné : `manager-app/lib/Screens/Statistics/statistics_screen.dart`
---
## Les sept défauts de l'écran actuel
*(État avant refonte — les numéros de ligne renvoient à l'ancienne version de 625 lignes.)*
Ce sont des problèmes d'**interface**, pas de données : `StatsSummaryDTO` contient déjà tout ce qu'il faut.
| # | Défaut | Où |
|---|---|---|
| 1 | **Libellés de sections tronqués à 8 caractères**`title.substring(0, 8)` sur des barres verticales. « Histoire du fort » → « Histoir… ». Le graphe le plus important est illisible sur son propre sujet, et c'est structurel : des barres verticales n'ont pas la place pour des titres | `:368` |
| 2 | **Deux des quatre KPI n'en sont pas** — « Top app » et « Top langue » affichent une valeur catégorielle unique, que les deux anneaux plus bas montrent en mieux, avec la distribution | `:199-201` |
| 3 | **Chips de canaux générées depuis `AppType.values`** sans filtrer sur ce que l'instance possède. Un client Essentiel voit une chip « Mobile » qu'il n'a pas, clique, tombe sur l'écran vide | `:130-136` |
| 4 | **Deux mécaniques de filtre empilées**`SegmentedButton` pour la période, `ChoiceChip` pour le canal, sur deux lignes de poids visuel différent, pour deux filtres de même nature | `:101-138` |
| 5 | **Deux anneaux côte à côte** — un donut se lit mal dès trois parts, et les langues peuvent en avoir dix | `:322-326` |
| 6 | **Palette qui se répète**`_chartColors[i % length]` sur six teintes : au-delà de six langues, deux langues reçoivent la même couleur. Et ce sont six nuances du même bleu, donc un dégradé utilisé comme palette catégorielle | `:394-401`, `:428` |
| 7 | **Aucune hiérarchie, aucun export** — KPI, courbe, graphes, tables : tout est une `Card` de poids égal empilée. Rien ne dit par où commencer, et il n'y a pas de bouton de rapport | `:168-181` |
---
## Décisions de refonte
### Le titre et la période
L'écran s'appelle **« Fréquentation »**, pas « Statistiques » — c'est la question que se pose le client. La période est écrite en toutes lettres sous le titre (*« Du 1ᵉʳ au 31 août 2026 »*) : aujourd'hui elle n'existe que dans le filtre, donc une capture d'écran ne dit pas de quoi elle parle.
### Un bandeau « à retenir »
Deux phrases en langage naturel au-dessus des chiffres. C'est ce qui fait qu'un client qui ouvre l'écran trois secondes en retire quelque chose. Les chiffres bruts ne se commentent pas tout seuls.
Généré côté backend à partir des mêmes agrégats (variation vs période précédente, contenu dominant, canal émergent). Pas d'appel LLM nécessaire pour la V1 — des règles suffisent.
### Quatre vrais KPI, tous avec une évolution
Un nombre sans comparaison ne veut rien dire. Chaque KPI porte sa variation vs période précédente.
| Multi-canal | Mono-canal |
|---|---|
| Visites · Durée moyenne · Contenus par visite · **Part du guide vocal** | Visites · Durée moyenne · Contenus par visite · **Visiteurs uniques** |
### Barres horizontales partout, une seule teinte
- Plus aucun anneau : au-delà de trois parts, l'œil compare mal des angles.
- Libellés complets, jamais tronqués.
- **Une seule teinte** : toutes les barres mesurent la même chose (un nombre de visites), c'est la longueur qui porte l'information. Colorer chaque barre suggérerait une distinction inexistante et deviendrait illisible dès sept catégories.
- Le problème de palette qui se répète disparaît de lui-même.
### Une seule barre de filtres
Période et canal sur la même ligne, même traitement visuel. **Chaque canal porte son volume** (`Guide vocal · 463`) : on voit où il y a de la matière avant de cliquer, au lieu de découvrir un écran vide.
### Règle mono-canal — importante
> **Compter les `ApplicationInstance` réellement actives de l'instance.**
> **Une seule** → masquer le filtre « Canal », la carte « Canaux », et remplacer le KPI vocal par « Visiteurs uniques ».
> **Deux ou plus** → afficher, avec **uniquement les canaux existants**.
C'est la situation courante aujourd'hui : les clients ont soit une app mobile, soit une borne, rarement les deux. Ça deviendra multi-canal avec un client comme Visit Namur (borne + mobile + vocal).
Un graphe à une seule barre à 100 % ne dit rien — et un écran qui ne s'adapte qu'à moitié se voit immédiatement. Le bandeau « à retenir » change aussi de deuxième phrase en mono-canal : sans canaux à comparer, l'information intéressante est ailleurs.
### La courbe
Aire simple, une seule série, avec :
- **bandes claires sur les week-ends** — la dent de scie devient lisible sans légende à décoder
- **pic cerclé et nommé** sous le graphe
- survol : trait vertical + valeur du jour
Pas de multi-séries par canal : quatre lignes emmêlées répondent mal à « est-ce que ça monte ? ». La répartition par canal est déjà donnée par les barres.
### Le rapport comme bloc, pas comme bouton
Le client voit **le document qu'il va envoyer**, avec son sommaire : fréquentation, contenus, canaux dont le vocal. C'est ce qui transforme l'écran en livrable — et c'est l'argument subside.
**Décidé le 2026-08-07 : v1 = bouton à la demande uniquement, généré côté client** (paquet `pdf` Dart, déjà présent). L'envoi mensuel automatique passe en v2 et se fera côté backend — voir `plan-import-ia-stats-subsides.md §1`.
Le sommaire annoncé par le bloc a été **corrigé pour ne promettre que ce qui existe** : la puce « questions posées au guide IA » et la mention « parcours terminés » ont été retirées, faute de données (voir ci-dessous).
---
## Onglet / canal « Vocal »
Moins de travail qu'il n'y paraît : **`AppType.Voice` existe déjà** dans le modèle, et `AiChatRequest.IsVoice` distingue déjà les interactions vocales.
-**Vérifié le 2026-08-07** : `VisitEvent` porte bien l'`AppType` (`StatsController.cs:52-65` à l'écriture, `:114` et `:191-195` à la lecture), et `Voice` est bien la valeur 4 de l'enum backend (`ApplicationInstance.cs:106-113`). Le canal vocal s'ajoute donc **sans changement backend** — c'est fait côté écran. Seul manquait `'Voice'` dans `AppTypeName` côté client généré, corrigé.
- À afficher : volume, **ce qui a été réellement écouté** (contenus déclenchés à la voix, POI atteints, durée d'écoute), part dans l'usage total.
- **À inclure dans l'export PDF** : « X % de nos visiteurs ont utilisé le guide vocal » est un argument de renouvellement de subside.
---
## Langue de l'interface
Toutes les chaînes passent par `AppLocalizations` (FR / NL / EN) — la langue du **gestionnaire**, distincte de celle des contenus. L'écran actuel le fait déjà correctement ; ne pas régresser.
---
## Ordre d'implémentation
| # | Tâche | Statut | Note |
|---|---|---|---|
| 1 | Barres horizontales à la place des barres verticales + anneaux | ✅ | Corrige les défauts 1, 5 et 6 d'un coup |
| 2 | Barre de filtres unifiée, canaux filtrés sur l'existant + volumes | ✅ | Défauts 3 et 4 |
| 3 | Règle mono-canal (masquage filtre + carte + KPI de substitution) | ✅ | Voir la réserve « visiteurs uniques » ci-dessous |
| 4 | KPI revus, avec variation vs période précédente | ✅ | Défaut 2 — **sans changement backend**, voir ci-dessous |
| 5 | Bandeau « à retenir » (règles, pas de LLM) | ✅ | Généré côté client, voir ci-dessous |
| 6 | Courbe en aire + bandes week-end + survol | ✅ | — |
| 7 | Bloc rapport + export PDF | ✅ | Bouton à la demande, PDF généré côté client. L'envoi mensuel est en v2 — voir ci-dessous |
| 8 | Canal vocal | ✅ | Filtre, carte Canaux et KPI de part vocale. Le détail « ce qui a été écouté » reste à faire |
---
## État d'implémentation — 2026-08-07
Écran refondu : `manager-app/lib/Screens/Statistics/statistics_screen.dart`.
Les sept défauts sont corrigés. Trois écarts assumés par rapport au plan et à la maquette, tous documentés ici.
### 1. La comparaison de période se fait côté client, pas côté backend
Le plan supposait un agrégat de période précédente à ajouter au backend. Inutile : `statsGetSummary` prend déjà `from`/`to`, donc l'écran **appelle deux fois le même endpoint** — période courante et période précédente de même durée. Trois requêtes au maximum (courante, précédente, et « tous canaux » quand un filtre de canal est actif), lancées en parallèle.
**Garde importante** : le backend rogne `from` sur `StatsHistoryDays` (`StatsController.cs:102-107`) sans le signaler. Une période précédente hors historique reviendrait tronquée, et la variation serait fausse sans qu'on le voie. L'écran ne demande donc la période précédente que si `StatsHistoryDays == 0 || StatsHistoryDays >= 2 × période` ; sinon les KPI s'affichent simplement sans variation. Même logique sur les chips de période : celles qui dépassent l'historique du plan sont désactivées.
### 2. Le bandeau « à retenir » est généré côté client
Le plan le voulait côté backend. Côté client c'est plus simple **et** mieux localisé : les phrases passent par `AppLocalizations`, donc elles suivent la langue du gestionnaire (FR/NL/EN) sans que le backend ait à la connaître. Les règles sont les mêmes :
- phrase 1 — variation vs période précédente (seuil de stabilité : ±3 %), ou à défaut le volume et la moyenne par jour ;
- phrase 2 — multi-canal : la part du guide vocal si elle existe, sinon le canal dominant ; mono-canal (ou écran déjà filtré sur un canal) : le contenu phare et sa part des consultations.
À déplacer côté backend le jour où l'export PDF aura besoin des mêmes phrases.
### 3. « Visiteurs uniques » n'existe pas dans le modèle de données
Le plan prévoyait ce KPI en substitution du vocal en mono-canal. Il n'est pas calculable : `TotalSessions` **est** le nombre de `SessionId` distincts (`StatsController.cs:121`), et `VisitEvent` ne porte aucun identifiant de visiteur au-delà de la session. « Visiteurs uniques » afficherait exactement le même nombre que « Visites », juste à côté — c'est-à-dire le défaut n° 2 réintroduit.
Le quatrième KPI est donc **« Contenus consultés »** (total des `SectionView` de la période) quand il n'y a pas de canal vocal. C'est un nombre distinct des trois autres, avec sa propre variation. Le vrai « visiteurs uniques » demanderait un identifiant de device persistant sur `VisitEvent` — décision produit, pas d'interface.
### 4. L'export PDF est côté client, et c'est un choix de périmètre
*(Décidé le 2026-08-07, livré le même jour.)*
**v1 = le bouton seul.** Le PDF est construit dans le navigateur avec le paquet `pdf` Dart — déjà dans le `pubspec`, déjà utilisé par `PDFHelper` pour les QR codes. Rien côté backend : pas de `StatsService` à extraire, pas d'endpoint, pas de `libfontconfig1` à ajouter au Dockerfile. L'écran a déjà tous les agrégats en main.
**v2 = l'envoi mensuel automatique**, qui lui *doit* être backend : personne n'a son navigateur ouvert le 1ᵉʳ du mois à 6 h. C'est là que QuestPDF + Hangfire reprennent leur sens, et c'est aussi là que se posent les trois questions qu'on a évitées en v1 : la config des destinataires, la fréquence, et une méthode d'envoi avec pièce jointe (`IEmailService` n'en a aucune — ses dix méthodes sont des templates figés).
Conséquences de l'implémentation côté client :
- **Le PDF et l'écran partagent les mêmes valeurs calculées.** `_kpis()`, `_takeawaySentences()`, `_contentBars()`, `_advancedTables()` alimentent les deux. Un chiffre ne peut pas diverger entre ce que le client voit et ce qu'il envoie à sa commune.
- **La couleur et le logo viennent du premier canal actif, l'app mobile en priorité.** `Instance` ne porte que `Name` : ni logo ni couleur. Le logo est lu par HTTP depuis Firebase Storage — **sans en-tête CORS sur le bucket, le navigateur refuse les octets** et la page de garde retombe sur le seul nom de l'instance. À traiter avec `v2/media-storage-plan.md` (le `gsutil cors set` y a sa place).
- **Un test couvre la génération** (`test/statistics_report_test.dart`, 4 cas). Il a immédiatement attrapé deux plantages : hauteur infinie sur la rangée de KPI (`CrossAxisAlignment.stretch` sous un `MultiPage` sans contrainte verticale, et le paquet `pdf` n'a pas d'`IntrinsicHeight`), et un NaN sur une série d'un seul point. Les deux auraient explosé au premier clic.
### 5. Deux promesses du bloc rapport n'avaient aucune donnée derrière
Le sommaire de la maquette annonçait quatre choses. Deux n'étaient adossées à rien :
| Promesse | État réel | Suite |
|---|---|---|
| « parcours terminés » | **Aucun `VisitEventType`** ne couvre l'achèvement d'un parcours guidé (`VisitEvent.cs:48-60`), et rien ne le trace côté visitapp | À ajouter : valeur d'enum + appel `track()` dans `GuidedPathContentProgressionPage` / `GuidedPathMapProgressionPage`. **Pas de plan existant — c'est ici qu'il faut le noter** |
| « ce que vos visiteurs ont demandé au guide IA » | Table `VisitorQuestion` inexistante, et `VisitEventType.AssistantMessage` existe dans l'enum mais **personne ne l'émet** (10 appels à `track()`, aucun pour l'assistant) | Spécifié dans `guide-ia-screen-plan.md §7`, part avec la migration v3 |
Les deux puces ont été retirées du bloc et remplacées par ce que le PDF contient réellement. La quatrième puce (POI, quiz, jeux, QR) ne s'affiche que si `HasAdvancedStats`.
### Autres points
- **Le bouton d'export du bandeau supérieur de la maquette n'a pas été repris** : il faisait doublon avec le bloc rapport, qui montre le document plutôt que de le promettre.
- **Courbe sur série continue** : `visitsByDay` n'émet que les jours qui ont des événements. L'écran reconstruit la série jour par jour de `from` à `to` en comblant les trous à zéro — sans ça les bandes de week-end tomberaient à côté et la pente serait fausse. Les bandes de week-end ne sont dessinées qu'en deçà de 62 jours ; au-delà c'est du bruit.
- **Statistiques avancées** : le backend vide `LanguageDistribution`, POI, quiz, jeux, articles, menu et QR quand `HasAdvancedStats` est faux (`StatsController.cs:359-369`). Les cartes correspondantes se masquent d'elles-mêmes et le bloc « plan Premium » prend la place — il est passé par `AppLocalizations`, il était en dur en français.
- **Nouveau chantier possible** : `DayStatDTO` ne porte que `Mobile` et `Tablet` en plus du total. Web, VR et vocal n'ont pas leur ventilation quotidienne. Sans importance ici (la courbe est mono-série par décision), mais à corriger si une ventilation par canal dans le temps devient nécessaire.
---
## Liens
- `guide-ia-screen-plan.md` — l'onglet « Ce que demandent vos visiteurs » suit les mêmes principes de forme
- `plan-import-ia-stats-subsides.md §1` — export PDF des stats