feat(exécution): dernière performance et meilleur score par exercice (ticket #81)

Ajoute la migration Drift v17 (colonne sourceExerciseIdSnapshot sur
workout_history_set_results et workout_history_step_results, index
associés), le port ExercisePerformanceReferenceRepository et son
implémentation Drift, et le use case
ExercisePerformanceReferenceUseCase (dernière performance + meilleur
score par métrique active pour un exercice donné).

Validé GO par Main : dart analyze propre (1 lint mineur de style),
178 tests (4 échecs préexistants sans rapport avec ce lot).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-22 07:54:49 +02:00
parent e05405157a
commit ac51c16b51
10 changed files with 1073 additions and 3 deletions

View File

@ -3329,6 +3329,9 @@ final class CloseWorkoutSessionUseCase {
final stepResults = await sessionRepository.listExerciseStepResults(
sessionId,
);
final snapshots = _exerciseSnapshotsById(
session.resolvedTemplateSnapshotJson,
);
final historyId = ids.newId();
final historyResults = _historyResultsFromActiveResults(
historyId: historyId,
@ -3341,6 +3344,7 @@ final class CloseWorkoutSessionUseCase {
final historyStepResults = _historyStepResultsFromActiveResults(
historyId: historyId,
results: stepResults,
resolvedTemplateSnapshotJson: session.resolvedTemplateSnapshotJson,
now: now,
ids: ids,
originDeviceId: originDeviceId,
@ -3377,6 +3381,8 @@ final class CloseWorkoutSessionUseCase {
'scoreInputModeSnapshot': result.scoreInputModeSnapshot.name,
'scoreLabelSnapshot': result.scoreLabelSnapshot,
'scoreUnitSnapshot': result.scoreUnitSnapshot,
'sourceExerciseIdSnapshot':
snapshots[result.exerciseSnapshotId]?.sourceExerciseId,
'status': result.status.name,
},
)
@ -3407,6 +3413,8 @@ final class CloseWorkoutSessionUseCase {
'actualScore': result.actualScore,
'actualScoreTimeMs': result.actualScoreTimeMs,
'note': result.note,
'sourceExerciseIdSnapshot':
snapshots[result.exerciseSnapshotId]?.sourceExerciseId,
},
)
.toList(),
@ -3571,6 +3579,59 @@ void _validateExerciseSteps(List<ExerciseStep> steps) {
}
}
final class ExercisePerformanceReferenceUseCase {
const ExercisePerformanceReferenceUseCase({required this.repository});
final ExercisePerformanceReferenceRepository repository;
Future<ExercisePerformanceReference> getExercisePerformanceReference({
required String exerciseId,
required ActivePerformanceMeasures activeMeasures,
required int currentSetIndex,
}) async {
final hasAnyHistory = await repository.hasAnyCompletedHistoryForExercise(
exerciseId,
);
if (!activeMeasures.hasAny) {
return ExercisePerformanceReference(
hasAnyHistoryForExercise: hasAnyHistory,
);
}
final last = await repository.findLatestSetPerformance(
exerciseId: exerciseId,
activeMeasures: activeMeasures,
currentSetIndex: currentSetIndex,
);
final recordMetric = _recordMetricFor(activeMeasures);
final record = recordMetric == null
? null
: await repository.findBestMetricPerformance(
exerciseId: exerciseId,
metric: recordMetric,
scoreInputMode: activeMeasures.scoreInputMode,
);
return ExercisePerformanceReference(
hasAnyHistoryForExercise: hasAnyHistory,
last: last,
record: record,
);
}
}
PerformanceMetric? _recordMetricFor(ActivePerformanceMeasures measures) {
if (measures.scoreEnabled) {
return PerformanceMetric.score;
}
if (measures.repsEnabled) {
return PerformanceMetric.reps;
}
if (measures.timeEnabled) {
return PerformanceMetric.time;
}
return null;
}
void _validateAuthInput({required String email, required String password}) {
final normalizedEmail = email.trim();
final hasBasicEmailShape = RegExp(
@ -4055,6 +4116,7 @@ List<WorkoutHistorySetResult> _historyResultsFromActiveResults({
result.scoreLabelSnapshot ?? snapshot?.scoreLabelSnapshot,
scoreUnitSnapshot:
result.scoreUnitSnapshot ?? snapshot?.scoreUnitSnapshot,
sourceExerciseIdSnapshot: snapshot?.sourceExerciseId,
startedAt: result.startedAt,
completedAt: result.completedAt,
status: result.status,
@ -4089,6 +4151,7 @@ Map<String, _ResolvedExerciseSnapshot> _exerciseSnapshotsById(
exerciseSnapshotId: id,
programNameSnapshot: programName,
exerciseNameSnapshot: exercise['exerciseNameSnapshot'] as String? ?? id,
sourceExerciseId: exercise['sourceExerciseId'] as String?,
timeEnabled: exercise['timeEnabled'] == true,
repsEnabled: exercise['repsEnabled'] == true,
scoreEnabled: exercise['scoreEnabled'] == true,
@ -4118,6 +4181,7 @@ final class _ResolvedExerciseSnapshot {
required this.exerciseSnapshotId,
required this.programNameSnapshot,
required this.exerciseNameSnapshot,
this.sourceExerciseId,
required this.timeEnabled,
required this.repsEnabled,
required this.scoreEnabled,
@ -4136,6 +4200,7 @@ final class _ResolvedExerciseSnapshot {
final String exerciseSnapshotId;
final String programNameSnapshot;
final String exerciseNameSnapshot;
final String? sourceExerciseId;
final bool timeEnabled;
final bool repsEnabled;
final bool scoreEnabled;
@ -4181,11 +4246,14 @@ List<ExerciseStep> _exerciseStepsFromSnapshot(Object? value) {
List<WorkoutHistoryStepResult> _historyStepResultsFromActiveResults({
required String historyId,
required List<ActiveExerciseStepResult> results,
required String resolvedTemplateSnapshotJson,
required DateTime now,
required IdGenerator ids,
required String originDeviceId,
}) {
final snapshots = _exerciseSnapshotsById(resolvedTemplateSnapshotJson);
return results.map((result) {
final snapshot = snapshots[result.exerciseSnapshotId];
return WorkoutHistoryStepResult(
metadata: _newMetadata(ids, originDeviceId, now),
workoutHistoryId: historyId,
@ -4214,6 +4282,7 @@ List<WorkoutHistoryStepResult> _historyStepResultsFromActiveResults({
actualScore: result.actualScore,
actualScoreTimeMs: result.actualScoreTimeMs,
note: result.note,
sourceExerciseIdSnapshot: snapshot?.sourceExerciseId,
);
}).toList();
}