# É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