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>
182 lines
15 KiB
Markdown
182 lines
15 KiB
Markdown
# É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
|