/// Carte d'affichage des scores. /// /// Affiche le score total, le nombre d'impacts, la moyenne et le pourcentage. /// Inclut la distribution des scores par valeur (1-10 pour cibles concentriques). library; import 'package:flutter/material.dart'; import '../../../core/constants/app_constants.dart'; import '../../../core/theme/app_theme.dart'; import '../../../core/widgets/metric_info_button.dart'; import '../../../data/models/target_type.dart'; import '../../../services/score_calculator_service.dart'; class ScoreCard extends StatelessWidget { final int totalScore; final int shotCount; final ScoreResult? scoreResult; final TargetType targetType; /// Score cumulé de la session (cibles déjà validées + cible en cours). /// /// null hors session : le bandeau de session n'est alors pas affiché. final int? sessionTotalScore; /// Nombre de cibles comptabilisées dans [sessionTotalScore]. final int sessionTargetCount; const ScoreCard({ super.key, required this.totalScore, required this.shotCount, this.scoreResult, required this.targetType, this.sessionTotalScore, this.sessionTargetCount = 0, }); @override Widget build(BuildContext context) { final maxScore = targetType == TargetType.concentric ? 10 : 5; return Card( child: Padding( padding: const EdgeInsets.all(AppConstants.defaultPadding), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon(Icons.scoreboard, color: AppTheme.primaryColor), const SizedBox(width: 8), Text( 'Score', style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, ), ), const Spacer(), if (sessionTotalScore != null) ...[ Flexible(child: _buildSessionBadge(context)), const SizedBox(width: 4), ], MetricInfoButton( title: 'Score', explanations: [ MetricExplanation( 'Total', 'Somme des points de tous vos impacts sur CETTE cible, ' 'sur le maximum possible (nombre d\'impacts × ' '$maxScore points).', ), const MetricExplanation( 'Session', 'Score cumulé de toutes les cibles de la session en ' 'cours, cible affichée comprise.', ), const MetricExplanation( 'Impacts', 'Nombre de tirs détectés sur la cible.', ), MetricExplanation( 'Moyenne', 'Points marqués en moyenne par impact, sur $maxScore.', ), const MetricExplanation( 'Réussite', 'Votre score exprimé en pourcentage du score maximum ' 'possible. C\'est une mesure du résultat, pas de la ' 'régularité des tirs.', ), ], ), ], ), const Divider(), Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ _buildScoreStat( context, 'Total', '$totalScore', subtitle: '/ ${shotCount * maxScore}', ), _buildScoreStat( context, 'Impacts', '$shotCount', ), _buildScoreStat( context, 'Moyenne', shotCount > 0 ? (totalScore / shotCount).toStringAsFixed(1) : '-', subtitle: '/ $maxScore', ), if (scoreResult != null) _buildScoreStat( context, 'Réussite', '${scoreResult!.percentage.toStringAsFixed(0)}%', ), ], ), if (scoreResult != null && scoreResult!.scoreDistribution.isNotEmpty) ...[ const SizedBox(height: 12), const Divider(), const SizedBox(height: 8), Text( 'Distribution des scores', style: Theme.of(context).textTheme.bodySmall?.copyWith( fontWeight: FontWeight.bold, ), ), const SizedBox(height: 8), _buildScoreDistribution(context), ], ], ), ), ); } /// Bandeau compact rappelant le score total de la session en cours. Widget _buildSessionBadge(BuildContext context) { // Le nombre de cibles n'est rappelé qu'à partir de la deuxième : sur la // première il n'apporte rien et allonge le bandeau pour rien. final cibles = sessionTargetCount > 1 ? ' ($sessionTargetCount cibles)' : ''; return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: AppTheme.primaryColor.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(12), ), child: Text( 'Session : $sessionTotalScore pts$cibles', maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.bodySmall?.copyWith( fontWeight: FontWeight.bold, color: AppTheme.primaryColor, ), ), ); } Widget _buildScoreStat( BuildContext context, String label, String value, { String? subtitle, }) { return Column( children: [ Text( value, style: Theme.of(context).textTheme.headlineSmall?.copyWith( fontWeight: FontWeight.bold, color: AppTheme.primaryColor, ), ), if (subtitle != null) Text( subtitle, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Colors.grey, ), ), Text( label, style: Theme.of(context).textTheme.bodySmall, ), ], ); } Widget _buildScoreDistribution(BuildContext context) { final maxScoreValue = targetType == TargetType.concentric ? 10 : 5; final distribution = scoreResult!.scoreDistribution; return Wrap( spacing: 8, runSpacing: 4, children: List.generate(maxScoreValue + 1, (index) { final score = maxScoreValue - index; final count = distribution[score] ?? 0; if (count == 0) return const SizedBox.shrink(); return Chip( label: Text( 'x$count', style: const TextStyle(fontSize: 12), ), avatar: CircleAvatar( radius: 12, backgroundColor: _getScoreColor(score, maxScoreValue), child: Text( '$score', style: const TextStyle( fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold, ), ), ), visualDensity: VisualDensity.compact, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, ); }), ); } Color _getScoreColor(int score, int maxScore) { if (score == maxScore) return Colors.amber; if (score >= maxScore * 0.8) return Colors.orange; if (score >= maxScore * 0.6) return Colors.blue; if (score >= maxScore * 0.4) return Colors.green; if (score >= maxScore * 0.2) return Colors.grey; return Colors.grey[400]!; } }