feat(data): édition ponctuelle des séries en séance (ticket #21)
Étend le modèle Drift (tables.dart, app_database.dart/.g.dart) et la couche application (entités, use cases, repositories) pour supporter l'édition ponctuelle des séries pendant l'exécution d'une séance, sans modifier le modèle/séance-modèle. build_runner OK, flutter analyze propre, 30/30 tests verts, build APK debug validé. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -717,11 +717,117 @@ final class ActiveWorkoutSessionUseCases {
|
||||
actualScore: actualScore,
|
||||
scoreLabelSnapshot: scoreLabelSnapshot,
|
||||
scoreUnitSnapshot: scoreUnitSnapshot,
|
||||
status: SetResultStatus.completed,
|
||||
);
|
||||
await sessionRepository.saveSetResult(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<ActiveSetResult> upsertSetResultAtPosition({
|
||||
required String sessionId,
|
||||
required int programIndex,
|
||||
required int exerciseIndex,
|
||||
required int setIndex,
|
||||
required SetResultStatus status,
|
||||
int? actualTimeMs,
|
||||
int? actualReps,
|
||||
double? actualScore,
|
||||
String? scoreLabelSnapshot,
|
||||
String? scoreUnitSnapshot,
|
||||
String? note,
|
||||
}) async {
|
||||
final session = await _requiredSession(sessionId);
|
||||
_ensureEditablePastPosition(
|
||||
session: session,
|
||||
programIndex: programIndex,
|
||||
exerciseIndex: exerciseIndex,
|
||||
setIndex: setIndex,
|
||||
);
|
||||
final snapshot = _findSetSnapshot(
|
||||
resolvedTemplateSnapshotJson: session.resolvedTemplateSnapshotJson,
|
||||
programIndex: programIndex,
|
||||
exerciseIndex: exerciseIndex,
|
||||
setIndex: setIndex,
|
||||
);
|
||||
if (snapshot == null) {
|
||||
throw const DomainException(
|
||||
'Set position not found in session snapshot.',
|
||||
);
|
||||
}
|
||||
final existingResults = await sessionRepository.listSetResults(sessionId);
|
||||
ActiveSetResult? existing;
|
||||
for (final result in existingResults) {
|
||||
if (result.programIndex == programIndex &&
|
||||
result.exerciseIndex == exerciseIndex &&
|
||||
result.setIndex == setIndex) {
|
||||
existing = result;
|
||||
break;
|
||||
}
|
||||
}
|
||||
final now = clock.now();
|
||||
final isSkipped = status == SetResultStatus.skipped;
|
||||
final upserted = ActiveSetResult(
|
||||
metadata: existing == null
|
||||
? _newMetadata(ids, originDeviceId, now)
|
||||
: existing.metadata.touch(now),
|
||||
activeWorkoutSessionId: sessionId,
|
||||
programSnapshotId: snapshot.programSnapshotId,
|
||||
exerciseSnapshotId: snapshot.exerciseSnapshotId,
|
||||
programIndex: programIndex,
|
||||
exerciseIndex: exerciseIndex,
|
||||
setIndex: setIndex,
|
||||
startedAt: existing?.startedAt,
|
||||
completedAt: isSkipped ? null : now,
|
||||
actualTimeMs: isSkipped ? null : actualTimeMs,
|
||||
actualReps: isSkipped ? null : actualReps,
|
||||
actualScore: isSkipped ? null : actualScore,
|
||||
scoreLabelSnapshot: isSkipped
|
||||
? null
|
||||
: scoreLabelSnapshot ?? snapshot.scoreLabelSnapshot,
|
||||
scoreUnitSnapshot: isSkipped
|
||||
? null
|
||||
: scoreUnitSnapshot ?? snapshot.scoreUnitSnapshot,
|
||||
note: note,
|
||||
status: status,
|
||||
);
|
||||
await sessionRepository.saveSetResult(upserted);
|
||||
return upserted;
|
||||
}
|
||||
|
||||
Future<List<SetResultPositionState>> listSetResults(String sessionId) async {
|
||||
final session = await _requiredSession(sessionId);
|
||||
final snapshots = _listSetSnapshots(session.resolvedTemplateSnapshotJson);
|
||||
final results = await sessionRepository.listSetResults(sessionId);
|
||||
final resultsByPosition = {
|
||||
for (final result in results)
|
||||
_positionKey(
|
||||
result.programIndex,
|
||||
result.exerciseIndex,
|
||||
result.setIndex,
|
||||
): result,
|
||||
};
|
||||
return snapshots.map((snapshot) {
|
||||
final result =
|
||||
resultsByPosition[_positionKey(
|
||||
snapshot.programIndex,
|
||||
snapshot.exerciseIndex,
|
||||
snapshot.setIndex,
|
||||
)];
|
||||
return SetResultPositionState(
|
||||
programIndex: snapshot.programIndex,
|
||||
exerciseIndex: snapshot.exerciseIndex,
|
||||
setIndex: snapshot.setIndex,
|
||||
status: result == null
|
||||
? SetPositionStatus.pending
|
||||
: switch (result.status) {
|
||||
SetResultStatus.completed => SetPositionStatus.completed,
|
||||
SetResultStatus.skipped => SetPositionStatus.skipped,
|
||||
},
|
||||
result: result,
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
Future<ActiveWorkoutSession> updateProgress({
|
||||
required String sessionId,
|
||||
required int programIndex,
|
||||
@ -921,6 +1027,7 @@ final class CloseWorkoutSessionUseCase {
|
||||
'actualScore': result.actualScore,
|
||||
'scoreLabelSnapshot': result.scoreLabelSnapshot,
|
||||
'scoreUnitSnapshot': result.scoreUnitSnapshot,
|
||||
'status': result.status.name,
|
||||
},
|
||||
)
|
||||
.toList(),
|
||||
@ -945,6 +1052,24 @@ final class WorkoutHistoryUseCases {
|
||||
Future<void> delete(String id) => repository.delete(id, clock.now());
|
||||
}
|
||||
|
||||
enum SetPositionStatus { pending, completed, skipped }
|
||||
|
||||
final class SetResultPositionState {
|
||||
const SetResultPositionState({
|
||||
required this.programIndex,
|
||||
required this.exerciseIndex,
|
||||
required this.setIndex,
|
||||
required this.status,
|
||||
this.result,
|
||||
});
|
||||
|
||||
final int programIndex;
|
||||
final int exerciseIndex;
|
||||
final int setIndex;
|
||||
final SetPositionStatus status;
|
||||
final ActiveSetResult? result;
|
||||
}
|
||||
|
||||
void _validateOverrideTargets(
|
||||
String programSnapshotJson,
|
||||
WorkoutTemplateExerciseOverrideConfig input,
|
||||
@ -973,6 +1098,132 @@ void _validateOverrideTargets(
|
||||
}
|
||||
}
|
||||
|
||||
void _ensureEditablePastPosition({
|
||||
required ActiveWorkoutSession session,
|
||||
required int programIndex,
|
||||
required int exerciseIndex,
|
||||
required int setIndex,
|
||||
}) {
|
||||
final comparison = _comparePositions(
|
||||
programIndex,
|
||||
exerciseIndex,
|
||||
setIndex,
|
||||
session.currentProgramIndex,
|
||||
session.currentExerciseIndex,
|
||||
session.currentSetIndex,
|
||||
);
|
||||
if (comparison >= 0) {
|
||||
throw const DomainException(
|
||||
'Only past set results can be edited outside the normal execution flow.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
int _comparePositions(
|
||||
int leftProgram,
|
||||
int leftExercise,
|
||||
int leftSet,
|
||||
int rightProgram,
|
||||
int rightExercise,
|
||||
int rightSet,
|
||||
) {
|
||||
final programComparison = leftProgram.compareTo(rightProgram);
|
||||
if (programComparison != 0) {
|
||||
return programComparison;
|
||||
}
|
||||
final exerciseComparison = leftExercise.compareTo(rightExercise);
|
||||
if (exerciseComparison != 0) {
|
||||
return exerciseComparison;
|
||||
}
|
||||
return leftSet.compareTo(rightSet);
|
||||
}
|
||||
|
||||
String _positionKey(int programIndex, int exerciseIndex, int setIndex) {
|
||||
return '$programIndex:$exerciseIndex:$setIndex';
|
||||
}
|
||||
|
||||
_SetPositionSnapshot? _findSetSnapshot({
|
||||
required String resolvedTemplateSnapshotJson,
|
||||
required int programIndex,
|
||||
required int exerciseIndex,
|
||||
required int setIndex,
|
||||
}) {
|
||||
for (final snapshot in _listSetSnapshots(resolvedTemplateSnapshotJson)) {
|
||||
if (snapshot.programIndex == programIndex &&
|
||||
snapshot.exerciseIndex == exerciseIndex &&
|
||||
snapshot.setIndex == setIndex) {
|
||||
return snapshot;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
List<_SetPositionSnapshot> _listSetSnapshots(
|
||||
String resolvedTemplateSnapshotJson,
|
||||
) {
|
||||
final decoded =
|
||||
jsonDecode(resolvedTemplateSnapshotJson) as Map<String, dynamic>;
|
||||
final programs = (decoded['programs'] as List<dynamic>? ?? const []);
|
||||
final snapshots = <_SetPositionSnapshot>[];
|
||||
for (var programIndex = 0; programIndex < programs.length; programIndex++) {
|
||||
final program = programs[programIndex] as Map<String, dynamic>;
|
||||
final programSnapshotId =
|
||||
program['id'] as String? ?? 'program-$programIndex';
|
||||
final programSnapshotJson = program['programSnapshotJson'] as String?;
|
||||
if (programSnapshotJson == null) {
|
||||
continue;
|
||||
}
|
||||
final programSnapshot =
|
||||
jsonDecode(programSnapshotJson) as Map<String, dynamic>;
|
||||
final exercises =
|
||||
(programSnapshot['exercises'] as List<dynamic>? ?? const []);
|
||||
for (
|
||||
var exerciseIndex = 0;
|
||||
exerciseIndex < exercises.length;
|
||||
exerciseIndex++
|
||||
) {
|
||||
final exercise = exercises[exerciseIndex] as Map<String, dynamic>;
|
||||
final exerciseSnapshotId =
|
||||
exercise['id'] as String? ?? 'exercise-$exerciseIndex';
|
||||
final setsCount = exercise['setsCount'] as int? ?? 0;
|
||||
for (var setIndex = 0; setIndex < setsCount; setIndex++) {
|
||||
snapshots.add(
|
||||
_SetPositionSnapshot(
|
||||
programSnapshotId: programSnapshotId,
|
||||
exerciseSnapshotId: exerciseSnapshotId,
|
||||
programIndex: programIndex,
|
||||
exerciseIndex: exerciseIndex,
|
||||
setIndex: setIndex,
|
||||
scoreLabelSnapshot: exercise['scoreLabelSnapshot'] as String?,
|
||||
scoreUnitSnapshot: exercise['scoreUnitSnapshot'] as String?,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return snapshots;
|
||||
}
|
||||
|
||||
final class _SetPositionSnapshot {
|
||||
const _SetPositionSnapshot({
|
||||
required this.programSnapshotId,
|
||||
required this.exerciseSnapshotId,
|
||||
required this.programIndex,
|
||||
required this.exerciseIndex,
|
||||
required this.setIndex,
|
||||
this.scoreLabelSnapshot,
|
||||
this.scoreUnitSnapshot,
|
||||
});
|
||||
|
||||
final String programSnapshotId;
|
||||
final String exerciseSnapshotId;
|
||||
final int programIndex;
|
||||
final int exerciseIndex;
|
||||
final int setIndex;
|
||||
final String? scoreLabelSnapshot;
|
||||
final String? scoreUnitSnapshot;
|
||||
}
|
||||
|
||||
List<WorkoutHistorySetResult> _historyResultsFromActiveResults({
|
||||
required String historyId,
|
||||
required List<ActiveSetResult> results,
|
||||
@ -1012,6 +1263,7 @@ List<WorkoutHistorySetResult> _historyResultsFromActiveResults({
|
||||
result.scoreUnitSnapshot ?? snapshot?.scoreUnitSnapshot,
|
||||
startedAt: result.startedAt,
|
||||
completedAt: result.completedAt,
|
||||
status: result.status,
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user