import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import 'package:manager_app/l10n/app_localizations.dart'; import 'package:manager_app/Models/managerContext.dart'; import 'package:manager_app/app_context.dart'; import 'package:manager_app/Components/common_loader.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(context), const SizedBox(height: 16), Expanded( child: FutureBuilder( future: _future, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const CommonLoader(); } if (snapshot.hasError || snapshot.data == null) { return Center(child: Text(AppLocalizations.of(context)!.statsLoadError, style: TextStyle(color: kBodyTextColor))); } final stats = snapshot.data!; return _buildDashboard(context, stats); }, ), ), ], ), ); } bool get _hasAdvancedStats => _managerAppContext.instanceDTO?.hasAdvancedStats ?? false; int get _statsHistoryDays => _managerAppContext.instanceDTO?.statsHistoryDays ?? 30; Widget _buildHeader(BuildContext context) { final l = AppLocalizations.of(context)!; final historyDays = _statsHistoryDays; final maxDays = historyDays == 0 ? 999 : historyDays; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(l.statisticsTitle, 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')), const ButtonSegment(value: 30, label: Text('30j')), ButtonSegment(value: 90, label: Text('90j'), enabled: maxDays >= 90), ], selected: {_selectedDays}, onSelectionChanged: (s) { _selectedDays = s.first; _load(); }, ), ], ), const SizedBox(height: 10), Wrap( spacing: 8, children: [ ChoiceChip( label: Text(l.statsAll), 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(BuildContext context, StatsSummaryDTO stats) { final l = AppLocalizations.of(context)!; 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( l.statsNoData, style: TextStyle(fontSize: 15, color: kBodyTextColor), ), if (_selectedAppType != null) ...[ const SizedBox(height: 8), Text( l.statsNoDataForType(_selectedAppType!.name), style: TextStyle(fontSize: 13, color: kBodyTextColor.withValues(alpha: 0.6)), ), ], ], ), ); } return SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildKpiRow(context, stats), const SizedBox(height: 20), _buildLineChart(context, stats), const SizedBox(height: 20), _buildChartsRow(context, stats), const SizedBox(height: 20), _buildTablesRow(context, stats), ], ), ); } Widget _buildKpiRow(BuildContext context, StatsSummaryDTO stats) { final l = AppLocalizations.of(context)!; 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(l.statsSessions, '${stats.totalSessions}', Icons.people_outline), const SizedBox(width: 12), _kpiCard(l.statsAvgDuration, _formatDuration(stats.avgVisitDurationSeconds), Icons.timer_outlined), const SizedBox(width: 12), _kpiCard(l.statsTopApp, topAppType?.key ?? '—', Icons.phone_iphone), const SizedBox(width: 12), _kpiCard(l.statsTopLang, 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(BuildContext context, StatsSummaryDTO stats) { final l = AppLocalizations.of(context)!; 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 mobileHasData = mobileSpots.any((s) => s.y > 0); final tabletHasData = tabletSpots.any((s) => s.y > 0); final showBreakdown = _selectedAppType == null && (mobileHasData || tabletHasData); 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(l.statsVisitsByDay, style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: kPrimaryColor)), const SizedBox(height: 4), if (showBreakdown) Row(children: [ if (mobileHasData) ...[_legendDot(kPrimaryColor), const SizedBox(width: 4), Text('Mobile', style: TextStyle(fontSize: 12, color: kBodyTextColor)), const SizedBox(width: 12)], if (tabletHasData) ...[_legendDot(kSecond), const SizedBox(width: 4), Text('Tablet', style: TextStyle(fontSize: 12, color: kBodyTextColor))], ]), const SizedBox(height: 16), SizedBox( height: 200, child: LineChart(LineChartData( minY: 0, 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: showBreakdown ? [ if (mobileHasData) _lineBar(mobileSpots, kPrimaryColor), if (tabletHasData) _lineBar(tabletSpots, kSecond), ] : [_lineBar(spots, kPrimaryColor)], )), ), ], ), ), ); } 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: spots.length <= 1), belowBarData: BarAreaData(show: true, color: color.withValues(alpha: 0.1)), ); } Widget _buildChartsRow(BuildContext context, StatsSummaryDTO stats) { final l = AppLocalizations.of(context)!; final showAppTypeDonut = _selectedAppType == null; return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded(flex: 2, child: _buildTopSectionsChart(context, stats)), const SizedBox(width: 12), if (showAppTypeDonut) ...[ Expanded(child: _buildDonut(stats.appTypeDistribution, l.statsApps)), const SizedBox(width: 12), ], Expanded(child: _buildDonut(stats.languageDistribution, l.statsLanguages)), ], ); } Widget _buildTopSectionsChart(BuildContext context, StatsSummaryDTO stats) { final l = AppLocalizations.of(context)!; 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(l.statsTopSections, 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 raw = sections[idx].sectionTitle ?? sections[idx].sectionId ?? ''; final title = raw.replaceAll(RegExp(r'<[^>]*>'), '').trim(); 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(BuildContext context, StatsSummaryDTO stats) { if (!_hasAdvancedStats) { return _buildPremiumLockedCard(context); } final tables = [ if (stats.topPois.isNotEmpty) Expanded(child: _buildPoiTable(context, stats)), if (stats.topAgendaEvents.isNotEmpty) Expanded(child: _buildAgendaTable(context, stats)), if (stats.quizStats.isNotEmpty) Expanded(child: _buildQuizTable(context, stats)), if (stats.gameStats.isNotEmpty) Expanded(child: _buildGameTable(context, stats)), if (stats.topArticles.isNotEmpty) Expanded(child: _buildArticleTable(context, stats)), if (stats.topMenuItems.isNotEmpty) Expanded(child: _buildMenuTable(context, stats)), if (stats.qrScans.totalScans > 0) Expanded(child: _buildQrCard(context, stats)), ]; if (tables.isEmpty) return const SizedBox(); final spaced = []; for (var i = 0; i < tables.length; i++) { spaced.add(tables[i]); if (i < tables.length - 1) spaced.add(const SizedBox(width: 12)); } return Row(crossAxisAlignment: CrossAxisAlignment.start, children: spaced); } Widget _buildPremiumLockedCard(BuildContext context) { return Card( elevation: 0, color: kWhite, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), child: Padding( padding: const EdgeInsets.all(24), child: Row( children: [ Icon(Icons.lock_outline, color: kBodyTextColor.withValues(alpha: 0.5), size: 28), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Statistiques avancées', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: kPrimaryColor)), const SizedBox(height: 4), Text( 'Les stats détaillées (POI, quiz, jeux, articles, QR…) sont disponibles avec le plan Premium.', style: TextStyle(fontSize: 13, color: kBodyTextColor), ), ], ), ), ], ), ), ); } Widget _buildPoiTable(BuildContext context, StatsSummaryDTO stats) { final l = AppLocalizations.of(context)!; return _tableCard(l.statsTopPOI, [l.statsPOI, l.statsTaps], stats.topPois.map((p) => [ p.title ?? p.geoPointId?.toString() ?? '—', '${p.taps}', ]).toList()); } Widget _buildAgendaTable(BuildContext context, StatsSummaryDTO stats) { final l = AppLocalizations.of(context)!; return _tableCard(l.statsTopAgenda, [l.statsEvent, l.statsTaps], stats.topAgendaEvents.map((e) => [ e.eventTitle ?? e.eventId ?? '—', '${e.taps}', ]).toList()); } Widget _buildQuizTable(BuildContext context, StatsSummaryDTO stats) { final l = AppLocalizations.of(context)!; return _tableCard(l.statsQuiz, [l.statsSection, l.statsAvgScore, l.statsCompletions], stats.quizStats.map((q) => [ q.sectionTitle ?? q.sectionId ?? '—', '${q.avgScore.toStringAsFixed(1)} / ${q.totalQuestions}', '${q.completions}', ]).toList()); } Widget _buildGameTable(BuildContext context, StatsSummaryDTO stats) { final l = AppLocalizations.of(context)!; return _tableCard(l.statsGames, [l.statsGameType, l.statsCompletions, l.statsAvgDuration], stats.gameStats.map((g) => [ g.gameType ?? '—', '${g.completions}', _formatDuration(g.avgDurationSeconds), ]).toList()); } Widget _buildArticleTable(BuildContext context, StatsSummaryDTO stats) { final l = AppLocalizations.of(context)!; return _tableCard(l.statsArticles, [l.statsSection, l.statsReadings], stats.topArticles.map((a) => [ a.sectionId ?? '—', '${a.reads}', ]).toList()); } Widget _buildMenuTable(BuildContext context, StatsSummaryDTO stats) { final l = AppLocalizations.of(context)!; return _tableCard(l.statsMenuTitle, [l.statsMenuItem, l.statsTaps], stats.topMenuItems.map((m) => [ m.menuItemTitle ?? m.targetSectionId ?? '—', '${m.taps}', ]).toList()); } Widget _buildQrCard(BuildContext context, StatsSummaryDTO stats) { final l = AppLocalizations.of(context)!; final qr = stats.qrScans; 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(l.statsQrScans, style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: kPrimaryColor)), const SizedBox(height: 12), _qrRow(Icons.qr_code_scanner, l.statsTotal, '${qr.totalScans}', kPrimaryColor), const SizedBox(height: 8), _qrRow(Icons.check_circle_outline, l.statsValid, '${qr.validScans}', Colors.green.shade600), const SizedBox(height: 8), _qrRow(Icons.cancel_outlined, l.statsInvalid, '${qr.invalidScans}', Colors.red.shade400), ], ), ), ); } Widget _qrRow(IconData icon, String label, String value, Color color) { return Row(children: [ Icon(icon, size: 16, color: color), const SizedBox(width: 6), Expanded(child: Text(label, style: TextStyle(fontSize: 13, color: kBodyTextColor))), Text(value, style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: color)), ]); } 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(), ], ), ], ), ), ); } }