Files
impact/lib/features/analysis/widgets/score_card.dart
qguillaume 1d8124c8d8 fix: clarifie les libelles de stats et corrige le calcul de l etalement moyen
- score_card: "Pourcentage" -> "Reussite", ajoute "/10" sur la moyenne
- grouping_stats: "Diametre" -> "Etalement", direction du decalage en toutes lettres (Droite, Haut-Droite...) au lieu de D/G/H/B
- statistics: corrige le bug de priorite (... ?? 0 * 100) qui affichait un etalement moyen errone (~0.9% au lieu de ~90%)
- ajoute un bouton d aide reutilisable (MetricInfoButton) expliquant chaque metrique, dont la distinction Precision (distance) / Reussite (score)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 10:27:10 +02:00

201 lines
6.2 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/// 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;
const ScoreCard({
super.key,
required this.totalScore,
required this.shotCount,
this.scoreResult,
required this.targetType,
});
@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(),
MetricInfoButton(
title: 'Score',
explanations: [
MetricExplanation(
'Total',
'Somme des points de tous vos impacts, sur le maximum '
'possible (nombre d\'impacts × $maxScore points).',
),
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),
],
],
),
),
);
}
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]!;
}
}