feat(statistiques): implémentation backend statistiques de progression (ticket #82)

Lots B1+B2+B3 : schéma/domaine, requêtes d'agrégation et ports/use cases pour les statistiques de progression, avec tests associés.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-22 08:35:47 +02:00
parent fdb301f746
commit 520f227e82
8 changed files with 1538 additions and 7 deletions

View File

@ -27,6 +27,232 @@ final class StarterContent {
final WorkoutTemplate workoutTemplate;
}
enum ProgressionPeriod { fourWeeks, threeMonths, all }
enum ProgressionMeasure { manualScore, stopwatchScore, reps, time }
enum ProgressionRangeLabelKind { sinceDate, sinceFirstSession, none }
enum ProgressionSeriesState {
ready,
singlePoint,
emptyForPeriod,
emptyButHasAllTimeData,
noGraphableMeasure,
textScoreOnly,
}
enum ProgressionSeriesSummaryKind { bestLast, totals, none }
final class ProgressionDateRange {
const ProgressionDateRange({required this.startedAt, required this.endedAt});
final DateTime? startedAt;
final DateTime endedAt;
}
final class ProgressionGlobalStatsData {
const ProgressionGlobalStatsData({
required this.completedSessionCount,
required this.totalActiveMs,
required this.activeWeekStarts,
required this.hasAnyCompletedHistory,
this.firstCompletedSessionAt,
});
final int completedSessionCount;
final int totalActiveMs;
final List<DateTime> activeWeekStarts;
final bool hasAnyCompletedHistory;
final DateTime? firstCompletedSessionAt;
}
final class ProgressionExerciseOptionData {
const ProgressionExerciseOptionData({
required this.exerciseKey,
required this.nameSnapshot,
required this.isArchived,
required this.lastPerformedAt,
});
final String exerciseKey;
final String nameSnapshot;
final bool isArchived;
final DateTime lastPerformedAt;
}
final class ProgressionMeasureOptionData {
const ProgressionMeasureOptionData({
required this.measure,
required this.label,
this.scoreLabel,
this.scoreUnit,
required this.lowerIsBetter,
});
final ProgressionMeasure measure;
final String label;
final String? scoreLabel;
final String? scoreUnit;
final bool lowerIsBetter;
}
final class ProgressionExerciseSeriesData {
const ProgressionExerciseSeriesData({
required this.exerciseKey,
required this.exerciseNameSnapshot,
required this.measure,
required this.points,
required this.hasAnyAllTimeData,
required this.hasAnyCompletedExerciseResult,
});
final String exerciseKey;
final String exerciseNameSnapshot;
final ProgressionMeasureOptionData measure;
final List<ProgressionPointData> points;
final bool hasAnyAllTimeData;
final bool hasAnyCompletedExerciseResult;
}
final class ProgressionPointData {
const ProgressionPointData({
required this.workoutHistoryId,
required this.workoutNameSnapshot,
required this.startedAt,
required this.value,
required this.rawValue,
});
final String workoutHistoryId;
final String workoutNameSnapshot;
final DateTime startedAt;
final double value;
final Object rawValue;
}
abstract interface class ProgressionStatsRepository {
Future<ProgressionGlobalStatsData> readGlobalStats(
ProgressionDateRange range,
);
Future<List<ProgressionExerciseOptionData>> listExerciseOptions(
ProgressionDateRange range,
);
Future<List<ProgressionMeasureOptionData>> listMeasureOptions({
required ProgressionDateRange range,
required String exerciseKey,
});
Future<ProgressionExerciseSeriesData> readExerciseSeries({
required ProgressionDateRange range,
required String exerciseKey,
required ProgressionMeasure measure,
});
}
final class ProgressionOverview {
const ProgressionOverview({
required this.period,
required this.rangeLabelKind,
required this.completedSessionCount,
required this.totalActiveMs,
required this.activeWeekCount,
required this.totalWeekCount,
required this.hasAnyCompletedHistory,
required this.exerciseOptions,
});
final ProgressionPeriod period;
final ProgressionRangeLabelKind rangeLabelKind;
final int completedSessionCount;
final int totalActiveMs;
final int activeWeekCount;
final int? totalWeekCount;
final bool hasAnyCompletedHistory;
final List<ProgressionExerciseOption> exerciseOptions;
}
final class ProgressionExerciseOption {
const ProgressionExerciseOption({
required this.exerciseKey,
required this.nameSnapshot,
required this.isArchived,
required this.lastPerformedAt,
required this.measures,
});
final String exerciseKey;
final String nameSnapshot;
final bool isArchived;
final DateTime lastPerformedAt;
final List<ProgressionMeasureOption> measures;
}
final class ProgressionMeasureOption {
const ProgressionMeasureOption({
required this.measure,
required this.label,
this.scoreLabel,
this.scoreUnit,
required this.lowerIsBetter,
});
final ProgressionMeasure measure;
final String label;
final String? scoreLabel;
final String? scoreUnit;
final bool lowerIsBetter;
}
final class ProgressionExerciseSeries {
const ProgressionExerciseSeries({
required this.exerciseKey,
required this.exerciseNameSnapshot,
required this.measure,
required this.points,
required this.summary,
required this.state,
});
final String exerciseKey;
final String exerciseNameSnapshot;
final ProgressionMeasureOption measure;
final List<ProgressionPoint> points;
final ProgressionSeriesSummary summary;
final ProgressionSeriesState state;
}
final class ProgressionPoint {
const ProgressionPoint({
required this.workoutHistoryId,
required this.workoutNameSnapshot,
required this.startedAt,
required this.value,
required this.rawValue,
});
final String workoutHistoryId;
final String workoutNameSnapshot;
final DateTime startedAt;
final double value;
final Object rawValue;
}
final class ProgressionSeriesSummary {
const ProgressionSeriesSummary({
required this.kind,
this.best,
this.last,
this.recentTotal,
this.lastSessionTotal,
});
final ProgressionSeriesSummaryKind kind;
final Object? best;
final Object? last;
final Object? recentTotal;
final Object? lastSessionTotal;
}
abstract interface class StarterSeedStateRepository {
Future<int> readAppliedStarterSeedVersion();
Future<void> writeAppliedStarterSeedVersion(int version, DateTime appliedAt);