diff --git a/lib/presentation/workout_execution_screen.dart b/lib/presentation/workout_execution_screen.dart index 351bc73..3227627 100644 --- a/lib/presentation/workout_execution_screen.dart +++ b/lib/presentation/workout_execution_screen.dart @@ -354,48 +354,60 @@ final class _WorkoutExecutionScreenState extends State { } Future _finishSet({required bool skipped}) async { - final score = double.tryParse(_scoreController.text.trim()); - final actualTimeMs = DateTime.now() - .toUtc() - .difference(_seriesStartedAt) - .inMilliseconds; - await widget.activeUseCases.recordCurrentSetResult( - sessionId: _session.metadata.id, - programSnapshotId: _plan.programAt(_position).id, - exerciseSnapshotId: _exercise.id, - programIndex: _position.programIndex, - exerciseIndex: _position.exerciseIndex, - setIndex: _position.setIndex, - actualReps: skipped ? null : (_exercise.repsEnabled ? _reps : null), - actualScore: skipped ? null : (_exercise.scoreEnabled ? score : null), - scoreUnitSnapshot: _exercise.scoreUnit, - actualTimeMs: skipped - ? null - : (_exercise.timeEnabled ? actualTimeMs : null), - ); - - final next = _plan.nextPosition(_position); - if (next == null) { - await _complete(); - return; - } - final shouldRest = _plan.shouldShowRestAfter(_position); - if (shouldRest) { - final rest = await widget.activeUseCases.startRestAfterSet( + try { + final score = double.tryParse(_scoreController.text.trim()); + final actualTimeMs = DateTime.now() + .toUtc() + .difference(_seriesStartedAt) + .inMilliseconds; + await widget.activeUseCases.recordCurrentSetResult( sessionId: _session.metadata.id, - afterProgramIndex: _position.programIndex, - afterExerciseIndex: _position.exerciseIndex, - afterSetIndex: _position.setIndex, - plannedRestSeconds: _exercise.restSeconds, + programSnapshotId: _plan.programAt(_position).id, + exerciseSnapshotId: _exercise.id, + programIndex: _position.programIndex, + exerciseIndex: _position.exerciseIndex, + setIndex: _position.setIndex, + actualReps: skipped ? null : (_exercise.repsEnabled ? _reps : null), + actualScore: skipped + ? null + : (_exercise.manualScoreEnabled ? score : null), + scoreInputModeSnapshot: _exercise.scoreInputMode, + scoreLabelSnapshot: _exercise.scoreLabel, + scoreUnitSnapshot: _exercise.scoreUnit, + actualTimeMs: skipped + ? null + : (_exercise.timeEnabled ? actualTimeMs : null), ); + + final next = _plan.nextPosition(_position); + if (next == null) { + await _complete(); + return; + } + final shouldRest = _plan.shouldShowRestAfter(_position); + if (shouldRest) { + final rest = await widget.activeUseCases.startRestAfterSet( + sessionId: _session.metadata.id, + afterProgramIndex: _position.programIndex, + afterExerciseIndex: _position.exerciseIndex, + afterSetIndex: _position.setIndex, + plannedRestSeconds: _exercise.restSeconds, + ); + if (!mounted) return; + _activeRestStateId = rest.metadata.id; + _remainingRestSeconds = _exercise.restSeconds; + _startRestTicker(next); + setState(() => _mode = WorkoutExecutionMode.rest); + return; + } + await _moveTo(next); + } on Exception catch (error) { if (!mounted) return; - _activeRestStateId = rest.metadata.id; - _remainingRestSeconds = _exercise.restSeconds; - _startRestTicker(next); - setState(() => _mode = WorkoutExecutionMode.rest); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Impossible de terminer la série : $error')), + ); return; } - await _moveTo(next); } void _startRestTicker(ExecutionPosition next) { @@ -1344,6 +1356,8 @@ final class ExecutionExercise { required this.repsEnabled, required this.scoreEnabled, required this.restSeconds, + this.scoreInputMode = ScoreInputMode.manual, + this.scoreLabel, this.targetTimeSeconds, this.targetReps, this.targetScore, @@ -1364,6 +1378,10 @@ final class ExecutionExercise { repsEnabled: exercise['repsEnabled'] as bool, scoreEnabled: exercise['scoreEnabled'] as bool, restSeconds: exercise['restSecondsOverride'] as int? ?? 0, + scoreInputMode: _scoreInputModeFromSnapshot( + exercise['scoreInputModeSnapshot'], + ), + scoreLabel: exercise['scoreLabelSnapshot'] as String?, targetTimeSeconds: (override?['targetTimeSecondsOverride'] as int?) ?? (exercise['targetTimeSeconds'] as int?), @@ -1384,10 +1402,23 @@ final class ExecutionExercise { final bool repsEnabled; final bool scoreEnabled; final int restSeconds; + final ScoreInputMode scoreInputMode; + final String? scoreLabel; final int? targetTimeSeconds; final int? targetReps; final double? targetScore; final String? scoreUnit; + + bool get manualScoreEnabled { + return scoreEnabled && scoreInputMode == ScoreInputMode.manual; + } +} + +ScoreInputMode _scoreInputModeFromSnapshot(Object? value) { + return switch (value) { + 'stopwatch' => ScoreInputMode.stopwatch, + _ => ScoreInputMode.manual, + }; } final class ExecutionPosition { diff --git a/test/presentation/workout_execution_screen_test.dart b/test/presentation/workout_execution_screen_test.dart index 7cf8944..573e286 100644 --- a/test/presentation/workout_execution_screen_test.dart +++ b/test/presentation/workout_execution_screen_test.dart @@ -218,6 +218,74 @@ void main() { expect(repository.results.single.actualTimeMs, greaterThan(0)); }); + testWidgets( + 'terminer la série 2 avec temps répétitions et score manuel avance', + (tester) async { + final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12)); + final repository = _FakeActiveSessionRepository(); + final session = ActiveWorkoutSession( + metadata: _metadata('session-1'), + sourceWorkoutTemplateId: 'template-1', + status: ActiveWorkoutStatus.running, + startedAt: DateTime.utc(2026, 7, 17, 12), + lastPersistedAt: DateTime.utc(2026, 7, 17, 12), + elapsedActiveMs: 0, + currentProgramIndex: 0, + currentExerciseIndex: 0, + currentSetIndex: 1, + resolvedTemplateSnapshotJson: _sessionSnapshot( + setsCount: 3, + restSeconds: 0, + targetTimeSeconds: 30, + targetReps: 5, + targetScore: 10, + scoreUnit: 'points', + ), + ); + repository.session = session; + + await tester.pumpWidget( + MaterialApp( + home: WorkoutExecutionScreen( + initialSession: session, + activeUseCases: _activeUseCases(repository, clock), + closeUseCase: _closeUseCase(repository, clock), + historyUseCases: _historyUseCases(clock), + workoutTemplateUseCases: _workoutTemplateUseCases(), + ), + ), + ); + + expect( + find.text('Programme 1/1 · Exercice 1/1 · Série 2/3'), + findsOneWidget, + ); + expect(find.text('30 s'), findsOneWidget); + + for (var i = 0; i < 3; i++) { + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); + } + await tester.enterText( + find.widgetWithText(TextField, 'Score (points)'), + '5', + ); + await tester.ensureVisible(find.text('Terminer la série')); + await tester.pumpAndSettle(); + await tester.tap(find.text('Terminer la série')); + await tester.pumpAndSettle(); + + expect(repository.results.single.setIndex, 1); + expect(repository.results.single.actualReps, 3); + expect(repository.results.single.actualScore, 5); + expect(repository.session?.currentSetIndex, 2); + expect( + find.text('Programme 1/1 · Exercice 1/1 · Série 3/3'), + findsOneWidget, + ); + }, + ); + testWidgets('la flèche de retour met la séance active en pause', ( tester, ) async { @@ -483,7 +551,15 @@ WorkoutTemplateUseCases _workoutTemplateUseCases() { ); } -String _sessionSnapshot({int setsCount = 3, int restSeconds = 0}) { +String _sessionSnapshot({ + int setsCount = 3, + int restSeconds = 0, + int targetTimeSeconds = 45, + int targetReps = 10, + double targetScore = 80, + String scoreUnit = 'kg', + ScoreInputMode scoreInputMode = ScoreInputMode.manual, +}) { return jsonEncode({ 'name': 'Séance jambes', 'programs': [ @@ -499,10 +575,12 @@ String _sessionSnapshot({int setsCount = 3, int restSeconds = 0}) { 'timeEnabled': true, 'repsEnabled': true, 'scoreEnabled': true, - 'targetTimeSeconds': 45, - 'targetReps': 10, - 'targetScore': 80, - 'scoreUnitSnapshot': 'kg', + 'targetTimeSeconds': targetTimeSeconds, + 'targetReps': targetReps, + 'targetScore': targetScore, + 'scoreInputModeSnapshot': scoreInputMode.name, + 'scoreLabelSnapshot': 'Score', + 'scoreUnitSnapshot': scoreUnit, 'restSecondsOverride': restSeconds, }, ],