import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import 'package:manager_app/Models/managerContext.dart'; import 'package:manager_app/app_context.dart'; import 'package:manager_app/constants.dart'; import 'package:manager_api_new/api.dart'; import 'package:provider/provider.dart'; class StatisticsScreen extends StatefulWidget { const StatisticsScreen({Key? key}) : super(key: key); @override _StatisticsScreenState createState() => _StatisticsScreenState(); } class _StatisticsScreenState extends State { int _selectedDays = 30; AppType? _selectedAppType; // null = "Tous" Future? _future; late ManagerAppContext _managerAppContext; @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) => _load()); } void _load() { final days = _selectedDays; final appType = _selectedAppType; setState(() { _future = _managerAppContext.clientAPI!.statsApi!.statsGetSummary( _managerAppContext.instanceId!, from: DateTime.now().subtract(Duration(days: days)), to: DateTime.now(), appType: appType?.name, ); }); } String _formatDuration(int seconds) { if (seconds < 60) return '${seconds}s'; final m = seconds ~/ 60; final s = seconds % 60; return '${m}m${s > 0 ? ' ${s}s' : ''}'; } @override Widget build(BuildContext context) { final appContext = Provider.of(context); _managerAppContext = appContext.getContext() as ManagerAppContext; return Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildHeader(), const SizedBox(height: 16), Expanded( child: FutureBuilder( future: _future, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); } if (snapshot.hasError || snapshot.data == null) { return Center(child: Text('Impossible de charger les statistiques', style: TextStyle(color: kBodyTextColor))); } final stats = snapshot.data!; return _buildDashboard(stats); }, ), ), ], ), ); } Widget _buildHeader() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('Statistiques', style: TextStyle(fontSize: 26, fontWeight: FontWeight.w600, color: kPrimaryColor)), SegmentedButton( style: SegmentedButton.styleFrom( selectedBackgroundColor: kPrimaryColor, selectedForegroundColor: kWhite, ), segments: const [ ButtonSegment(value: 7, label: Text('7j')), ButtonSegment(value: 30, label: Text('30j')), ButtonSegment(value: 90, label: Text('90j')), ], selected: {_selectedDays}, onSelectionChanged: (s) { _selectedDays = s.first; _load(); }, ), ], ), const SizedBox(height: 10), Wrap( spacing: 8, children: [ ChoiceChip( label: const Text('Tous'), selected: _selectedAppType == null, selectedColor: kPrimaryColor, labelStyle: TextStyle(color: _selectedAppType == null ? kWhite : kBodyTextColor), onSelected: (_) { _selectedAppType = null; _load(); }, ), ...AppType.values.map((type) => ChoiceChip( label: Text(type.name), selected: _selectedAppType == type, selectedColor: kPrimaryColor, labelStyle: TextStyle(color: _selectedAppType == type ? kWhite : kBodyTextColor), onSelected: (_) { _selectedAppType = type; _load(); }, )), ], ), ], ); } Widget _buildDashboard(StatsSummaryDTO stats) { if (stats.totalSessions == 0) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.bar_chart_outlined, size: 56, color: kBodyTextColor.withValues(alpha: 0.4)), const SizedBox(height: 16), Text( 'Pas encore de données pour cette période', style: TextStyle(fontSize: 15, color: kBodyTextColor), ), if (_selectedAppType != null) ...[ const SizedBox(height: 8), Text( 'Aucun event reçu pour le type "${_selectedAppType!.name}"', style: TextStyle(fontSize: 13, color: kBodyTextColor.withValues(alpha: 0.6)), ), ], ], ), ); } return SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // KPI cards _buildKpiRow(stats), const SizedBox(height: 20), // Line chart _buildLineChart(stats), const SizedBox(height: 20), // Bar chart + Donut charts _buildChartsRow(stats), const SizedBox(height: 20), // Bottom tables _buildTablesRow(stats), ], ), ); } Widget _buildKpiRow(StatsSummaryDTO stats) { final topAppType = stats.appTypeDistribution.isNotEmpty ? stats.appTypeDistribution.entries.reduce((a, b) => a.value > b.value ? a : b) : null; final topLang = stats.languageDistribution.isNotEmpty ? stats.languageDistribution.entries.reduce((a, b) => a.value > b.value ? a : b) : null; return Row( children: [ _kpiCard('Sessions', '${stats.totalSessions}', Icons.people_outline), const SizedBox(width: 12), _kpiCard('Durée moy.', _formatDuration(stats.avgVisitDurationSeconds), Icons.timer_outlined), const SizedBox(width: 12), _kpiCard('App top', topAppType?.key ?? '—', Icons.phone_iphone), const SizedBox(width: 12), _kpiCard('Langue top', topLang?.key.toUpperCase() ?? '—', Icons.language), ], ); } Widget _kpiCard(String label, String value, IconData icon) { return Expanded( child: Card( elevation: 0, color: kWhite, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), child: Padding( padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon(icon, color: kPrimaryColor, size: 22), const SizedBox(height: 8), Text(value, style: TextStyle(fontSize: 26, fontWeight: FontWeight.w700, color: kPrimaryColor)), const SizedBox(height: 4), Text(label, style: TextStyle(fontSize: 13, color: kBodyTextColor)), ], ), ), ), ); } Widget _buildLineChart(StatsSummaryDTO stats) { if (stats.visitsByDay.isEmpty) return const SizedBox(); final spots = []; final mobileSpots = []; final tabletSpots = []; for (var i = 0; i < stats.visitsByDay.length; i++) { final d = stats.visitsByDay[i]; spots.add(FlSpot(i.toDouble(), d.total.toDouble())); mobileSpots.add(FlSpot(i.toDouble(), d.mobile.toDouble())); tabletSpots.add(FlSpot(i.toDouble(), d.tablet.toDouble())); } final isFiltered = _selectedAppType != null; return Card( elevation: 0, color: kWhite, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), child: Padding( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Visites par jour', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: kPrimaryColor)), const SizedBox(height: 4), if (!isFiltered) Row(children: [ _legendDot(kPrimaryColor), const SizedBox(width: 4), Text('Mobile', style: TextStyle(fontSize: 12, color: kBodyTextColor)), const SizedBox(width: 12), _legendDot(kSecond), const SizedBox(width: 4), Text('Tablet', style: TextStyle(fontSize: 12, color: kBodyTextColor)), ]), const SizedBox(height: 16), SizedBox( height: 200, child: LineChart(LineChartData( gridData: FlGridData(show: true, drawVerticalLine: false), titlesData: FlTitlesData( leftTitles: AxisTitles(sideTitles: SideTitles(showTitles: true, reservedSize: 32)), rightTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)), topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)), bottomTitles: AxisTitles(sideTitles: SideTitles( showTitles: true, interval: (stats.visitsByDay.length / 5).ceilToDouble().clamp(1, double.infinity), getTitlesWidget: (value, meta) { final idx = value.toInt(); if (idx < 0 || idx >= stats.visitsByDay.length) return const SizedBox(); final date = stats.visitsByDay[idx].date ?? ''; return Text(date.length >= 5 ? date.substring(5) : date, style: const TextStyle(fontSize: 10)); }, )), ), borderData: FlBorderData(show: false), lineBarsData: isFiltered ? [_lineBar(spots, kPrimaryColor)] : [_lineBar(mobileSpots, kPrimaryColor), _lineBar(tabletSpots, kSecond)], )), ), ], ), ), ); } Widget _legendDot(Color color) { return Container(width: 10, height: 10, decoration: BoxDecoration(color: color, shape: BoxShape.circle)); } LineChartBarData _lineBar(List spots, Color color) { return LineChartBarData( spots: spots, isCurved: true, color: color, barWidth: 2, dotData: FlDotData(show: false), belowBarData: BarAreaData(show: true, color: color.withOpacity(0.1)), ); } Widget _buildChartsRow(StatsSummaryDTO stats) { final showAppTypeDonut = _selectedAppType == null; return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded(flex: 2, child: _buildTopSectionsChart(stats)), const SizedBox(width: 12), if (showAppTypeDonut) ...[ Expanded(child: _buildDonut(stats.appTypeDistribution, 'Apps')), const SizedBox(width: 12), ], Expanded(child: _buildDonut(stats.languageDistribution, 'Langues')), ], ); } Widget _buildTopSectionsChart(StatsSummaryDTO stats) { if (stats.topSections.isEmpty) return const SizedBox(); final sections = stats.topSections.take(8).toList(); final maxViews = sections.map((s) => s.views).reduce((a, b) => a > b ? a : b); return Card( elevation: 0, color: kWhite, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), child: Padding( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Top sections', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: kPrimaryColor)), const SizedBox(height: 16), SizedBox( height: 240, child: BarChart(BarChartData( alignment: BarChartAlignment.spaceAround, maxY: maxViews.toDouble() * 1.2, barTouchData: BarTouchData(enabled: true), titlesData: FlTitlesData( leftTitles: AxisTitles(sideTitles: SideTitles(showTitles: true, reservedSize: 32)), rightTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)), topTitles: AxisTitles(sideTitles: SideTitles(showTitles: false)), bottomTitles: AxisTitles(sideTitles: SideTitles( showTitles: true, getTitlesWidget: (value, meta) { final idx = value.toInt(); if (idx < 0 || idx >= sections.length) return const SizedBox(); final title = sections[idx].sectionTitle ?? sections[idx].sectionId ?? ''; return Padding( padding: const EdgeInsets.only(top: 4), child: Text( title.length > 8 ? '${title.substring(0, 8)}…' : title, style: const TextStyle(fontSize: 10), ), ); }, )), ), borderData: FlBorderData(show: false), gridData: FlGridData(drawVerticalLine: false), barGroups: List.generate(sections.length, (i) => BarChartGroupData( x: i, barRods: [BarChartRodData( toY: sections[i].views.toDouble(), color: kPrimaryColor, width: 16, borderRadius: const BorderRadius.vertical(top: Radius.circular(4)), )], )), )), ), ], ), ), ); } static const _chartColors = [ Color(0xFF264863), Color(0xFF4A8FAD), Color(0xFF87C4D8), Color(0xFFC2C9D6), Color(0xFF8BA7B8), Color(0xFF1A3347), ]; Widget _buildDonut(Map data, String title) { if (data.isEmpty) return const SizedBox(); final total = data.values.fold(0, (a, b) => a + b); final entries = data.entries.toList(); return Card( elevation: 0, color: kWhite, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), child: Padding( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ Text(title, style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: kPrimaryColor)), const SizedBox(height: 16), SizedBox( height: 160, child: PieChart(PieChartData( centerSpaceRadius: 45, sectionsSpace: 2, sections: List.generate(entries.length, (i) { final pct = total > 0 ? (entries[i].value / total * 100).round() : 0; return PieChartSectionData( value: entries[i].value.toDouble(), color: _chartColors[i % _chartColors.length], title: '$pct%', titleStyle: const TextStyle(fontSize: 11, color: Colors.white, fontWeight: FontWeight.w600), radius: 40, ); }), )), ), const SizedBox(height: 12), ...List.generate(entries.length, (i) => Padding( padding: const EdgeInsets.symmetric(vertical: 2), child: Row(children: [ Container(width: 10, height: 10, decoration: BoxDecoration( color: _chartColors[i % _chartColors.length], shape: BoxShape.circle)), const SizedBox(width: 6), Expanded(child: Text(entries[i].key, style: TextStyle(fontSize: 12, color: kBodyTextColor), overflow: TextOverflow.ellipsis)), Text('${entries[i].value}', style: TextStyle(fontSize: 12, color: kBodyTextColor)), ]), )), ], ), ), ); } Widget _buildTablesRow(StatsSummaryDTO stats) { return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (stats.topPois.isNotEmpty) Expanded(child: _buildPoiTable(stats)), if (stats.topPois.isNotEmpty && stats.topAgendaEvents.isNotEmpty) const SizedBox(width: 12), if (stats.topAgendaEvents.isNotEmpty) Expanded(child: _buildAgendaTable(stats)), if ((stats.topPois.isNotEmpty || stats.topAgendaEvents.isNotEmpty) && stats.quizStats.isNotEmpty) const SizedBox(width: 12), if (stats.quizStats.isNotEmpty) Expanded(child: _buildQuizTable(stats)), if ((stats.topPois.isNotEmpty || stats.topAgendaEvents.isNotEmpty || stats.quizStats.isNotEmpty) && stats.gameStats.isNotEmpty) const SizedBox(width: 12), if (stats.gameStats.isNotEmpty) Expanded(child: _buildGameTable(stats)), ], ); } Widget _buildPoiTable(StatsSummaryDTO stats) { return _tableCard('Top POI', ['POI', 'Taps'], stats.topPois.map((p) => [ p.title ?? p.geoPointId?.toString() ?? '—', '${p.taps}', ]).toList()); } Widget _buildAgendaTable(StatsSummaryDTO stats) { return _tableCard('Top événements agenda', ['Événement', 'Taps'], stats.topAgendaEvents.map((e) => [ e.eventTitle ?? e.eventId ?? '—', '${e.taps}', ]).toList()); } Widget _buildQuizTable(StatsSummaryDTO stats) { return _tableCard('Quiz', ['Section', 'Score moy', 'Complétions'], stats.quizStats.map((q) => [ q.sectionTitle ?? q.sectionId ?? '—', '${q.avgScore.toStringAsFixed(1)} / ${q.totalQuestions}', '${q.completions}', ]).toList()); } Widget _buildGameTable(StatsSummaryDTO stats) { return _tableCard('Jeux', ['Type', 'Complétions', 'Durée moy.'], stats.gameStats.map((g) => [ g.gameType ?? '—', '${g.completions}', _formatDuration(g.avgDurationSeconds), ]).toList()); } Widget _tableCard(String title, List headers, List> rows) { return Card( elevation: 0, color: kWhite, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: kPrimaryColor)), const SizedBox(height: 12), Table( columnWidths: const {0: FlexColumnWidth(2), 1: FlexColumnWidth(1)}, children: [ TableRow( decoration: BoxDecoration(border: Border(bottom: BorderSide(color: kSecond))), children: headers.map((h) => Padding( padding: const EdgeInsets.only(bottom: 8), child: Text(h, style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: kBodyTextColor)), )).toList(), ), ...rows.map((row) => TableRow( children: row.map((cell) => Padding( padding: const EdgeInsets.symmetric(vertical: 6), child: Text(cell, style: TextStyle(fontSize: 13, color: kBodyTextColor), overflow: TextOverflow.ellipsis), )).toList(), )).toList(), ], ), ], ), ), ); } }