/// Écran des statistiques détaillées. /// /// Affiche les métriques de performance avec filtrage par période /// (session, semaine, mois, tout). Inclut heat map, précision, /// écart-type et distribution régionale des tirs. library; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../../core/constants/app_constants.dart'; import '../../core/theme/app_theme.dart'; import '../../data/models/session.dart'; import '../../data/repositories/session_repository.dart'; import '../../services/statistics_service.dart'; import 'widgets/heat_map_widget.dart'; class StatisticsScreen extends StatefulWidget { final Session? singleSession; // If provided, show stats for this session only const StatisticsScreen({super.key, this.singleSession}); @override State createState() => _StatisticsScreenState(); } class _StatisticsScreenState extends State { final StatisticsService _statisticsService = StatisticsService(); StatsPeriod _selectedPeriod = StatsPeriod.all; SessionStatistics? _statistics; bool _isLoading = true; List _allSessions = []; @override void initState() { super.initState(); // Use addPostFrameCallback to ensure context is available WidgetsBinding.instance.addPostFrameCallback((_) { _loadStatistics(); }); } Future _loadStatistics() async { if (!mounted) return; setState(() => _isLoading = true); try { if (widget.singleSession != null) { // Single session mode _statistics = _statisticsService.calculateStatistics( [widget.singleSession!], period: StatsPeriod.session, targetCenterX: widget.singleSession!.targetCenterX ?? 0.5, targetCenterY: widget.singleSession!.targetCenterY ?? 0.5, ); } else { // Load all sessions final repository = context.read(); _allSessions = await repository.getAllSessions(); _calculateStats(); } } catch (e) { debugPrint('Error loading statistics: $e'); } if (mounted) { setState(() => _isLoading = false); } } void _calculateStats() { debugPrint('Calculating stats for ${_allSessions.length} sessions, period: $_selectedPeriod'); for (final session in _allSessions) { debugPrint(' Session: ${session.id}, shots: ${session.shots.length}, date: ${session.createdAt}'); } _statistics = _statisticsService.calculateStatistics( _allSessions, period: _selectedPeriod, ); debugPrint('Statistics result: totalShots=${_statistics?.totalShots}, totalScore=${_statistics?.totalScore}'); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.singleSession != null ? 'Statistiques Session' : 'Statistiques'), ), body: _isLoading ? const Center(child: CircularProgressIndicator()) : _statistics == null || _statistics!.totalShots == 0 ? _buildEmptyState() : _buildStatistics(), ); } Widget _buildEmptyState() { return Center( child: Padding( padding: const EdgeInsets.all(AppConstants.defaultPadding), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.analytics_outlined, size: 64, color: Colors.grey.shade400), const SizedBox(height: 16), Text( 'Aucune donnee disponible', style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(height: 8), Text( 'Effectuez des sessions de tir pour voir vos statistiques', textAlign: TextAlign.center, style: TextStyle(color: Colors.grey.shade600), ), const SizedBox(height: 16), Text( 'Sessions trouvees: ${_allSessions.length}', style: TextStyle(color: Colors.grey.shade400, fontSize: 12), ), if (_allSessions.isNotEmpty) Text( 'Tirs totaux: ${_allSessions.fold(0, (sum, s) => sum + s.shots.length)}', style: TextStyle(color: Colors.grey.shade400, fontSize: 12), ), ], ), ), ); } Widget _buildStatistics() { return RefreshIndicator( onRefresh: _loadStatistics, child: SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.all(AppConstants.defaultPadding), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Period filter (only for multi-session view) if (widget.singleSession == null) _buildPeriodFilter(), const SizedBox(height: 16), // Summary cards _buildSummaryCards(), const SizedBox(height: 24), // Heat Map _buildHeatMapSection(), const SizedBox(height: 24), // Precision stats _buildPrecisionSection(), const SizedBox(height: 24), // Standard deviation _buildStdDevSection(), const SizedBox(height: 24), // Regional distribution _buildRegionalSection(), ], ), ), ); } Widget _buildPeriodFilter() { return Card( child: Padding( padding: const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Periode', style: TextStyle(fontWeight: FontWeight.bold), ), const SizedBox(height: 8), SegmentedButton( segments: const [ ButtonSegment( value: StatsPeriod.week, label: Text('7 jours'), icon: Icon(Icons.date_range), ), ButtonSegment( value: StatsPeriod.month, label: Text('30 jours'), icon: Icon(Icons.calendar_month), ), ButtonSegment( value: StatsPeriod.all, label: Text('Tout'), icon: Icon(Icons.all_inclusive), ), ], selected: {_selectedPeriod}, onSelectionChanged: (selection) { setState(() { _selectedPeriod = selection.first; _calculateStats(); }); }, ), const SizedBox(height: 8), Text( '${_statistics!.sessions.length} session(s) - ${_statistics!.totalShots} tir(s)', style: TextStyle(color: Colors.grey.shade600, fontSize: 12), ), ], ), ), ); } Widget _buildSummaryCards() { return Row( children: [ Expanded( child: _StatCard( icon: Icons.gps_fixed, title: 'Tirs', value: '${_statistics!.totalShots}', color: AppTheme.primaryColor, ), ), const SizedBox(width: 12), Expanded( child: _StatCard( icon: Icons.score, title: 'Score Total', value: '${_statistics!.totalScore}', color: AppTheme.secondaryColor, ), ), ], ); } Widget _buildHeatMapSection() { return Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon(Icons.grid_on, color: AppTheme.primaryColor), const SizedBox(width: 8), const Text( 'Zones Chaudes', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), ), ], ), const SizedBox(height: 8), Text( 'Repartition de vos tirs sur la cible', style: TextStyle(color: Colors.grey.shade600, fontSize: 12), ), const SizedBox(height: 16), Center( child: HeatMapWidget( heatMap: _statistics!.heatMap, size: MediaQuery.of(context).size.width - 80, ), ), const SizedBox(height: 12), // Legend - gradient bar Container( height: 24, margin: const EdgeInsets.symmetric(horizontal: 16), decoration: BoxDecoration( borderRadius: BorderRadius.circular(4), gradient: const LinearGradient( colors: [ Color(0xFF2196F3), // Blue (cold) Color(0xFF00BCD4), // Cyan Color(0xFFFFEB3B), // Yellow Color(0xFFFF9800), // Orange Color(0xFFFF1744), // Red (hot) ], ), ), ), const SizedBox(height: 4), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Padding( padding: const EdgeInsets.only(left: 16), child: Text('Peu', style: TextStyle(fontSize: 12, color: Colors.grey.shade600)), ), Padding( padding: const EdgeInsets.only(right: 16), child: Text('Beaucoup', style: TextStyle(fontSize: 12, color: Colors.grey.shade600)), ), ], ), ], ), ), ); } Widget _buildLegendItem(Color color, String label) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 4), child: Row( mainAxisSize: MainAxisSize.min, children: [ Container( width: 16, height: 16, decoration: BoxDecoration( color: color, borderRadius: BorderRadius.circular(2), border: Border.all(color: Colors.grey.shade400), ), ), const SizedBox(width: 4), Text(label, style: const TextStyle(fontSize: 10)), ], ), ); } Widget _buildPrecisionSection() { final precision = _statistics!.precision; return Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon(Icons.center_focus_strong, color: AppTheme.successColor), const SizedBox(width: 8), const Text( 'Precision', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), ), ], ), const SizedBox(height: 16), Row( children: [ Expanded( child: _buildPrecisionGauge( 'Precision', precision.precisionScore, 'Distance moyenne du centre', ), ), const SizedBox(width: 16), Expanded( child: _buildPrecisionGauge( 'Regularite', precision.consistencyScore, 'Groupement des tirs', ), ), ], ), const Divider(height: 32), _buildStatRow('Distance moyenne du centre', '${(precision.avgDistanceFromCenter * 100).toStringAsFixed(1)}%'), _buildStatRow('Diametre de groupement', '${(precision.groupingDiameter * 100).toStringAsFixed(1)}%'), _buildStatRow('Score moyen', _statistics!.avgScore.toStringAsFixed(2)), _buildStatRow('Meilleur score', '${_statistics!.maxScore}'), _buildStatRow('Plus bas score', '${_statistics!.minScore}'), ], ), ), ); } Widget _buildPrecisionGauge(String title, double value, String subtitle) { final color = value > 70 ? AppTheme.successColor : value > 40 ? AppTheme.warningColor : AppTheme.errorColor; return Column( children: [ Stack( alignment: Alignment.center, children: [ SizedBox( width: 80, height: 80, child: CircularProgressIndicator( value: value / 100, strokeWidth: 8, backgroundColor: Colors.grey.shade200, valueColor: AlwaysStoppedAnimation(color), ), ), Text( '${value.toStringAsFixed(0)}', style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, color: color, ), ), ], ), const SizedBox(height: 8), Text( title, style: const TextStyle(fontWeight: FontWeight.bold), ), Text( subtitle, style: TextStyle(fontSize: 10, color: Colors.grey.shade600), textAlign: TextAlign.center, ), ], ); } Widget _buildStdDevSection() { final stdDev = _statistics!.stdDev; return Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon(Icons.stacked_line_chart, color: AppTheme.warningColor), const SizedBox(width: 8), const Text( 'Ecart Type', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), ), ], ), const SizedBox(height: 8), Text( 'Mesure de la dispersion de vos tirs', style: TextStyle(color: Colors.grey.shade600, fontSize: 12), ), const SizedBox(height: 16), _buildStatRow('Ecart type X (horizontal)', '${(stdDev.stdDevX * 100).toStringAsFixed(2)}%'), _buildStatRow('Ecart type Y (vertical)', '${(stdDev.stdDevY * 100).toStringAsFixed(2)}%'), _buildStatRow('Ecart type radial', '${(stdDev.stdDevRadial * 100).toStringAsFixed(2)}%'), _buildStatRow('Ecart type score', stdDev.stdDevScore.toStringAsFixed(2)), const Divider(height: 24), _buildStatRow('Position moyenne X', '${(stdDev.meanX * 100).toStringAsFixed(1)}%'), _buildStatRow('Position moyenne Y', '${(stdDev.meanY * 100).toStringAsFixed(1)}%'), _buildStatRow('Score moyen', stdDev.meanScore.toStringAsFixed(2)), ], ), ), ); } Widget _buildRegionalSection() { final regional = _statistics!.regional; return Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon(Icons.explore, color: AppTheme.secondaryColor), const SizedBox(width: 8), const Text( 'Distribution Regionale', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), ), ], ), const SizedBox(height: 16), // Dominant direction Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: AppTheme.primaryColor.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8), ), child: Row( children: [ const Icon(Icons.compass_calibration, color: AppTheme.primaryColor), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Direction dominante'), Text( regional.dominantDirection, style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 18, ), ), ], ), ), ], ), ), const SizedBox(height: 16), // Bias if (regional.biasX.abs() > 0.02 || regional.biasY.abs() > 0.02) Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: AppTheme.warningColor.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8), ), child: Row( children: [ const Icon(Icons.warning_amber, color: AppTheme.warningColor), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Biais detecte'), Text( _getBiasDescription(regional.biasX, regional.biasY), style: const TextStyle(fontWeight: FontWeight.bold), ), ], ), ), ], ), ), const SizedBox(height: 16), // Sector distribution const Text('Repartition par secteur:', style: TextStyle(fontWeight: FontWeight.bold)), const SizedBox(height: 8), Wrap( spacing: 8, runSpacing: 8, children: regional.sectorDistribution.entries.map((entry) { final percentage = _statistics!.totalShots > 0 ? (entry.value / _statistics!.totalShots * 100) : 0.0; return _buildSectorChip(entry.key, entry.value, percentage); }).toList(), ), const SizedBox(height: 16), // Quadrant distribution const Text('Repartition par quadrant:', style: TextStyle(fontWeight: FontWeight.bold)), const SizedBox(height: 8), _buildQuadrantGrid(regional.quadrantDistribution), ], ), ), ); } Widget _buildStatRow(String label, String value) { return Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(label), Text(value, style: const TextStyle(fontWeight: FontWeight.bold)), ], ), ); } Widget _buildSectorChip(String sector, int count, double percentage) { return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: count > 0 ? AppTheme.primaryColor.withValues(alpha: 0.1) : Colors.grey.shade100, borderRadius: BorderRadius.circular(16), border: Border.all( color: count > 0 ? AppTheme.primaryColor : Colors.grey.shade300, ), ), child: Text( '$sector: $count (${percentage.toStringAsFixed(0)}%)', style: TextStyle( fontSize: 12, color: count > 0 ? AppTheme.primaryColor : Colors.grey.shade600, ), ), ); } Widget _buildQuadrantGrid(Map quadrants) { return Table( border: TableBorder.all(color: Colors.grey.shade300), children: [ TableRow( children: [ _buildQuadrantCell('Haut-Gauche', quadrants['Haut-Gauche'] ?? 0), _buildQuadrantCell('Haut-Droite', quadrants['Haut-Droite'] ?? 0), ], ), TableRow( children: [ _buildQuadrantCell('Bas-Gauche', quadrants['Bas-Gauche'] ?? 0), _buildQuadrantCell('Bas-Droite', quadrants['Bas-Droite'] ?? 0), ], ), ], ); } Widget _buildQuadrantCell(String label, int count) { final percentage = _statistics!.totalShots > 0 ? (count / _statistics!.totalShots * 100) : 0.0; final intensity = _statistics!.totalShots > 0 ? count / _statistics!.totalShots : 0.0; return Container( padding: const EdgeInsets.all(16), color: Color.lerp(Colors.white, AppTheme.primaryColor, intensity * 0.5), child: Column( children: [ Text( '$count', style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 24, ), ), Text( '${percentage.toStringAsFixed(0)}%', style: TextStyle(color: Colors.grey.shade600), ), Text( label, style: const TextStyle(fontSize: 10), textAlign: TextAlign.center, ), ], ), ); } String _getBiasDescription(double biasX, double biasY) { final descriptions = []; if (biasX.abs() > 0.02) { descriptions.add(biasX > 0 ? 'vers la droite' : 'vers la gauche'); } if (biasY.abs() > 0.02) { descriptions.add(biasY > 0 ? 'vers le bas' : 'vers le haut'); } return 'Tendance ${descriptions.join(' et ')}'; } } class _StatCard extends StatelessWidget { final IconData icon; final String title; final String value; final Color color; const _StatCard({ required this.icon, required this.title, required this.value, required this.color, }); @override Widget build(BuildContext context) { return Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( children: [ Icon(icon, color: color, size: 32), const SizedBox(height: 8), Text( value, style: TextStyle( fontSize: 24, fontWeight: FontWeight.bold, color: color, ), ), Text( title, style: TextStyle(color: Colors.grey.shade600), ), ], ), ), ); } }