merge(main): édition ponctuelle des séries en séance (ticket #21)
Fusionne feature/#21-edition-ponctuelle-backend — build_runner OK, 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:
@ -5,3 +5,5 @@
|
||||
- [gametime-architecture-initial-stack-data-model](gametime-architecture-initial-stack-data-model.md) — memory note gametime-architecture-initial-stack-data-model
|
||||
- [gametime-dev-environment](gametime-dev-environment.md) — memory note gametime-dev-environment
|
||||
- [gametime-visual-identity](gametime-visual-identity.md) — memory note gametime-visual-identity
|
||||
- [gametime-ux-execution-nav-and-program-simplification](gametime-ux-execution-nav-and-program-simplification.md) — memory note gametime-ux-execution-nav-and-program-simplification
|
||||
- [gametime-architecture-set-editing](gametime-architecture-set-editing.md) — memory note gametime-architecture-set-editing
|
||||
|
||||
25
.ideai/memory/gametime-architecture-set-editing.md
Normal file
25
.ideai/memory/gametime-architecture-set-editing.md
Normal file
@ -0,0 +1,25 @@
|
||||
---
|
||||
name: gametime-architecture-set-editing
|
||||
description: memory note gametime-architecture-set-editing
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
# GameTime — Cadrage Architect : édition ponctuelle de séries (2026-07-18)
|
||||
|
||||
Réponse d'Architect au besoin UX de navigation libre dans une séance active (mémoire "gametime-ux-execution-nav-and-program-simplification").
|
||||
|
||||
## Ce qui existe déjà et est réutilisable
|
||||
- `ActiveSetResults` (Drift) a déjà `programIndex`, `exerciseIndex`, `setIndex` avec contrainte `UNIQUE (active_workout_session_id, program_index, exercise_index, set_index)` — l'identification positionnelle d'une série est stable, un upsert par position est possible sans ambiguïté.
|
||||
- `recordCurrentSetResult` n'avance pas le curseur de progression lui-même (c'est `updateProgress`, séparé) mais reste sémantiquement dédié à la série courante (recrée un résultat avec nouvel ID/métadonnées) — pas réutilisable tel quel pour l'édition ponctuelle.
|
||||
|
||||
## Ce qui doit être ajouté
|
||||
- Champ `status` sur `ActiveSetResult` (enum `completed | skipped`), avec invariant : `skipped` implique aucune valeur `actual*` renseignée. Migration Drift nécessaire (schemaVersion+1).
|
||||
- Nouveau use case `upsertSetResultAtPosition(...)` : vérifie que la position appartient au snapshot de la séance, conserve l'id/createdAt si une ligne existe déjà à cette position, écrit `completed` ou `skipped`, et **ne touche jamais** `currentProgramIndex/currentExerciseIndex/currentSetIndex`.
|
||||
- Nouveau use case `listSetResults(sessionId)` pour construire l'état du plan de séance (à faire / en cours / terminée / passée par position).
|
||||
|
||||
## Invariants à respecter côté DevBackend
|
||||
- L'édition ponctuelle ne crée, ne relance et ne termine jamais de repos (`ActiveRestState` reste un événement indépendant attaché à la série précédente).
|
||||
- Le temps total de séance reste calculé depuis les horodatages de session, jamais recalculé depuis la liste des résultats.
|
||||
- Pas d'édition de série future en v1 (seulement passé/courant).
|
||||
- Si la position éditée correspond à la position courante, rediriger vers le flux d'exécution normal plutôt que permettre une double édition simultanée.
|
||||
- Propager le statut `skipped` jusqu'à l'historique (WorkoutHistorySetResult) pour que le snapshot final reste lisible et cohérent avec ce qui a été vécu pendant la séance.
|
||||
@ -0,0 +1,40 @@
|
||||
---
|
||||
name: gametime-ux-execution-nav-and-program-simplification
|
||||
description: memory note gametime-ux-execution-nav-and-program-simplification
|
||||
metadata:
|
||||
type: project
|
||||
---
|
||||
# GameTime — Refonte navigation d'exécution + simplification liste programme (UX, 2026-07-18)
|
||||
|
||||
Suite à un retour utilisateur après premier test réel de la v1.
|
||||
|
||||
## Point 1 — Navigation libre dans une séance en cours
|
||||
|
||||
Distinction centrale : **position courante** (où en est réellement la séance) vs **édition ponctuelle** (ouvrir une série passée/passée pour la corriger, sans déplacer le curseur ni relancer un repos/recalcul).
|
||||
|
||||
- Nouvelle action "Voir le plan" sur l'écran d'exécution → bottom sheet plein écran "Plan de séance" listant tous les programmes/exercices/séries de la séance avec leur état : À faire / En cours / Terminée (résumé valeurs) / Passée (Aucun résultat).
|
||||
- Règle d'accessibilité : Terminée et Passée sont tapables → ouvrent une bottom sheet "Modifier la série" (mêmes champs que l'exécution active, boutons Enregistrer / Marquer comme passée / Annuler). En cours → ferme le plan et revient à l'écran actif. À faire → non tapable en v1 (pas de saut vers le futur, seulement correction du passé).
|
||||
- Édition d'une série ne déplace jamais `currentProgramIndex/currentExerciseIndex/currentSetIndex`, ne relance pas de repos, ne recalcule pas le temps total (basé sur la séance, pas la somme des séries).
|
||||
- Repos actif pendant une édition : continue en arrière-plan, bandeau compact "Repos en cours · 00:32" affiché en haut du plan/de l'édition ; s'il arrive à zéro pendant l'édition, bandeau devient "Repos terminé · Reprendre" sans fermer brutalement la vue.
|
||||
- Cas limites : modifier la série courante via le plan → ferme la sheet et revient à l'écran principal (pas de double édition) ; vider une série terminée → confirmation "Supprimer le résultat de cette série ? / Marquer comme passée" ; logique d'édition n'existe que pour la séance active, pas pour l'historique (autre écran).
|
||||
|
||||
Impacts backend identifiés par UX (à faire trancher par Architect avant implémentation) :
|
||||
- `listSetResults(sessionId)` pour construire l'état du plan.
|
||||
- `upsertSetResultAtPosition(...)` distinct de `recordCurrentSetResult(...)` — écrire un résultat à une position arbitraire sans avancer le curseur.
|
||||
- Distinguer explicitement un résultat "skipped" d'un résultat absent (probablement déjà couvert par le modèle ActiveSetResult existant, à vérifier).
|
||||
- Ne jamais appeler la logique d'avancement de progression lors d'une édition ponctuelle.
|
||||
|
||||
## Point 2 — Liste d'exercices de programme simplifiée
|
||||
|
||||
- Par défaut, chaque ligne d'exercice dans un programme n'affiche QUE : poignée de déplacement, nom de l'exercice, repos affiché (ex: "Repos 45 s"), badge "Exercice archivé" si pertinent, et une icône "personnaliser" (tune/edit_note) avec tooltip "Personnaliser l'exercice".
|
||||
- Plus de checkboxes/mesures/cibles/nombre de séries visibles par défaut dans la liste.
|
||||
- L'icône "personnaliser" ouvre un **écran séparé** "Personnaliser l'exercice" (pas une bottom sheet, pas de déplié inline — clavier numérique + plusieurs sections méritent l'espace d'un écran plein) avec : nombre de séries, mesures à suivre (toggles, au moins une active obligatoire), objectifs par mesure activée, repos après chaque série, actions Enregistrer / Supprimer du programme.
|
||||
- Ajout d'un exercice au programme : ajouté immédiatement avec des valeurs par défaut (3 séries, toutes les mesures disponibles de l'exercice actives, repos = défaut du programme), retour à la liste, snackbar "Exercice ajouté" avec action rapide "Personnaliser".
|
||||
- Validation : au moins une mesure active (message inline si tout décoché), repos positif obligatoire, confirmation avant suppression d'un exercice du programme.
|
||||
|
||||
## Découpage proposé par UX (à transformer en tickets)
|
||||
1. Programme · cartes exercice compactes + écran de personnalisation séparé.
|
||||
2. Exécution · plan de séance consultable (lecture seule des états, sans édition).
|
||||
3. Exécution · correction des séries passées/terminées (édition ponctuelle, gestion repos actif pendant édition).
|
||||
|
||||
Point à trancher avec Architect avant d'attaquer les tickets 2 et 3 : le modèle d'écriture d'un résultat de série à une position arbitraire sans avancer le curseur de progression — vérifier que ça ne casse pas les invariants déjà posés (ActiveSetResult, ActiveWorkoutSession, horodatages) définis dans la mémoire "gametime-architecture-initial-stack-data-model" et les tickets #4/#9/#13/#14.
|
||||
6
.ideai/tickets/20/carnet.md
Normal file
6
.ideai/tickets/20/carnet.md
Normal file
@ -0,0 +1,6 @@
|
||||
---
|
||||
issueRef: "#20"
|
||||
version: 2
|
||||
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||
updatedAt: 1784325859788
|
||||
---
|
||||
16
.ideai/tickets/20/issue.md
Normal file
16
.ideai/tickets/20/issue.md
Normal file
@ -0,0 +1,16 @@
|
||||
---
|
||||
id: "8355edb7-bdbd-4f93-820c-d7de0affebef"
|
||||
number: 20
|
||||
title: "[DevFrontend] Programme : cartes exercice compactes + écran de personnalisation séparé"
|
||||
status: "inProgress"
|
||||
priority: "high"
|
||||
sprint: null
|
||||
links: [{"target":"#7","kind":"relatesTo"}]
|
||||
agentRefs: [{"agentId":"9933c93a-b8a1-4164-a3bb-7063fdad747d","role":"assigned"}]
|
||||
createdBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||
createdAt: 1784325837553
|
||||
updatedAt: 1784325859788
|
||||
version: 2
|
||||
---
|
||||
Simplifier la liste d'exercices d'un programme (lib/presentation/program_screen.dart) : chaque ligne n'affiche par défaut que la poignée de déplacement, le nom de l'exercice, le repos affiché, et une icône "personnaliser" (tooltip "Personnaliser l'exercice") ouvrant un écran séparé "Personnaliser l'exercice" avec la configuration complète (séries, mesures, cibles, repos). Ajout d'un exercice = valeurs par défaut immédiates (3 séries, toutes les mesures disponibles actives, repos = défaut du programme) + snackbar "Exercice ajouté" avec action rapide "Personnaliser". Cf. mémoire "gametime-ux-execution-nav-and-program-simplification" point 2 pour le détail complet.
|
||||
6
.ideai/tickets/21/carnet.md
Normal file
6
.ideai/tickets/21/carnet.md
Normal file
@ -0,0 +1,6 @@
|
||||
---
|
||||
issueRef: "#21"
|
||||
version: 3
|
||||
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||
updatedAt: 1784326729918
|
||||
---
|
||||
16
.ideai/tickets/21/issue.md
Normal file
16
.ideai/tickets/21/issue.md
Normal file
@ -0,0 +1,16 @@
|
||||
---
|
||||
id: "2444e487-58c1-4180-9fac-c5d5df9bdd68"
|
||||
number: 21
|
||||
title: "[DevBackend] Modèle et use cases pour l'édition ponctuelle de séries en séance active"
|
||||
status: "closed"
|
||||
priority: "high"
|
||||
sprint: null
|
||||
links: [{"target":"#9","kind":"relatesTo"}]
|
||||
agentRefs: [{"agentId":"10ee045b-1c41-479e-ba03-dceed9edd495","role":"assigned"}]
|
||||
createdBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||
createdAt: 1784325841003
|
||||
updatedAt: 1784326729918
|
||||
version: 3
|
||||
---
|
||||
Ajouter le nécessaire pour permettre de consulter et corriger le résultat d'une série déjà terminée/passée sans déplacer le curseur de progression de la séance. Cf. mémoire "gametime-architecture-set-editing" (cadrage Architect) pour le détail exact : champ status (completed|skipped) sur ActiveSetResult + migration Drift, use case upsertSetResultAtPosition, use case listSetResults(sessionId), propagation du statut skipped vers l'historique.
|
||||
6
.ideai/tickets/22/carnet.md
Normal file
6
.ideai/tickets/22/carnet.md
Normal file
@ -0,0 +1,6 @@
|
||||
---
|
||||
issueRef: "#22"
|
||||
version: 2
|
||||
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||
updatedAt: 1784325851266
|
||||
---
|
||||
16
.ideai/tickets/22/issue.md
Normal file
16
.ideai/tickets/22/issue.md
Normal file
@ -0,0 +1,16 @@
|
||||
---
|
||||
id: "a9ed002f-5e6c-4658-9a1b-ee772ce623ea"
|
||||
number: 22
|
||||
title: "[DevFrontend] Exécution : plan de séance consultable"
|
||||
status: "open"
|
||||
priority: "high"
|
||||
sprint: null
|
||||
links: [{"target":"#9","kind":"relatesTo"},{"target":"#21","kind":"dependsOn"}]
|
||||
agentRefs: [{"agentId":"9933c93a-b8a1-4164-a3bb-7063fdad747d","role":"assigned"}]
|
||||
createdBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||
createdAt: 1784325843738
|
||||
updatedAt: 1784325851266
|
||||
version: 2
|
||||
---
|
||||
Ajouter une action "Voir le plan" sur l'écran d'exécution (lib/presentation/workout_execution_screen.dart) ouvrant une bottom sheet plein écran "Plan de séance" listant tous les programmes/exercices/séries avec leur état (À faire / En cours / Terminée avec résumé / Passée). Dépend du use case listSetResults du ticket backend dédié. Cf. mémoire "gametime-ux-execution-nav-and-program-simplification" point 1.
|
||||
6
.ideai/tickets/23/carnet.md
Normal file
6
.ideai/tickets/23/carnet.md
Normal file
@ -0,0 +1,6 @@
|
||||
---
|
||||
issueRef: "#23"
|
||||
version: 3
|
||||
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||
updatedAt: 1784325855260
|
||||
---
|
||||
16
.ideai/tickets/23/issue.md
Normal file
16
.ideai/tickets/23/issue.md
Normal file
@ -0,0 +1,16 @@
|
||||
---
|
||||
id: "5b61b1a0-6bed-4d51-912f-cedf27f2b1a2"
|
||||
number: 23
|
||||
title: "[DevFrontend] Exécution : édition ponctuelle d'une série passée ou terminée"
|
||||
status: "open"
|
||||
priority: "high"
|
||||
sprint: null
|
||||
links: [{"target":"#9","kind":"relatesTo"},{"target":"#21","kind":"dependsOn"},{"target":"#22","kind":"dependsOn"}]
|
||||
agentRefs: [{"agentId":"9933c93a-b8a1-4164-a3bb-7063fdad747d","role":"assigned"}]
|
||||
createdBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||
createdAt: 1784325846325
|
||||
updatedAt: 1784325855260
|
||||
version: 3
|
||||
---
|
||||
Depuis le plan de séance, permettre de taper une série Terminée ou Passée pour ouvrir une bottom sheet "Modifier la série" (mêmes champs que l'exécution active) et sauvegarder via upsertSetResultAtPosition, sans déplacer le curseur de progression ni affecter les repos/temps total. Gestion du repos actif pendant l'édition (bandeau compact persistant). Cf. mémoire "gametime-ux-execution-nav-and-program-simplification" point 1 pour le détail complet des cas limites.
|
||||
@ -1,3 +1,3 @@
|
||||
{
|
||||
"nextNumber": 20
|
||||
"nextNumber": 24
|
||||
}
|
||||
@ -214,6 +214,54 @@
|
||||
"9933c93a-b8a1-4164-a3bb-7063fdad747d"
|
||||
],
|
||||
"updatedAt": 1784325166560
|
||||
},
|
||||
{
|
||||
"issueRef": "#20",
|
||||
"path": "20",
|
||||
"title": "[DevFrontend] Programme : cartes exercice compactes + écran de personnalisation séparé",
|
||||
"status": "inProgress",
|
||||
"priority": "high",
|
||||
"sprint": null,
|
||||
"assignedAgentIds": [
|
||||
"9933c93a-b8a1-4164-a3bb-7063fdad747d"
|
||||
],
|
||||
"updatedAt": 1784325859788
|
||||
},
|
||||
{
|
||||
"issueRef": "#21",
|
||||
"path": "21",
|
||||
"title": "[DevBackend] Modèle et use cases pour l'édition ponctuelle de séries en séance active",
|
||||
"status": "closed",
|
||||
"priority": "high",
|
||||
"sprint": null,
|
||||
"assignedAgentIds": [
|
||||
"10ee045b-1c41-479e-ba03-dceed9edd495"
|
||||
],
|
||||
"updatedAt": 1784326729918
|
||||
},
|
||||
{
|
||||
"issueRef": "#22",
|
||||
"path": "22",
|
||||
"title": "[DevFrontend] Exécution : plan de séance consultable",
|
||||
"status": "open",
|
||||
"priority": "high",
|
||||
"sprint": null,
|
||||
"assignedAgentIds": [
|
||||
"9933c93a-b8a1-4164-a3bb-7063fdad747d"
|
||||
],
|
||||
"updatedAt": 1784325851266
|
||||
},
|
||||
{
|
||||
"issueRef": "#23",
|
||||
"path": "23",
|
||||
"title": "[DevFrontend] Exécution : édition ponctuelle d'une série passée ou terminée",
|
||||
"status": "open",
|
||||
"priority": "high",
|
||||
"sprint": null,
|
||||
"assignedAgentIds": [
|
||||
"9933c93a-b8a1-4164-a3bb-7063fdad747d"
|
||||
],
|
||||
"updatedAt": 1784325855260
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -8,6 +8,8 @@ enum WorkoutMeasure { time, reps, score }
|
||||
|
||||
enum ActiveWorkoutStatus { running, paused, savedExit, completed, abandoned }
|
||||
|
||||
enum SetResultStatus { completed, skipped }
|
||||
|
||||
final class DomainException implements Exception {
|
||||
const DomainException(this.message);
|
||||
|
||||
@ -572,7 +574,7 @@ final class ActiveWorkoutSession {
|
||||
}
|
||||
|
||||
final class ActiveSetResult {
|
||||
const ActiveSetResult({
|
||||
ActiveSetResult({
|
||||
required this.metadata,
|
||||
required this.activeWorkoutSessionId,
|
||||
required this.programSnapshotId,
|
||||
@ -588,7 +590,13 @@ final class ActiveSetResult {
|
||||
this.scoreLabelSnapshot,
|
||||
this.scoreUnitSnapshot,
|
||||
this.note,
|
||||
});
|
||||
this.status = SetResultStatus.completed,
|
||||
}) {
|
||||
if (status == SetResultStatus.skipped &&
|
||||
(actualTimeMs != null || actualReps != null || actualScore != null)) {
|
||||
throw const DomainException('Skipped set results cannot have values.');
|
||||
}
|
||||
}
|
||||
|
||||
final EntityMetadata metadata;
|
||||
final String activeWorkoutSessionId;
|
||||
@ -605,6 +613,7 @@ final class ActiveSetResult {
|
||||
final String? scoreLabelSnapshot;
|
||||
final String? scoreUnitSnapshot;
|
||||
final String? note;
|
||||
final SetResultStatus status;
|
||||
}
|
||||
|
||||
final class ActiveRestState {
|
||||
@ -682,7 +691,7 @@ final class WorkoutHistory {
|
||||
}
|
||||
|
||||
final class WorkoutHistorySetResult {
|
||||
const WorkoutHistorySetResult({
|
||||
WorkoutHistorySetResult({
|
||||
required this.metadata,
|
||||
required this.workoutHistoryId,
|
||||
required this.programSnapshotId,
|
||||
@ -705,7 +714,15 @@ final class WorkoutHistorySetResult {
|
||||
this.scoreUnitSnapshot,
|
||||
this.startedAt,
|
||||
this.completedAt,
|
||||
});
|
||||
this.status = SetResultStatus.completed,
|
||||
}) {
|
||||
if (status == SetResultStatus.skipped &&
|
||||
(actualTimeMs != null || actualReps != null || actualScore != null)) {
|
||||
throw const DomainException(
|
||||
'Skipped history results cannot have values.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final EntityMetadata metadata;
|
||||
final String workoutHistoryId;
|
||||
@ -729,6 +746,7 @@ final class WorkoutHistorySetResult {
|
||||
final String? scoreUnitSnapshot;
|
||||
final DateTime? startedAt;
|
||||
final DateTime? completedAt;
|
||||
final SetResultStatus status;
|
||||
}
|
||||
|
||||
String _nonBlank(String? value, String label) {
|
||||
|
||||
@ -35,7 +35,7 @@ final class AppDatabase extends _$AppDatabase {
|
||||
}
|
||||
|
||||
@override
|
||||
int get schemaVersion => 1;
|
||||
int get schemaVersion => 2;
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration {
|
||||
@ -44,6 +44,19 @@ final class AppDatabase extends _$AppDatabase {
|
||||
await migrator.createAll();
|
||||
await _createIndexes();
|
||||
},
|
||||
onUpgrade: (migrator, from, to) async {
|
||||
if (from < 2) {
|
||||
await customStatement(
|
||||
'ALTER TABLE active_set_results ADD COLUMN status TEXT NOT NULL '
|
||||
"DEFAULT 'completed' CHECK (status IN ('completed', 'skipped'))",
|
||||
);
|
||||
await customStatement(
|
||||
'ALTER TABLE workout_history_set_results ADD COLUMN status TEXT '
|
||||
"NOT NULL DEFAULT 'completed' CHECK (status IN ('completed', "
|
||||
"'skipped'))",
|
||||
);
|
||||
}
|
||||
},
|
||||
beforeOpen: (details) async {
|
||||
await customStatement('PRAGMA foreign_keys = ON');
|
||||
},
|
||||
|
||||
@ -3578,6 +3578,16 @@ class $ActiveSetResultsTable extends ActiveSetResults
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: false,
|
||||
);
|
||||
static const VerificationMeta _statusMeta = const VerificationMeta('status');
|
||||
@override
|
||||
late final GeneratedColumn<String> status = GeneratedColumn<String>(
|
||||
'status',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: false,
|
||||
defaultValue: const Constant('completed'),
|
||||
);
|
||||
@override
|
||||
List<GeneratedColumn> get $columns => [
|
||||
id,
|
||||
@ -3605,6 +3615,7 @@ class $ActiveSetResultsTable extends ActiveSetResults
|
||||
scoreLabelSnapshot,
|
||||
scoreUnitSnapshot,
|
||||
note,
|
||||
status,
|
||||
];
|
||||
@override
|
||||
String get aliasedName => _alias ?? actualTableName;
|
||||
@ -3837,6 +3848,12 @@ class $ActiveSetResultsTable extends ActiveSetResults
|
||||
note.isAcceptableOrUnknown(data['note']!, _noteMeta),
|
||||
);
|
||||
}
|
||||
if (data.containsKey('status')) {
|
||||
context.handle(
|
||||
_statusMeta,
|
||||
status.isAcceptableOrUnknown(data['status']!, _statusMeta),
|
||||
);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
@ -3946,6 +3963,10 @@ class $ActiveSetResultsTable extends ActiveSetResults
|
||||
DriftSqlType.string,
|
||||
data['${effectivePrefix}note'],
|
||||
),
|
||||
status: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.string,
|
||||
data['${effectivePrefix}status'],
|
||||
)!,
|
||||
);
|
||||
}
|
||||
|
||||
@ -3981,6 +4002,7 @@ class ActiveSetResult extends DataClass implements Insertable<ActiveSetResult> {
|
||||
final String? scoreLabelSnapshot;
|
||||
final String? scoreUnitSnapshot;
|
||||
final String? note;
|
||||
final String status;
|
||||
const ActiveSetResult({
|
||||
required this.id,
|
||||
required this.createdAt,
|
||||
@ -4007,6 +4029,7 @@ class ActiveSetResult extends DataClass implements Insertable<ActiveSetResult> {
|
||||
this.scoreLabelSnapshot,
|
||||
this.scoreUnitSnapshot,
|
||||
this.note,
|
||||
required this.status,
|
||||
});
|
||||
@override
|
||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
@ -4060,6 +4083,7 @@ class ActiveSetResult extends DataClass implements Insertable<ActiveSetResult> {
|
||||
if (!nullToAbsent || note != null) {
|
||||
map['note'] = Variable<String>(note);
|
||||
}
|
||||
map['status'] = Variable<String>(status);
|
||||
return map;
|
||||
}
|
||||
|
||||
@ -4112,6 +4136,7 @@ class ActiveSetResult extends DataClass implements Insertable<ActiveSetResult> {
|
||||
? const Value.absent()
|
||||
: Value(scoreUnitSnapshot),
|
||||
note: note == null && nullToAbsent ? const Value.absent() : Value(note),
|
||||
status: Value(status),
|
||||
);
|
||||
}
|
||||
|
||||
@ -4156,6 +4181,7 @@ class ActiveSetResult extends DataClass implements Insertable<ActiveSetResult> {
|
||||
json['scoreUnitSnapshot'],
|
||||
),
|
||||
note: serializer.fromJson<String?>(json['note']),
|
||||
status: serializer.fromJson<String>(json['status']),
|
||||
);
|
||||
}
|
||||
@override
|
||||
@ -4189,6 +4215,7 @@ class ActiveSetResult extends DataClass implements Insertable<ActiveSetResult> {
|
||||
'scoreLabelSnapshot': serializer.toJson<String?>(scoreLabelSnapshot),
|
||||
'scoreUnitSnapshot': serializer.toJson<String?>(scoreUnitSnapshot),
|
||||
'note': serializer.toJson<String?>(note),
|
||||
'status': serializer.toJson<String>(status),
|
||||
};
|
||||
}
|
||||
|
||||
@ -4218,6 +4245,7 @@ class ActiveSetResult extends DataClass implements Insertable<ActiveSetResult> {
|
||||
Value<String?> scoreLabelSnapshot = const Value.absent(),
|
||||
Value<String?> scoreUnitSnapshot = const Value.absent(),
|
||||
Value<String?> note = const Value.absent(),
|
||||
String? status,
|
||||
}) => ActiveSetResult(
|
||||
id: id ?? this.id,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
@ -4253,6 +4281,7 @@ class ActiveSetResult extends DataClass implements Insertable<ActiveSetResult> {
|
||||
? scoreUnitSnapshot.value
|
||||
: this.scoreUnitSnapshot,
|
||||
note: note.present ? note.value : this.note,
|
||||
status: status ?? this.status,
|
||||
);
|
||||
ActiveSetResult copyWithCompanion(ActiveSetResultsCompanion data) {
|
||||
return ActiveSetResult(
|
||||
@ -4315,6 +4344,7 @@ class ActiveSetResult extends DataClass implements Insertable<ActiveSetResult> {
|
||||
? data.scoreUnitSnapshot.value
|
||||
: this.scoreUnitSnapshot,
|
||||
note: data.note.present ? data.note.value : this.note,
|
||||
status: data.status.present ? data.status.value : this.status,
|
||||
);
|
||||
}
|
||||
|
||||
@ -4345,7 +4375,8 @@ class ActiveSetResult extends DataClass implements Insertable<ActiveSetResult> {
|
||||
..write('actualScore: $actualScore, ')
|
||||
..write('scoreLabelSnapshot: $scoreLabelSnapshot, ')
|
||||
..write('scoreUnitSnapshot: $scoreUnitSnapshot, ')
|
||||
..write('note: $note')
|
||||
..write('note: $note, ')
|
||||
..write('status: $status')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
@ -4377,6 +4408,7 @@ class ActiveSetResult extends DataClass implements Insertable<ActiveSetResult> {
|
||||
scoreLabelSnapshot,
|
||||
scoreUnitSnapshot,
|
||||
note,
|
||||
status,
|
||||
]);
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
@ -4406,7 +4438,8 @@ class ActiveSetResult extends DataClass implements Insertable<ActiveSetResult> {
|
||||
other.actualScore == this.actualScore &&
|
||||
other.scoreLabelSnapshot == this.scoreLabelSnapshot &&
|
||||
other.scoreUnitSnapshot == this.scoreUnitSnapshot &&
|
||||
other.note == this.note);
|
||||
other.note == this.note &&
|
||||
other.status == this.status);
|
||||
}
|
||||
|
||||
class ActiveSetResultsCompanion extends UpdateCompanion<ActiveSetResult> {
|
||||
@ -4435,6 +4468,7 @@ class ActiveSetResultsCompanion extends UpdateCompanion<ActiveSetResult> {
|
||||
final Value<String?> scoreLabelSnapshot;
|
||||
final Value<String?> scoreUnitSnapshot;
|
||||
final Value<String?> note;
|
||||
final Value<String> status;
|
||||
final Value<int> rowid;
|
||||
const ActiveSetResultsCompanion({
|
||||
this.id = const Value.absent(),
|
||||
@ -4462,6 +4496,7 @@ class ActiveSetResultsCompanion extends UpdateCompanion<ActiveSetResult> {
|
||||
this.scoreLabelSnapshot = const Value.absent(),
|
||||
this.scoreUnitSnapshot = const Value.absent(),
|
||||
this.note = const Value.absent(),
|
||||
this.status = const Value.absent(),
|
||||
this.rowid = const Value.absent(),
|
||||
});
|
||||
ActiveSetResultsCompanion.insert({
|
||||
@ -4490,6 +4525,7 @@ class ActiveSetResultsCompanion extends UpdateCompanion<ActiveSetResult> {
|
||||
this.scoreLabelSnapshot = const Value.absent(),
|
||||
this.scoreUnitSnapshot = const Value.absent(),
|
||||
this.note = const Value.absent(),
|
||||
this.status = const Value.absent(),
|
||||
this.rowid = const Value.absent(),
|
||||
}) : id = Value(id),
|
||||
createdAt = Value(createdAt),
|
||||
@ -4529,6 +4565,7 @@ class ActiveSetResultsCompanion extends UpdateCompanion<ActiveSetResult> {
|
||||
Expression<String>? scoreLabelSnapshot,
|
||||
Expression<String>? scoreUnitSnapshot,
|
||||
Expression<String>? note,
|
||||
Expression<String>? status,
|
||||
Expression<int>? rowid,
|
||||
}) {
|
||||
return RawValuesInsertable({
|
||||
@ -4561,6 +4598,7 @@ class ActiveSetResultsCompanion extends UpdateCompanion<ActiveSetResult> {
|
||||
'score_label_snapshot': scoreLabelSnapshot,
|
||||
if (scoreUnitSnapshot != null) 'score_unit_snapshot': scoreUnitSnapshot,
|
||||
if (note != null) 'note': note,
|
||||
if (status != null) 'status': status,
|
||||
if (rowid != null) 'rowid': rowid,
|
||||
});
|
||||
}
|
||||
@ -4591,6 +4629,7 @@ class ActiveSetResultsCompanion extends UpdateCompanion<ActiveSetResult> {
|
||||
Value<String?>? scoreLabelSnapshot,
|
||||
Value<String?>? scoreUnitSnapshot,
|
||||
Value<String?>? note,
|
||||
Value<String>? status,
|
||||
Value<int>? rowid,
|
||||
}) {
|
||||
return ActiveSetResultsCompanion(
|
||||
@ -4620,6 +4659,7 @@ class ActiveSetResultsCompanion extends UpdateCompanion<ActiveSetResult> {
|
||||
scoreLabelSnapshot: scoreLabelSnapshot ?? this.scoreLabelSnapshot,
|
||||
scoreUnitSnapshot: scoreUnitSnapshot ?? this.scoreUnitSnapshot,
|
||||
note: note ?? this.note,
|
||||
status: status ?? this.status,
|
||||
rowid: rowid ?? this.rowid,
|
||||
);
|
||||
}
|
||||
@ -4706,6 +4746,9 @@ class ActiveSetResultsCompanion extends UpdateCompanion<ActiveSetResult> {
|
||||
if (note.present) {
|
||||
map['note'] = Variable<String>(note.value);
|
||||
}
|
||||
if (status.present) {
|
||||
map['status'] = Variable<String>(status.value);
|
||||
}
|
||||
if (rowid.present) {
|
||||
map['rowid'] = Variable<int>(rowid.value);
|
||||
}
|
||||
@ -4740,6 +4783,7 @@ class ActiveSetResultsCompanion extends UpdateCompanion<ActiveSetResult> {
|
||||
..write('scoreLabelSnapshot: $scoreLabelSnapshot, ')
|
||||
..write('scoreUnitSnapshot: $scoreUnitSnapshot, ')
|
||||
..write('note: $note, ')
|
||||
..write('status: $status, ')
|
||||
..write('rowid: $rowid')
|
||||
..write(')'))
|
||||
.toString();
|
||||
@ -12033,6 +12077,16 @@ class $WorkoutHistorySetResultsTable extends WorkoutHistorySetResults
|
||||
type: DriftSqlType.dateTime,
|
||||
requiredDuringInsert: false,
|
||||
);
|
||||
static const VerificationMeta _statusMeta = const VerificationMeta('status');
|
||||
@override
|
||||
late final GeneratedColumn<String> status = GeneratedColumn<String>(
|
||||
'status',
|
||||
aliasedName,
|
||||
false,
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: false,
|
||||
defaultValue: const Constant('completed'),
|
||||
);
|
||||
@override
|
||||
List<GeneratedColumn> get $columns => [
|
||||
id,
|
||||
@ -12067,6 +12121,7 @@ class $WorkoutHistorySetResultsTable extends WorkoutHistorySetResults
|
||||
scoreUnitSnapshot,
|
||||
startedAt,
|
||||
completedAt,
|
||||
status,
|
||||
];
|
||||
@override
|
||||
String get aliasedName => _alias ?? actualTableName;
|
||||
@ -12375,6 +12430,12 @@ class $WorkoutHistorySetResultsTable extends WorkoutHistorySetResults
|
||||
),
|
||||
);
|
||||
}
|
||||
if (data.containsKey('status')) {
|
||||
context.handle(
|
||||
_statusMeta,
|
||||
status.isAcceptableOrUnknown(data['status']!, _statusMeta),
|
||||
);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
@ -12515,6 +12576,10 @@ class $WorkoutHistorySetResultsTable extends WorkoutHistorySetResults
|
||||
DriftSqlType.dateTime,
|
||||
data['${effectivePrefix}completed_at'],
|
||||
),
|
||||
status: attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.string,
|
||||
data['${effectivePrefix}status'],
|
||||
)!,
|
||||
);
|
||||
}
|
||||
|
||||
@ -12558,6 +12623,7 @@ class WorkoutHistorySetResult extends DataClass
|
||||
final String? scoreUnitSnapshot;
|
||||
final DateTime? startedAt;
|
||||
final DateTime? completedAt;
|
||||
final String status;
|
||||
const WorkoutHistorySetResult({
|
||||
required this.id,
|
||||
required this.createdAt,
|
||||
@ -12591,6 +12657,7 @@ class WorkoutHistorySetResult extends DataClass
|
||||
this.scoreUnitSnapshot,
|
||||
this.startedAt,
|
||||
this.completedAt,
|
||||
required this.status,
|
||||
});
|
||||
@override
|
||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
@ -12657,6 +12724,7 @@ class WorkoutHistorySetResult extends DataClass
|
||||
if (!nullToAbsent || completedAt != null) {
|
||||
map['completed_at'] = Variable<DateTime>(completedAt);
|
||||
}
|
||||
map['status'] = Variable<String>(status);
|
||||
return map;
|
||||
}
|
||||
|
||||
@ -12723,6 +12791,7 @@ class WorkoutHistorySetResult extends DataClass
|
||||
completedAt: completedAt == null && nullToAbsent
|
||||
? const Value.absent()
|
||||
: Value(completedAt),
|
||||
status: Value(status),
|
||||
);
|
||||
}
|
||||
|
||||
@ -12786,6 +12855,7 @@ class WorkoutHistorySetResult extends DataClass
|
||||
),
|
||||
startedAt: serializer.fromJson<DateTime?>(json['startedAt']),
|
||||
completedAt: serializer.fromJson<DateTime?>(json['completedAt']),
|
||||
status: serializer.fromJson<String>(json['status']),
|
||||
);
|
||||
}
|
||||
@override
|
||||
@ -12826,6 +12896,7 @@ class WorkoutHistorySetResult extends DataClass
|
||||
'scoreUnitSnapshot': serializer.toJson<String?>(scoreUnitSnapshot),
|
||||
'startedAt': serializer.toJson<DateTime?>(startedAt),
|
||||
'completedAt': serializer.toJson<DateTime?>(completedAt),
|
||||
'status': serializer.toJson<String>(status),
|
||||
};
|
||||
}
|
||||
|
||||
@ -12862,6 +12933,7 @@ class WorkoutHistorySetResult extends DataClass
|
||||
Value<String?> scoreUnitSnapshot = const Value.absent(),
|
||||
Value<DateTime?> startedAt = const Value.absent(),
|
||||
Value<DateTime?> completedAt = const Value.absent(),
|
||||
String? status,
|
||||
}) => WorkoutHistorySetResult(
|
||||
id: id ?? this.id,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
@ -12909,6 +12981,7 @@ class WorkoutHistorySetResult extends DataClass
|
||||
: this.scoreUnitSnapshot,
|
||||
startedAt: startedAt.present ? startedAt.value : this.startedAt,
|
||||
completedAt: completedAt.present ? completedAt.value : this.completedAt,
|
||||
status: status ?? this.status,
|
||||
);
|
||||
WorkoutHistorySetResult copyWithCompanion(
|
||||
WorkoutHistorySetResultsCompanion data,
|
||||
@ -12996,6 +13069,7 @@ class WorkoutHistorySetResult extends DataClass
|
||||
completedAt: data.completedAt.present
|
||||
? data.completedAt.value
|
||||
: this.completedAt,
|
||||
status: data.status.present ? data.status.value : this.status,
|
||||
);
|
||||
}
|
||||
|
||||
@ -13033,7 +13107,8 @@ class WorkoutHistorySetResult extends DataClass
|
||||
..write('scoreLabelSnapshot: $scoreLabelSnapshot, ')
|
||||
..write('scoreUnitSnapshot: $scoreUnitSnapshot, ')
|
||||
..write('startedAt: $startedAt, ')
|
||||
..write('completedAt: $completedAt')
|
||||
..write('completedAt: $completedAt, ')
|
||||
..write('status: $status')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
@ -13072,6 +13147,7 @@ class WorkoutHistorySetResult extends DataClass
|
||||
scoreUnitSnapshot,
|
||||
startedAt,
|
||||
completedAt,
|
||||
status,
|
||||
]);
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
@ -13108,7 +13184,8 @@ class WorkoutHistorySetResult extends DataClass
|
||||
other.scoreLabelSnapshot == this.scoreLabelSnapshot &&
|
||||
other.scoreUnitSnapshot == this.scoreUnitSnapshot &&
|
||||
other.startedAt == this.startedAt &&
|
||||
other.completedAt == this.completedAt);
|
||||
other.completedAt == this.completedAt &&
|
||||
other.status == this.status);
|
||||
}
|
||||
|
||||
class WorkoutHistorySetResultsCompanion
|
||||
@ -13145,6 +13222,7 @@ class WorkoutHistorySetResultsCompanion
|
||||
final Value<String?> scoreUnitSnapshot;
|
||||
final Value<DateTime?> startedAt;
|
||||
final Value<DateTime?> completedAt;
|
||||
final Value<String> status;
|
||||
final Value<int> rowid;
|
||||
const WorkoutHistorySetResultsCompanion({
|
||||
this.id = const Value.absent(),
|
||||
@ -13179,6 +13257,7 @@ class WorkoutHistorySetResultsCompanion
|
||||
this.scoreUnitSnapshot = const Value.absent(),
|
||||
this.startedAt = const Value.absent(),
|
||||
this.completedAt = const Value.absent(),
|
||||
this.status = const Value.absent(),
|
||||
this.rowid = const Value.absent(),
|
||||
});
|
||||
WorkoutHistorySetResultsCompanion.insert({
|
||||
@ -13214,6 +13293,7 @@ class WorkoutHistorySetResultsCompanion
|
||||
this.scoreUnitSnapshot = const Value.absent(),
|
||||
this.startedAt = const Value.absent(),
|
||||
this.completedAt = const Value.absent(),
|
||||
this.status = const Value.absent(),
|
||||
this.rowid = const Value.absent(),
|
||||
}) : id = Value(id),
|
||||
createdAt = Value(createdAt),
|
||||
@ -13265,6 +13345,7 @@ class WorkoutHistorySetResultsCompanion
|
||||
Expression<String>? scoreUnitSnapshot,
|
||||
Expression<DateTime>? startedAt,
|
||||
Expression<DateTime>? completedAt,
|
||||
Expression<String>? status,
|
||||
Expression<int>? rowid,
|
||||
}) {
|
||||
return RawValuesInsertable({
|
||||
@ -13311,6 +13392,7 @@ class WorkoutHistorySetResultsCompanion
|
||||
if (scoreUnitSnapshot != null) 'score_unit_snapshot': scoreUnitSnapshot,
|
||||
if (startedAt != null) 'started_at': startedAt,
|
||||
if (completedAt != null) 'completed_at': completedAt,
|
||||
if (status != null) 'status': status,
|
||||
if (rowid != null) 'rowid': rowid,
|
||||
});
|
||||
}
|
||||
@ -13348,6 +13430,7 @@ class WorkoutHistorySetResultsCompanion
|
||||
Value<String?>? scoreUnitSnapshot,
|
||||
Value<DateTime?>? startedAt,
|
||||
Value<DateTime?>? completedAt,
|
||||
Value<String>? status,
|
||||
Value<int>? rowid,
|
||||
}) {
|
||||
return WorkoutHistorySetResultsCompanion(
|
||||
@ -13384,6 +13467,7 @@ class WorkoutHistorySetResultsCompanion
|
||||
scoreUnitSnapshot: scoreUnitSnapshot ?? this.scoreUnitSnapshot,
|
||||
startedAt: startedAt ?? this.startedAt,
|
||||
completedAt: completedAt ?? this.completedAt,
|
||||
status: status ?? this.status,
|
||||
rowid: rowid ?? this.rowid,
|
||||
);
|
||||
}
|
||||
@ -13499,6 +13583,9 @@ class WorkoutHistorySetResultsCompanion
|
||||
if (completedAt.present) {
|
||||
map['completed_at'] = Variable<DateTime>(completedAt.value);
|
||||
}
|
||||
if (status.present) {
|
||||
map['status'] = Variable<String>(status.value);
|
||||
}
|
||||
if (rowid.present) {
|
||||
map['rowid'] = Variable<int>(rowid.value);
|
||||
}
|
||||
@ -13540,6 +13627,7 @@ class WorkoutHistorySetResultsCompanion
|
||||
..write('scoreUnitSnapshot: $scoreUnitSnapshot, ')
|
||||
..write('startedAt: $startedAt, ')
|
||||
..write('completedAt: $completedAt, ')
|
||||
..write('status: $status, ')
|
||||
..write('rowid: $rowid')
|
||||
..write(')'))
|
||||
.toString();
|
||||
@ -18114,6 +18202,7 @@ typedef $$ActiveSetResultsTableCreateCompanionBuilder =
|
||||
Value<String?> scoreLabelSnapshot,
|
||||
Value<String?> scoreUnitSnapshot,
|
||||
Value<String?> note,
|
||||
Value<String> status,
|
||||
Value<int> rowid,
|
||||
});
|
||||
typedef $$ActiveSetResultsTableUpdateCompanionBuilder =
|
||||
@ -18143,6 +18232,7 @@ typedef $$ActiveSetResultsTableUpdateCompanionBuilder =
|
||||
Value<String?> scoreLabelSnapshot,
|
||||
Value<String?> scoreUnitSnapshot,
|
||||
Value<String?> note,
|
||||
Value<String> status,
|
||||
Value<int> rowid,
|
||||
});
|
||||
|
||||
@ -18307,6 +18397,11 @@ class $$ActiveSetResultsTableFilterComposer
|
||||
builder: (column) => ColumnFilters(column),
|
||||
);
|
||||
|
||||
ColumnFilters<String> get status => $composableBuilder(
|
||||
column: $table.status,
|
||||
builder: (column) => ColumnFilters(column),
|
||||
);
|
||||
|
||||
$$ActiveWorkoutSessionsTableFilterComposer get activeWorkoutSessionId {
|
||||
final $$ActiveWorkoutSessionsTableFilterComposer composer =
|
||||
$composerBuilder(
|
||||
@ -18461,6 +18556,11 @@ class $$ActiveSetResultsTableOrderingComposer
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
);
|
||||
|
||||
ColumnOrderings<String> get status => $composableBuilder(
|
||||
column: $table.status,
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
);
|
||||
|
||||
$$ActiveWorkoutSessionsTableOrderingComposer get activeWorkoutSessionId {
|
||||
final $$ActiveWorkoutSessionsTableOrderingComposer composer =
|
||||
$composerBuilder(
|
||||
@ -18599,6 +18699,9 @@ class $$ActiveSetResultsTableAnnotationComposer
|
||||
GeneratedColumn<String> get note =>
|
||||
$composableBuilder(column: $table.note, builder: (column) => column);
|
||||
|
||||
GeneratedColumn<String> get status =>
|
||||
$composableBuilder(column: $table.status, builder: (column) => column);
|
||||
|
||||
$$ActiveWorkoutSessionsTableAnnotationComposer get activeWorkoutSessionId {
|
||||
final $$ActiveWorkoutSessionsTableAnnotationComposer composer =
|
||||
$composerBuilder(
|
||||
@ -18679,6 +18782,7 @@ class $$ActiveSetResultsTableTableManager
|
||||
Value<String?> scoreLabelSnapshot = const Value.absent(),
|
||||
Value<String?> scoreUnitSnapshot = const Value.absent(),
|
||||
Value<String?> note = const Value.absent(),
|
||||
Value<String> status = const Value.absent(),
|
||||
Value<int> rowid = const Value.absent(),
|
||||
}) => ActiveSetResultsCompanion(
|
||||
id: id,
|
||||
@ -18706,6 +18810,7 @@ class $$ActiveSetResultsTableTableManager
|
||||
scoreLabelSnapshot: scoreLabelSnapshot,
|
||||
scoreUnitSnapshot: scoreUnitSnapshot,
|
||||
note: note,
|
||||
status: status,
|
||||
rowid: rowid,
|
||||
),
|
||||
createCompanionCallback:
|
||||
@ -18735,6 +18840,7 @@ class $$ActiveSetResultsTableTableManager
|
||||
Value<String?> scoreLabelSnapshot = const Value.absent(),
|
||||
Value<String?> scoreUnitSnapshot = const Value.absent(),
|
||||
Value<String?> note = const Value.absent(),
|
||||
Value<String> status = const Value.absent(),
|
||||
Value<int> rowid = const Value.absent(),
|
||||
}) => ActiveSetResultsCompanion.insert(
|
||||
id: id,
|
||||
@ -18762,6 +18868,7 @@ class $$ActiveSetResultsTableTableManager
|
||||
scoreLabelSnapshot: scoreLabelSnapshot,
|
||||
scoreUnitSnapshot: scoreUnitSnapshot,
|
||||
note: note,
|
||||
status: status,
|
||||
rowid: rowid,
|
||||
),
|
||||
withReferenceMapper: (p0) => p0
|
||||
@ -23550,6 +23657,7 @@ typedef $$WorkoutHistorySetResultsTableCreateCompanionBuilder =
|
||||
Value<String?> scoreUnitSnapshot,
|
||||
Value<DateTime?> startedAt,
|
||||
Value<DateTime?> completedAt,
|
||||
Value<String> status,
|
||||
Value<int> rowid,
|
||||
});
|
||||
typedef $$WorkoutHistorySetResultsTableUpdateCompanionBuilder =
|
||||
@ -23586,6 +23694,7 @@ typedef $$WorkoutHistorySetResultsTableUpdateCompanionBuilder =
|
||||
Value<String?> scoreUnitSnapshot,
|
||||
Value<DateTime?> startedAt,
|
||||
Value<DateTime?> completedAt,
|
||||
Value<String> status,
|
||||
Value<int> rowid,
|
||||
});
|
||||
|
||||
@ -23786,6 +23895,11 @@ class $$WorkoutHistorySetResultsTableFilterComposer
|
||||
builder: (column) => ColumnFilters(column),
|
||||
);
|
||||
|
||||
ColumnFilters<String> get status => $composableBuilder(
|
||||
column: $table.status,
|
||||
builder: (column) => ColumnFilters(column),
|
||||
);
|
||||
|
||||
$$WorkoutHistoriesTableFilterComposer get workoutHistoryId {
|
||||
final $$WorkoutHistoriesTableFilterComposer composer = $composerBuilder(
|
||||
composer: this,
|
||||
@ -23974,6 +24088,11 @@ class $$WorkoutHistorySetResultsTableOrderingComposer
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
);
|
||||
|
||||
ColumnOrderings<String> get status => $composableBuilder(
|
||||
column: $table.status,
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
);
|
||||
|
||||
$$WorkoutHistoriesTableOrderingComposer get workoutHistoryId {
|
||||
final $$WorkoutHistoriesTableOrderingComposer composer = $composerBuilder(
|
||||
composer: this,
|
||||
@ -24148,6 +24267,9 @@ class $$WorkoutHistorySetResultsTableAnnotationComposer
|
||||
builder: (column) => column,
|
||||
);
|
||||
|
||||
GeneratedColumn<String> get status =>
|
||||
$composableBuilder(column: $table.status, builder: (column) => column);
|
||||
|
||||
$$WorkoutHistoriesTableAnnotationComposer get workoutHistoryId {
|
||||
final $$WorkoutHistoriesTableAnnotationComposer composer = $composerBuilder(
|
||||
composer: this,
|
||||
@ -24243,6 +24365,7 @@ class $$WorkoutHistorySetResultsTableTableManager
|
||||
Value<String?> scoreUnitSnapshot = const Value.absent(),
|
||||
Value<DateTime?> startedAt = const Value.absent(),
|
||||
Value<DateTime?> completedAt = const Value.absent(),
|
||||
Value<String> status = const Value.absent(),
|
||||
Value<int> rowid = const Value.absent(),
|
||||
}) => WorkoutHistorySetResultsCompanion(
|
||||
id: id,
|
||||
@ -24277,6 +24400,7 @@ class $$WorkoutHistorySetResultsTableTableManager
|
||||
scoreUnitSnapshot: scoreUnitSnapshot,
|
||||
startedAt: startedAt,
|
||||
completedAt: completedAt,
|
||||
status: status,
|
||||
rowid: rowid,
|
||||
),
|
||||
createCompanionCallback:
|
||||
@ -24313,6 +24437,7 @@ class $$WorkoutHistorySetResultsTableTableManager
|
||||
Value<String?> scoreUnitSnapshot = const Value.absent(),
|
||||
Value<DateTime?> startedAt = const Value.absent(),
|
||||
Value<DateTime?> completedAt = const Value.absent(),
|
||||
Value<String> status = const Value.absent(),
|
||||
Value<int> rowid = const Value.absent(),
|
||||
}) => WorkoutHistorySetResultsCompanion.insert(
|
||||
id: id,
|
||||
@ -24347,6 +24472,7 @@ class $$WorkoutHistorySetResultsTableTableManager
|
||||
scoreUnitSnapshot: scoreUnitSnapshot,
|
||||
startedAt: startedAt,
|
||||
completedAt: completedAt,
|
||||
status: status,
|
||||
rowid: rowid,
|
||||
),
|
||||
withReferenceMapper: (p0) => p0
|
||||
|
||||
@ -929,6 +929,7 @@ db.ActiveSetResultsCompanion _activeSetResultCompanion(
|
||||
scoreLabelSnapshot: Value(result.scoreLabelSnapshot),
|
||||
scoreUnitSnapshot: Value(result.scoreUnitSnapshot),
|
||||
note: Value(result.note),
|
||||
status: Value(_setResultStatusToDb(result.status)),
|
||||
);
|
||||
}
|
||||
|
||||
@ -949,6 +950,7 @@ domain.ActiveSetResult _activeSetResultFromRow(db.ActiveSetResult row) {
|
||||
scoreLabelSnapshot: row.scoreLabelSnapshot,
|
||||
scoreUnitSnapshot: row.scoreUnitSnapshot,
|
||||
note: row.note,
|
||||
status: _setResultStatusFromDb(row.status),
|
||||
);
|
||||
}
|
||||
|
||||
@ -1059,6 +1061,7 @@ db.WorkoutHistorySetResultsCompanion _workoutHistorySetResultCompanion(
|
||||
scoreUnitSnapshot: Value(result.scoreUnitSnapshot),
|
||||
startedAt: Value(result.startedAt),
|
||||
completedAt: Value(result.completedAt),
|
||||
status: Value(_setResultStatusToDb(result.status)),
|
||||
);
|
||||
}
|
||||
|
||||
@ -1106,6 +1109,7 @@ domain.WorkoutHistorySetResult _workoutHistorySetResultFromRow(
|
||||
scoreUnitSnapshot: row.scoreUnitSnapshot,
|
||||
startedAt: row.startedAt,
|
||||
completedAt: row.completedAt,
|
||||
status: _setResultStatusFromDb(row.status),
|
||||
);
|
||||
}
|
||||
|
||||
@ -1143,6 +1147,17 @@ domain.MediaKind _mediaKindFromDb(String value) => switch (value) {
|
||||
_ => throw domain.DomainException('Unknown media kind: $value'),
|
||||
};
|
||||
|
||||
String _setResultStatusToDb(domain.SetResultStatus status) => switch (status) {
|
||||
domain.SetResultStatus.completed => 'completed',
|
||||
domain.SetResultStatus.skipped => 'skipped',
|
||||
};
|
||||
|
||||
domain.SetResultStatus _setResultStatusFromDb(String value) => switch (value) {
|
||||
'completed' => domain.SetResultStatus.completed,
|
||||
'skipped' => domain.SetResultStatus.skipped,
|
||||
_ => throw domain.DomainException('Unknown set result status: $value'),
|
||||
};
|
||||
|
||||
domain.ActiveWorkoutStatus _activeStatusFromDb(String value) => switch (value) {
|
||||
'running' => domain.ActiveWorkoutStatus.running,
|
||||
'paused' => domain.ActiveWorkoutStatus.paused,
|
||||
|
||||
@ -244,6 +244,7 @@ class ActiveSetResults extends SyncableTable {
|
||||
TextColumn get scoreLabelSnapshot => text().nullable()();
|
||||
TextColumn get scoreUnitSnapshot => text().nullable()();
|
||||
TextColumn get note => text().nullable()();
|
||||
TextColumn get status => text().withDefault(const Constant('completed'))();
|
||||
|
||||
@override
|
||||
List<String> get customConstraints => [
|
||||
@ -255,6 +256,9 @@ class ActiveSetResults extends SyncableTable {
|
||||
'CHECK (actual_time_ms IS NULL OR actual_time_ms >= 0)',
|
||||
'CHECK (actual_reps IS NULL OR actual_reps >= 0)',
|
||||
'CHECK (actual_score IS NULL OR actual_score >= 0)',
|
||||
"CHECK (status IN ('completed', 'skipped'))",
|
||||
'CHECK (status != \'skipped\' OR (actual_time_ms IS NULL '
|
||||
'AND actual_reps IS NULL AND actual_score IS NULL))',
|
||||
'CHECK (actual_score IS NULL OR (score_label_snapshot IS NOT NULL '
|
||||
'AND length(trim(score_label_snapshot)) > 0 '
|
||||
'AND score_unit_snapshot IS NOT NULL '
|
||||
@ -332,6 +336,7 @@ class WorkoutHistorySetResults extends SyncableTable {
|
||||
TextColumn get scoreUnitSnapshot => text().nullable()();
|
||||
DateTimeColumn get startedAt => dateTime().nullable()();
|
||||
DateTimeColumn get completedAt => dateTime().nullable()();
|
||||
TextColumn get status => text().withDefault(const Constant('completed'))();
|
||||
|
||||
@override
|
||||
List<String> get customConstraints => [
|
||||
@ -349,6 +354,9 @@ class WorkoutHistorySetResults extends SyncableTable {
|
||||
'CHECK (actual_time_ms IS NULL OR actual_time_ms >= 0)',
|
||||
'CHECK (actual_reps IS NULL OR actual_reps >= 0)',
|
||||
'CHECK (actual_score IS NULL OR actual_score >= 0)',
|
||||
"CHECK (status IN ('completed', 'skipped'))",
|
||||
'CHECK (status != \'skipped\' OR (actual_time_ms IS NULL '
|
||||
'AND actual_reps IS NULL AND actual_score IS NULL))',
|
||||
'CHECK (actual_score IS NULL OR (score_label_snapshot IS NOT NULL '
|
||||
'AND length(trim(score_label_snapshot)) > 0 '
|
||||
'AND score_unit_snapshot IS NOT NULL '
|
||||
|
||||
@ -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,
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user