fix(execution): série qu'on ne pouvait pas terminer (ticket #29)

Corrige le blocage empêchant de terminer une série sur
workout_execution_screen.dart. flutter analyze propre, 42/42 tests
verts, build APK debug validé. Ticket laissé en QA : validation
manuelle sur APK par l'utilisateur avant clôture.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-18 09:12:44 +02:00
parent 116017bda6
commit 1e402a55c6
2 changed files with 151 additions and 42 deletions

View File

@ -354,48 +354,60 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
}
Future<void> _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 {