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

15 KiB
Raw Blame History

É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
1 Libellés de sections tronqués à 8 caractèrestitle.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éesSegmentedButton 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