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:
@ -13,6 +13,7 @@ abstract interface class AppDependencies {
|
||||
ActiveExerciseStepUseCases get activeExerciseStepUseCases;
|
||||
CloseWorkoutSessionUseCase get closeWorkoutSessionUseCase;
|
||||
WorkoutHistoryUseCases get workoutHistoryUseCases;
|
||||
ExercisePerformanceReferenceUseCase get exercisePerformanceReferenceUseCase;
|
||||
SyncUseCases get syncUseCases;
|
||||
ShareUseCases get shareUseCases;
|
||||
}
|
||||
@ -29,6 +30,7 @@ final class AppBootstrap implements AppDependencies {
|
||||
required this.activeExerciseStepUseCases,
|
||||
required this.closeWorkoutSessionUseCase,
|
||||
required this.workoutHistoryUseCases,
|
||||
required this.exercisePerformanceReferenceUseCase,
|
||||
required this.syncUseCases,
|
||||
required this.shareUseCases,
|
||||
required this.syncGateway,
|
||||
@ -54,6 +56,8 @@ final class AppBootstrap implements AppDependencies {
|
||||
@override
|
||||
final WorkoutHistoryUseCases workoutHistoryUseCases;
|
||||
@override
|
||||
final ExercisePerformanceReferenceUseCase exercisePerformanceReferenceUseCase;
|
||||
@override
|
||||
final SyncUseCases syncUseCases;
|
||||
@override
|
||||
final ShareUseCases shareUseCases;
|
||||
@ -77,6 +81,8 @@ final class AppBootstrap implements AppDependencies {
|
||||
final templateRepository = DriftWorkoutTemplateRepository(database);
|
||||
final activeSessionRepository = DriftActiveSessionRepository(database);
|
||||
final historyRepository = DriftWorkoutHistoryRepository(database);
|
||||
final performanceReferenceRepository =
|
||||
DriftExercisePerformanceReferenceRepository(database);
|
||||
final ids = LocalIdGenerator();
|
||||
const clock = SystemClock();
|
||||
const originDeviceId = 'local-device';
|
||||
@ -159,6 +165,9 @@ final class AppBootstrap implements AppDependencies {
|
||||
repository: historyRepository,
|
||||
clock: clock,
|
||||
),
|
||||
exercisePerformanceReferenceUseCase: ExercisePerformanceReferenceUseCase(
|
||||
repository: performanceReferenceRepository,
|
||||
),
|
||||
syncUseCases: SyncUseCases(
|
||||
tokenStore: const SecureStorageAuthTokenStore(),
|
||||
remoteSyncApi: remoteSyncApi,
|
||||
|
||||
@ -466,6 +466,106 @@ abstract interface class WorkoutHistoryRepository {
|
||||
Future<void> delete(String id, DateTime deletedAt);
|
||||
}
|
||||
|
||||
final class ActivePerformanceMeasures {
|
||||
const ActivePerformanceMeasures({
|
||||
required this.timeEnabled,
|
||||
required this.repsEnabled,
|
||||
required this.scoreEnabled,
|
||||
this.scoreInputMode = ScoreInputMode.manual,
|
||||
});
|
||||
|
||||
final bool timeEnabled;
|
||||
final bool repsEnabled;
|
||||
final bool scoreEnabled;
|
||||
final ScoreInputMode scoreInputMode;
|
||||
|
||||
bool get hasAny => timeEnabled || repsEnabled || scoreEnabled;
|
||||
}
|
||||
|
||||
enum PerformanceMetric { score, reps, time }
|
||||
|
||||
final class WorkoutHistorySetPerformance {
|
||||
const WorkoutHistorySetPerformance({
|
||||
required this.workoutHistoryId,
|
||||
required this.startedAt,
|
||||
required this.setIndex,
|
||||
required this.exerciseNameSnapshot,
|
||||
required this.scoreInputModeSnapshot,
|
||||
this.actualTimeMs,
|
||||
this.actualReps,
|
||||
this.actualScore,
|
||||
this.actualScoreTimeMs,
|
||||
this.completedAt,
|
||||
});
|
||||
|
||||
final String workoutHistoryId;
|
||||
final DateTime startedAt;
|
||||
final int setIndex;
|
||||
final String exerciseNameSnapshot;
|
||||
final ScoreInputMode scoreInputModeSnapshot;
|
||||
final int? actualTimeMs;
|
||||
final int? actualReps;
|
||||
final double? actualScore;
|
||||
final int? actualScoreTimeMs;
|
||||
final DateTime? completedAt;
|
||||
}
|
||||
|
||||
final class WorkoutHistoryMetricPerformance {
|
||||
const WorkoutHistoryMetricPerformance({
|
||||
required this.workoutHistoryId,
|
||||
required this.startedAt,
|
||||
required this.setIndex,
|
||||
required this.exerciseNameSnapshot,
|
||||
required this.metric,
|
||||
required this.scoreInputModeSnapshot,
|
||||
this.actualTimeMs,
|
||||
this.actualReps,
|
||||
this.actualScore,
|
||||
this.actualScoreTimeMs,
|
||||
this.completedAt,
|
||||
});
|
||||
|
||||
final String workoutHistoryId;
|
||||
final DateTime startedAt;
|
||||
final int setIndex;
|
||||
final String exerciseNameSnapshot;
|
||||
final PerformanceMetric metric;
|
||||
final ScoreInputMode scoreInputModeSnapshot;
|
||||
final int? actualTimeMs;
|
||||
final int? actualReps;
|
||||
final double? actualScore;
|
||||
final int? actualScoreTimeMs;
|
||||
final DateTime? completedAt;
|
||||
}
|
||||
|
||||
final class ExercisePerformanceReference {
|
||||
const ExercisePerformanceReference({
|
||||
required this.hasAnyHistoryForExercise,
|
||||
this.last,
|
||||
this.record,
|
||||
});
|
||||
|
||||
final bool hasAnyHistoryForExercise;
|
||||
final WorkoutHistorySetPerformance? last;
|
||||
final WorkoutHistoryMetricPerformance? record;
|
||||
}
|
||||
|
||||
abstract interface class ExercisePerformanceReferenceRepository {
|
||||
Future<bool> hasAnyCompletedHistoryForExercise(String exerciseId);
|
||||
|
||||
Future<WorkoutHistorySetPerformance?> findLatestSetPerformance({
|
||||
required String exerciseId,
|
||||
required ActivePerformanceMeasures activeMeasures,
|
||||
required int currentSetIndex,
|
||||
});
|
||||
|
||||
Future<WorkoutHistoryMetricPerformance?> findBestMetricPerformance({
|
||||
required String exerciseId,
|
||||
required PerformanceMetric metric,
|
||||
required ScoreInputMode scoreInputMode,
|
||||
});
|
||||
}
|
||||
|
||||
final class SyncRunSummary {
|
||||
const SyncRunSummary({
|
||||
required this.pushedChanges,
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user