Thomas Fransolet bb4d4fe2d5 Écran Statistiques refondu + Guide IA + onboarding (auth, abonnement) + i18n FR/EN/NL
Statistiques — refonte complète, aucun changement backend
  statistics_screen.dart réécrit (+1832) : barres horizontales monochromes à la place
  des barres verticales tronquées et des deux anneaux, barre de filtres unique avec les
  volumes par canal, règle mono-canal, 4 KPI portant chacun leur variation, bandeau
  « à retenir », courbe en aire avec bandes de week-end. La période précédente s'obtient
  en rappelant le même endpoint.
  statistics_report.dart : export PDF généré côté client (paquet pdf Dart), il partage
  les valeurs calculées de l'écran — un chiffre ne peut pas diverger entre l'écran et le
  document envoyé à la commune. Deux puces du sommaire promettaient des données
  inexistantes (parcours terminés, questions au guide IA), retirées.
  ⚠️ Jamais ouvert dans un navigateur. Cases de test : test-plan.md §8bis / §8ter.

Guide IA
  Screens/GuideIa/guide_ia_screen.dart — onglet Configuration. Menu conditionné à
  isAssistant, le même drapeau que la garde d'AiController. L'onglet « Ce que demandent
  vos visiteurs » n'est pas dans ce commit : le schéma backend est prêt, l'UI non.

Onboarding self-service
  Screens/Auth/ (mot de passe oublié, définition du mot de passe),
  Screens/Billing/subscription_screen.dart, ai_quota_hint.dart.
  ⚠️ Aucun parcours joué de bout en bout — test-plan.md §18.

Parcours guidés
  progression_mode.dart : 9 booléens sur 3 niveaux remplacés par 3 questions.
  Popups GuidedPath / GuidedStep / QuizQuestion mises à jour en conséquence.

Client API (manager_api_new) — édité À LA MAIN, ne pas relancer la génération
  onboarding_api.dart, authentication_api.dart (+80), instance_dto (champs Guide*),
  guided_step / quiz_question_guided_step (flags morts retirés).
  Le // @dart=2.18 manquant dans onboarding_api.dart cassait les 3 apps Flutter d'un
  coup — corrigé ici.

i18n : ~180 clés par langue (FR/EN/NL) + fichiers générés.
Tests : progression_mode_test, statistics_report_test (le second a attrapé deux
plantages qui seraient sortis au premier clic).

flutter build web . flutter analyze : 68 erreurs, toutes dans les fichiers modèle
orphelins de manager_api_new — dette connue, pas une régression, ces fichiers ne sont
pas dans le graphe de compilation.
2026-08-09 22:14:37 +02:00

171 lines
6.7 KiB
Dart

import 'package:flutter/material.dart';
import 'package:manager_api_new/api.dart';
import 'package:provider/provider.dart';
import 'package:manager_app/app_context.dart';
import 'package:manager_app/Models/managerContext.dart';
import 'package:manager_app/constants.dart';
import 'package:manager_app/Screens/Configurations/Section/SubSection/Parcours/parcours_config.dart';
class SectionParcoursConfig extends StatefulWidget {
final ParcoursDTO initialValue;
final ValueChanged<ParcoursDTO> onChanged;
const SectionParcoursConfig({
Key? key,
required this.initialValue,
required this.onChanged,
}) : super(key: key);
@override
_SectionParcoursConfigState createState() => _SectionParcoursConfigState();
}
class _SectionParcoursConfigState extends State<SectionParcoursConfig> {
late ParcoursDTO parcoursDTO;
List<SectionDTO> availableMaps = [];
@override
void initState() {
super.initState();
parcoursDTO = widget.initialValue;
WidgetsBinding.instance.addPostFrameCallback((_) => _loadAvailableMaps());
}
Future<void> _loadAvailableMaps() async {
if (parcoursDTO.configurationId == null || !mounted) return;
final appContext = Provider.of<AppContext>(context, listen: false);
final api = (appContext.getContext() as ManagerAppContext).clientAPI!.sectionApi!;
try {
final sections = await api.sectionGetFromConfiguration(parcoursDTO.configurationId!);
if (sections == null || !mounted) return;
setState(() {
availableMaps = sections.where((s) => s.type == SectionType.Map).toList();
});
} catch (e) {
// Silently keep empty on error
}
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// ── Où se déroule le parcours ? ──
Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Où se déroule le parcours ?",
style: TextStyle(fontWeight: FontWeight.w600)),
RadioGroup<bool>(
groupValue: parcoursDTO.showMap ?? true,
onChanged: (val) {
setState(() => parcoursDTO.showMap = val);
widget.onChanged(parcoursDTO);
},
child: Column(
children: [
RadioListTile<bool>(
dense: true,
contentPadding: EdgeInsets.zero,
activeColor: kPrimaryColor,
value: false,
title: Text("En salle", style: TextStyle(fontSize: 14)),
subtitle: Text(
"Une suite d'étapes dans un espace restreint, sans carte ni géolocalisation.",
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
),
RadioListTile<bool>(
dense: true,
contentPadding: EdgeInsets.zero,
activeColor: kPrimaryColor,
value: true,
title: Text("Sur le terrain", style: TextStyle(fontSize: 14)),
subtitle: Text(
"Le visiteur se déplace : carte et position sur chaque étape.",
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
),
],
),
),
],
),
),
if (parcoursDTO.showMap == true)
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Carte de base (optionnel)",
style: TextStyle(fontWeight: FontWeight.w500)),
DropdownButton<String?>(
isExpanded: true,
value: parcoursDTO.baseSectionMapId,
items: [
DropdownMenuItem<String?>(
value: null, child: Text("Aucune")),
...availableMaps.map((m) => DropdownMenuItem<String?>(
value: m.id,
child: Text(m.label ?? m.id ?? ''))),
],
onChanged: availableMaps.isEmpty
? null
: (val) {
setState(
() => parcoursDTO.baseSectionMapId = val);
widget.onChanged(parcoursDTO);
},
),
if (availableMaps.isEmpty)
Text(
"Aucune section Carte dans cette configuration — créez-en une pour pouvoir la réutiliser ici.",
style: TextStyle(
fontSize: 12, color: Colors.grey[600]),
),
],
),
),
),
],
),
),
// ── Liste des parcours ──
if (parcoursDTO.id != null)
SizedBox(
height: 500,
child: ParcoursConfig(
initialValue: parcoursDTO.guidedPaths ?? [],
parentId: parcoursDTO.id!,
isEvent: false,
isParcours: true,
isGeolocated: parcoursDTO.showMap ?? true,
onChanged: (paths) {
setState(() => parcoursDTO.guidedPaths = paths);
widget.onChanged(parcoursDTO);
},
),
)
else
Padding(
padding: const EdgeInsets.all(16),
child: Text(
"Sauvegardez d'abord la section pour gérer les parcours.",
style: TextStyle(fontStyle: FontStyle.italic, color: Colors.grey[600]),
),
),
],
);
}
}