import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:isar/isar.dart'; import '../../../core/providers/app_providers.dart'; import '../../../data/local/models/models.dart'; import '../../scoring/application/scoring_service.dart'; /// Pure-data summary of local archery statistics, derived from completed /// [PointRecord]s. All display strings are localized in zh-CN. class StatsSummary { const StatsSummary({ this.todayArrowCount = 0, this.trainingDays = 0, this.totalArrowCount = 0, this.averageScore, this.scoreDistribution = const [], this.hitPoints = const [], }); final int todayArrowCount; final int trainingDays; final int totalArrowCount; final double? averageScore; final List scoreDistribution; final List hitPoints; bool get hasData => totalArrowCount > 0; double get todayCalories => todayArrowCount * 1.6; /// Capped at 10 per PRD: todayArrows * 5 / 60. double get todayIntensityRaw { final v = todayArrowCount * 5 / 60.0; return v > 10 ? 10 : v; } String get todayIntensityLabel { final v = todayIntensityRaw; if (v > 6) return '重度'; if (v >= 4) return '中度'; return '轻度'; } String get averageScoreLabel => averageScore == null ? '-' : averageScore!.toStringAsFixed(1); } /// One bucket in the ring-value distribution. [label] follows the /// "M / X / 1~10" convention from the PRD. class ScoreBucket { const ScoreBucket({required this.label, required this.count}); final String label; final int count; } final statsProvider = FutureProvider((ref) async { // Re-compute whenever the records list changes. ref.watch(pointRecordsProvider); final isar = await ref.watch(isarProvider.future); final records = await isar.pointRecords .filter() .statusEqualTo(PointRecordStatus.completed) .sortByCreatedAtDesc() .findAll(); return _compute(records); }); StatsSummary _compute(List records) { if (records.isEmpty) return const StatsSummary(); final today = DateTime.now(); final todayDate = DateTime(today.year, today.month, today.day); int todayArrows = 0; int totalArrows = 0; double scoreSum = 0; int scoredArrows = 0; final days = {}; final allHits = []; final counts = {}; for (final r in records) { final day = DateTime(r.createdAt.year, r.createdAt.month, r.createdAt.day); days.add(day); totalArrows += r.scores.length; if (day == todayDate) { todayArrows += r.scores.length; } for (final s in r.scores) { scoreSum += s; scoredArrows += 1; final key = _bucketLabel(s); counts[key] = (counts[key] ?? 0) + 1; } allHits.addAll(r.hitPoints); } final order = const [ 'X', '10', '9', '8', '7', '6', '5', '4', '3', '2', '1', 'M', ]; final distribution = []; for (final label in order) { final c = counts[label] ?? 0; if (c > 0) distribution.add(ScoreBucket(label: label, count: c)); } return StatsSummary( todayArrowCount: todayArrows, trainingDays: days.length, totalArrowCount: totalArrows, averageScore: scoredArrows == 0 ? null : scoreSum / scoredArrows, scoreDistribution: distribution, hitPoints: allHits, ); } String _bucketLabel(int score) { if (score == 0) return 'M'; if (score == 10) return '10'; return '$score'; }