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:
2026-07-18 00:19:02 +02:00
parent 7eb8f260e0
commit abfbb64de2
7 changed files with 621 additions and 15 deletions

View File

@ -209,6 +209,123 @@ void main() {
expect(active?.metadata.id, 'latest-active');
});
test('upsertSetResultAtPosition does not move session cursor', () async {
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
final session = _sessionWithSnapshot(
currentProgramIndex: 0,
currentExerciseIndex: 0,
currentSetIndex: 1,
);
final repository = _FakeActiveSessionRepository()..session = session;
final useCase = _activeUseCase(repository, clock);
await useCase.upsertSetResultAtPosition(
sessionId: session.metadata.id,
programIndex: 0,
exerciseIndex: 0,
setIndex: 0,
status: SetResultStatus.completed,
actualReps: 12,
);
expect(repository.session?.currentProgramIndex, 0);
expect(repository.session?.currentExerciseIndex, 0);
expect(repository.session?.currentSetIndex, 1);
expect(repository.results.single.actualReps, 12);
});
test(
'upsertSetResultAtPosition preserves existing id and createdAt',
() async {
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
final session = _sessionWithSnapshot(
currentProgramIndex: 0,
currentExerciseIndex: 0,
currentSetIndex: 1,
);
final existingCreatedAt = DateTime.utc(2026, 7, 17, 10);
final existing = ActiveSetResult(
metadata: EntityMetadata(
id: 'result-1',
createdAt: existingCreatedAt,
updatedAt: existingCreatedAt,
originDeviceId: 'device-1',
),
activeWorkoutSessionId: session.metadata.id,
programSnapshotId: 'program-snapshot-1',
exerciseSnapshotId: 'exercise-snapshot-1',
programIndex: 0,
exerciseIndex: 0,
setIndex: 0,
actualReps: 8,
);
final repository = _FakeActiveSessionRepository()
..session = session
..results.add(existing);
final useCase = _activeUseCase(repository, clock);
final updated = await useCase.upsertSetResultAtPosition(
sessionId: session.metadata.id,
programIndex: 0,
exerciseIndex: 0,
setIndex: 0,
status: SetResultStatus.completed,
actualReps: 12,
);
expect(updated.metadata.id, 'result-1');
expect(updated.metadata.createdAt, existingCreatedAt);
expect(updated.actualReps, 12);
},
);
test(
'listSetResults returns pending, skipped and completed statuses',
() async {
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
final session = _sessionWithSnapshot(
currentProgramIndex: 0,
currentExerciseIndex: 0,
currentSetIndex: 2,
setsCount: 3,
);
final repository = _FakeActiveSessionRepository()
..session = session
..results.addAll([
ActiveSetResult(
metadata: _metadata('completed'),
activeWorkoutSessionId: session.metadata.id,
programSnapshotId: 'program-snapshot-1',
exerciseSnapshotId: 'exercise-snapshot-1',
programIndex: 0,
exerciseIndex: 0,
setIndex: 0,
actualReps: 10,
status: SetResultStatus.completed,
),
ActiveSetResult(
metadata: _metadata('skipped'),
activeWorkoutSessionId: session.metadata.id,
programSnapshotId: 'program-snapshot-1',
exerciseSnapshotId: 'exercise-snapshot-1',
programIndex: 0,
exerciseIndex: 0,
setIndex: 1,
status: SetResultStatus.skipped,
),
]);
final useCase = _activeUseCase(repository, clock);
final states = await useCase.listSetResults(session.metadata.id);
expect(states.map((state) => state.status), [
SetPositionStatus.completed,
SetPositionStatus.skipped,
SetPositionStatus.pending,
]);
},
);
}
EntityMetadata _metadata(String id) {
@ -294,13 +411,17 @@ ActiveWorkoutSessionUseCases _activeUseCase(
}
final class _FakeActiveSessionRepository implements ActiveSessionRepository {
ActiveWorkoutSession? session;
final results = <ActiveSetResult>[];
final restStates = <String, ActiveRestState>{};
@override
Future<ActiveWorkoutSession?> findById(String id) async => null;
Future<ActiveWorkoutSession?> findById(String id) async {
return session?.metadata.id == id ? session : null;
}
@override
Future<ActiveWorkoutSession?> findOpen() async => null;
Future<ActiveWorkoutSession?> findOpen() async => session;
@override
Future<ActiveRestState?> findRestStateById(String id) async => restStates[id];
@ -313,11 +434,16 @@ final class _FakeActiveSessionRepository implements ActiveSessionRepository {
}
@override
Future<List<ActiveSetResult>> listSetResults(String sessionId) async =>
const [];
Future<List<ActiveSetResult>> listSetResults(String sessionId) async {
return results
.where((result) => result.activeWorkoutSessionId == sessionId)
.toList();
}
@override
Future<void> save(ActiveWorkoutSession session) async {}
Future<void> save(ActiveWorkoutSession session) async {
this.session = session;
}
@override
Future<void> saveRestState(ActiveRestState restState) async {
@ -325,5 +451,53 @@ final class _FakeActiveSessionRepository implements ActiveSessionRepository {
}
@override
Future<void> saveSetResult(ActiveSetResult result) async {}
Future<void> saveSetResult(ActiveSetResult result) async {
results.removeWhere(
(existing) =>
existing.activeWorkoutSessionId == result.activeWorkoutSessionId &&
existing.programIndex == result.programIndex &&
existing.exerciseIndex == result.exerciseIndex &&
existing.setIndex == result.setIndex,
);
results.add(result);
}
}
ActiveWorkoutSession _sessionWithSnapshot({
required int currentProgramIndex,
required int currentExerciseIndex,
required int currentSetIndex,
int setsCount = 2,
}) {
return ActiveWorkoutSession(
metadata: _metadata('session-1'),
status: ActiveWorkoutStatus.running,
startedAt: DateTime.utc(2026, 7, 17, 12),
lastPersistedAt: DateTime.utc(2026, 7, 17, 12),
elapsedActiveMs: 0,
currentProgramIndex: currentProgramIndex,
currentExerciseIndex: currentExerciseIndex,
currentSetIndex: currentSetIndex,
resolvedTemplateSnapshotJson: jsonEncode({
'programs': [
{
'id': 'program-snapshot-1',
'programNameSnapshot': 'Programme',
'programSnapshotJson': jsonEncode({
'exercises': [
{
'id': 'exercise-snapshot-1',
'exerciseNameSnapshot': 'Squat',
'setsCount': setsCount,
'timeEnabled': false,
'repsEnabled': true,
'scoreEnabled': false,
'targetReps': 10,
},
],
}),
},
],
}),
);
}