merge(main): plan de séance et historique avec résultats d'étapes (ticket #61)
Fusionne feature/exercise-steps-61-plan-history — pub get OK, dart format appliqué, analyze propre (mêmes infos préexistantes), 102/102 tests verts, build APK debug validé. Dernier ticket UI du chantier "Exercice à plusieurs étapes" (#54) — il ne reste que la QA finale (#62). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
issueRef: "#60"
|
issueRef: "#60"
|
||||||
version: 1
|
version: 2
|
||||||
updatedBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"}
|
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
updatedAt: 1784481536357
|
updatedAt: 1784484539686
|
||||||
---
|
---
|
||||||
|
|||||||
@ -2,15 +2,15 @@
|
|||||||
id: "4807a946-834e-44a4-8a39-25ad951e6668"
|
id: "4807a946-834e-44a4-8a39-25ad951e6668"
|
||||||
number: 60
|
number: 60
|
||||||
title: "[DevFrontend] Exécution de séance avec module séquence et bips"
|
title: "[DevFrontend] Exécution de séance avec module séquence et bips"
|
||||||
status: "open"
|
status: "qa"
|
||||||
priority: "medium"
|
priority: "medium"
|
||||||
sprint: null
|
sprint: null
|
||||||
links: [{"target":"#54","kind":"relatesTo"},{"target":"#58","kind":"dependsOn"},{"target":"#59","kind":"dependsOn"}]
|
links: [{"target":"#54","kind":"relatesTo"},{"target":"#58","kind":"dependsOn"},{"target":"#59","kind":"dependsOn"}]
|
||||||
agentRefs: [{"agentId":"9933c93a-b8a1-4164-a3bb-7063fdad747d","role":"assigned"}]
|
agentRefs: [{"agentId":"9933c93a-b8a1-4164-a3bb-7063fdad747d","role":"assigned"}]
|
||||||
createdBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"}
|
createdBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"}
|
||||||
updatedBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"}
|
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
createdAt: 1784481536357
|
createdAt: 1784481536357
|
||||||
updatedAt: 1784481536357
|
updatedAt: 1784484539686
|
||||||
version: 1
|
version: 2
|
||||||
---
|
---
|
||||||
Implémenter le module `Séquence` sur l'écran d'exécution : affichage passage/étape, étapes temps avec compte à rebours, étapes répétitions avec validation manuelle, boucle multi-passages si reps de série actives, skips distincts étape/passage/série, persistance après pause/kill via use cases backend. Ajouter un port audio testable et un adapter Flutter utilisant des bips courts/longs, sans dépendance des tests widget à un lecteur audio réel.
|
Implémenter le module `Séquence` sur l'écran d'exécution : affichage passage/étape, étapes temps avec compte à rebours, étapes répétitions avec validation manuelle, boucle multi-passages si reps de série actives, skips distincts étape/passage/série, persistance après pause/kill via use cases backend. Ajouter un port audio testable et un adapter Flutter utilisant des bips courts/longs, sans dépendance des tests widget à un lecteur audio réel.
|
||||||
@ -667,13 +667,13 @@
|
|||||||
"issueRef": "#60",
|
"issueRef": "#60",
|
||||||
"path": "60",
|
"path": "60",
|
||||||
"title": "[DevFrontend] Exécution de séance avec module séquence et bips",
|
"title": "[DevFrontend] Exécution de séance avec module séquence et bips",
|
||||||
"status": "open",
|
"status": "qa",
|
||||||
"priority": "medium",
|
"priority": "medium",
|
||||||
"sprint": null,
|
"sprint": null,
|
||||||
"assignedAgentIds": [
|
"assignedAgentIds": [
|
||||||
"9933c93a-b8a1-4164-a3bb-7063fdad747d"
|
"9933c93a-b8a1-4164-a3bb-7063fdad747d"
|
||||||
],
|
],
|
||||||
"updatedAt": 1784481536357
|
"updatedAt": 1784484539686
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"issueRef": "#61",
|
"issueRef": "#61",
|
||||||
|
|||||||
@ -151,10 +151,21 @@ final class HistoryDetailScreen extends StatelessWidget {
|
|||||||
style: Theme.of(context).textTheme.titleSmall,
|
style: Theme.of(context).textTheme.titleSmall,
|
||||||
),
|
),
|
||||||
for (final set in exercise.sets)
|
for (final set in exercise.sets)
|
||||||
ListTile(
|
Padding(
|
||||||
dense: true,
|
padding: const EdgeInsets.only(bottom: 8),
|
||||||
title: Text('Série ${set.setIndex + 1}'),
|
child: Column(
|
||||||
subtitle: Text(set.summary),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
dense: true,
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
title: Text('Série ${set.setIndex + 1}'),
|
||||||
|
subtitle: Text(set.summary),
|
||||||
|
),
|
||||||
|
if (set.stepPassages.isNotEmpty)
|
||||||
|
_HistoryStepResultsSection(passages: set.stepPassages),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
@ -246,6 +257,10 @@ final class HistoryDetailData {
|
|||||||
? history.results.map(_resultFromDomain).toList()
|
? history.results.map(_resultFromDomain).toList()
|
||||||
: (snapshot['results'] as List<dynamic>? ?? const [])
|
: (snapshot['results'] as List<dynamic>? ?? const [])
|
||||||
.cast<Map<String, dynamic>>();
|
.cast<Map<String, dynamic>>();
|
||||||
|
final rawStepResults = history.stepResults.isNotEmpty
|
||||||
|
? history.stepResults.map(_stepResultFromDomain).toList()
|
||||||
|
: (snapshot['stepResults'] as List<dynamic>? ?? const [])
|
||||||
|
.cast<Map<String, dynamic>>();
|
||||||
final programs = <HistoryProgramDetail>[];
|
final programs = <HistoryProgramDetail>[];
|
||||||
final rawPrograms = resolved['programs'] as List<dynamic>? ?? const [];
|
final rawPrograms = resolved['programs'] as List<dynamic>? ?? const [];
|
||||||
for (
|
for (
|
||||||
@ -270,12 +285,25 @@ final class HistoryDetailData {
|
|||||||
return result['programIndex'] == programIndex &&
|
return result['programIndex'] == programIndex &&
|
||||||
result['exerciseIndex'] == exerciseIndex;
|
result['exerciseIndex'] == exerciseIndex;
|
||||||
}).toList();
|
}).toList();
|
||||||
|
final exerciseStepResults = rawStepResults.where((result) {
|
||||||
|
return result['programIndex'] == programIndex &&
|
||||||
|
result['exerciseIndex'] == exerciseIndex;
|
||||||
|
}).toList();
|
||||||
exercises.add(
|
exercises.add(
|
||||||
HistoryExerciseDetail(
|
HistoryExerciseDetail(
|
||||||
name: rawExercise['exerciseNameSnapshot'] as String,
|
name: rawExercise['exerciseNameSnapshot'] as String,
|
||||||
sets: [
|
sets: [
|
||||||
for (final result in exerciseResults)
|
for (final result in exerciseResults)
|
||||||
HistorySetDetail.fromSnapshot(rawExercise, result),
|
HistorySetDetail.fromSnapshot(
|
||||||
|
rawExercise,
|
||||||
|
result,
|
||||||
|
exerciseStepResults
|
||||||
|
.where(
|
||||||
|
(stepResult) =>
|
||||||
|
stepResult['setIndex'] == result['setIndex'],
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -308,11 +336,16 @@ final class HistoryExerciseDetail {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final class HistorySetDetail {
|
final class HistorySetDetail {
|
||||||
const HistorySetDetail({required this.setIndex, required this.summary});
|
const HistorySetDetail({
|
||||||
|
required this.setIndex,
|
||||||
|
required this.summary,
|
||||||
|
required this.stepPassages,
|
||||||
|
});
|
||||||
|
|
||||||
factory HistorySetDetail.fromSnapshot(
|
factory HistorySetDetail.fromSnapshot(
|
||||||
Map<String, dynamic> exercise,
|
Map<String, dynamic> exercise,
|
||||||
Map<String, dynamic> result,
|
Map<String, dynamic> result,
|
||||||
|
List<Map<String, dynamic>> stepResults,
|
||||||
) {
|
) {
|
||||||
final parts = <String>[];
|
final parts = <String>[];
|
||||||
if (exercise['timeEnabled'] == true && result['actualTimeMs'] != null) {
|
if (exercise['timeEnabled'] == true && result['actualTimeMs'] != null) {
|
||||||
@ -345,11 +378,101 @@ final class HistorySetDetail {
|
|||||||
return HistorySetDetail(
|
return HistorySetDetail(
|
||||||
setIndex: result['setIndex'] as int,
|
setIndex: result['setIndex'] as int,
|
||||||
summary: parts.isEmpty ? 'Aucune mesure saisie' : parts.join(' · '),
|
summary: parts.isEmpty ? 'Aucune mesure saisie' : parts.join(' · '),
|
||||||
|
stepPassages: _stepPassagesFromResults(stepResults),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final int setIndex;
|
final int setIndex;
|
||||||
final String summary;
|
final String summary;
|
||||||
|
final List<HistoryStepPassageDetail> stepPassages;
|
||||||
|
}
|
||||||
|
|
||||||
|
final class HistoryStepPassageDetail {
|
||||||
|
const HistoryStepPassageDetail({required this.index, required this.steps});
|
||||||
|
|
||||||
|
final int index;
|
||||||
|
final List<HistoryStepResultDetail> steps;
|
||||||
|
}
|
||||||
|
|
||||||
|
final class HistoryStepResultDetail {
|
||||||
|
const HistoryStepResultDetail({
|
||||||
|
required this.index,
|
||||||
|
required this.name,
|
||||||
|
required this.status,
|
||||||
|
required this.valueLabel,
|
||||||
|
this.scoreLabel,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory HistoryStepResultDetail.fromResult(Map<String, dynamic> result) {
|
||||||
|
final status = _setResultStatusFromSnapshot(result['status']);
|
||||||
|
return HistoryStepResultDetail(
|
||||||
|
index: result['stepIndex'] as int,
|
||||||
|
name: result['stepNameSnapshot'] as String,
|
||||||
|
status: status,
|
||||||
|
valueLabel: status == SetResultStatus.skipped
|
||||||
|
? null
|
||||||
|
: _historyStepValueLabel(result),
|
||||||
|
scoreLabel: status == SetResultStatus.skipped
|
||||||
|
? null
|
||||||
|
: _historyStepScoreLabel(result),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final int index;
|
||||||
|
final String name;
|
||||||
|
final SetResultStatus status;
|
||||||
|
final String? valueLabel;
|
||||||
|
final String? scoreLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
final class _HistoryStepResultsSection extends StatelessWidget {
|
||||||
|
const _HistoryStepResultsSection({required this.passages});
|
||||||
|
|
||||||
|
final List<HistoryStepPassageDetail> passages;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final tokens = courtBlazerTokensOf(context);
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 16, right: 16, bottom: 8),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Étapes réalisées',
|
||||||
|
style: Theme.of(context).textTheme.labelLarge,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
for (final passage in passages) ...[
|
||||||
|
Text(
|
||||||
|
'Passage ${passage.index + 1}',
|
||||||
|
style: Theme.of(context).textTheme.labelMedium,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
for (final step in passage.steps)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 4),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(child: Text(_historyStepLine(step))),
|
||||||
|
if (step.status == SetResultStatus.skipped) ...[
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Chip(
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
label: const Text('Passée'),
|
||||||
|
side: BorderSide(color: tokens.border),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, List<WorkoutHistory>> groupHistoriesByPeriod(
|
Map<String, List<WorkoutHistory>> groupHistoriesByPeriod(
|
||||||
@ -395,6 +518,99 @@ Map<String, dynamic> _resultFromDomain(WorkoutHistorySetResult result) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> _stepResultFromDomain(WorkoutHistoryStepResult result) {
|
||||||
|
return {
|
||||||
|
'programIndex': result.programIndex,
|
||||||
|
'exerciseIndex': result.exerciseIndex,
|
||||||
|
'setIndex': result.setIndex,
|
||||||
|
'passageIndex': result.passageIndex,
|
||||||
|
'stepIndex': result.stepIndex,
|
||||||
|
'stepNameSnapshot': result.stepNameSnapshot,
|
||||||
|
'stepTypeSnapshot': result.stepTypeSnapshot.name,
|
||||||
|
'targetValueSnapshot': result.targetValueSnapshot,
|
||||||
|
'hasScoreSnapshot': result.hasScoreSnapshot,
|
||||||
|
'scoreInputModeSnapshot': result.scoreInputModeSnapshot?.name,
|
||||||
|
'scoreLabelSnapshot': result.scoreLabelSnapshot,
|
||||||
|
'scoreUnitSnapshot': result.scoreUnitSnapshot,
|
||||||
|
'actualTimeMs': result.actualTimeMs,
|
||||||
|
'actualReps': result.actualReps,
|
||||||
|
'actualScore': result.actualScore,
|
||||||
|
'actualScoreTimeMs': result.actualScoreTimeMs,
|
||||||
|
'status': result.status.name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
List<HistoryStepPassageDetail> _stepPassagesFromResults(
|
||||||
|
List<Map<String, dynamic>> results,
|
||||||
|
) {
|
||||||
|
if (results.isEmpty) return const [];
|
||||||
|
final byPassage = <int, List<Map<String, dynamic>>>{};
|
||||||
|
for (final result in results) {
|
||||||
|
final passageIndex = result['passageIndex'] as int;
|
||||||
|
byPassage.putIfAbsent(passageIndex, () => []).add(result);
|
||||||
|
}
|
||||||
|
final passages = byPassage.entries.toList()
|
||||||
|
..sort((left, right) => left.key.compareTo(right.key));
|
||||||
|
return passages
|
||||||
|
.map((entry) {
|
||||||
|
final steps = entry.value
|
||||||
|
..sort(
|
||||||
|
(left, right) =>
|
||||||
|
(left['stepIndex'] as int).compareTo(right['stepIndex'] as int),
|
||||||
|
);
|
||||||
|
return HistoryStepPassageDetail(
|
||||||
|
index: entry.key,
|
||||||
|
steps: steps.map(HistoryStepResultDetail.fromResult).toList(),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.toList(growable: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _historyStepLine(HistoryStepResultDetail step) {
|
||||||
|
final parts = <String>['Étape ${step.index + 1}', step.name];
|
||||||
|
if (step.valueLabel != null) {
|
||||||
|
parts.add(step.valueLabel!);
|
||||||
|
}
|
||||||
|
if (step.scoreLabel != null) {
|
||||||
|
parts.add(step.scoreLabel!);
|
||||||
|
}
|
||||||
|
return parts.join(' · ');
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _historyStepValueLabel(Map<String, dynamic> result) {
|
||||||
|
final type = result['stepTypeSnapshot'];
|
||||||
|
if (type == ExerciseStepType.time.name && result['actualTimeMs'] != null) {
|
||||||
|
return 'Temps ${_formatDurationMs(result['actualTimeMs'] as int)}';
|
||||||
|
}
|
||||||
|
if (type == ExerciseStepType.reps.name && result['actualReps'] != null) {
|
||||||
|
return 'Répétitions ${result['actualReps']}';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _historyStepScoreLabel(Map<String, dynamic> result) {
|
||||||
|
if (result['hasScoreSnapshot'] != true) return null;
|
||||||
|
final mode = _scoreInputModeFromSnapshot(result['scoreInputModeSnapshot']);
|
||||||
|
if (mode == ScoreInputMode.stopwatch && result['actualScoreTimeMs'] != null) {
|
||||||
|
return 'Score chrono : ${_formatScoreStopwatch(Duration(milliseconds: result['actualScoreTimeMs'] as int))}';
|
||||||
|
}
|
||||||
|
if (mode == ScoreInputMode.manual && result['actualScore'] != null) {
|
||||||
|
final scoreLabel = result['scoreLabelSnapshot'] as String? ?? 'Score';
|
||||||
|
final score = _formatScoreValue(result['actualScore'] as num);
|
||||||
|
final unit = result['scoreUnitSnapshot'] as String?;
|
||||||
|
final suffix = unit == null || unit.trim().isEmpty ? '' : ' $unit';
|
||||||
|
return '$scoreLabel : $score$suffix';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
SetResultStatus _setResultStatusFromSnapshot(Object? value) {
|
||||||
|
return switch (value) {
|
||||||
|
'skipped' => SetResultStatus.skipped,
|
||||||
|
_ => SetResultStatus.completed,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
String _historySummary(WorkoutHistory history) {
|
String _historySummary(WorkoutHistory history) {
|
||||||
final snapshot =
|
final snapshot =
|
||||||
jsonDecode(history.historySnapshotJson) as Map<String, dynamic>;
|
jsonDecode(history.historySnapshotJson) as Map<String, dynamic>;
|
||||||
|
|||||||
@ -13,7 +13,8 @@ import 'theme.dart';
|
|||||||
|
|
||||||
enum WorkoutExecutionMode { active, rest, paused, finished }
|
enum WorkoutExecutionMode { active, rest, paused, finished }
|
||||||
|
|
||||||
typedef VideoMediaBuilder = Widget Function(BuildContext context, MediaAsset asset);
|
typedef VideoMediaBuilder =
|
||||||
|
Widget Function(BuildContext context, MediaAsset asset);
|
||||||
|
|
||||||
final class WorkoutExecutionScreen extends StatefulWidget {
|
final class WorkoutExecutionScreen extends StatefulWidget {
|
||||||
const WorkoutExecutionScreen({
|
const WorkoutExecutionScreen({
|
||||||
@ -244,7 +245,7 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
|||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
minimumSize: const Size.fromHeight(52),
|
minimumSize: const Size.fromHeight(52),
|
||||||
),
|
),
|
||||||
child: const Text('Passer'),
|
child: Text(_hasActiveStepSequence ? 'Passer la série' : 'Passer'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@ -632,7 +633,9 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
|||||||
} on Exception catch (error) {
|
} on Exception catch (error) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text('Impossible de rafraîchir la séquence : $error')),
|
SnackBar(
|
||||||
|
content: Text('Impossible de rafraîchir la séquence : $error'),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -642,7 +645,8 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
|||||||
final previousKey = previous == null
|
final previousKey = previous == null
|
||||||
? null
|
? null
|
||||||
: '${previous.state.currentPassageIndex}:${previous.state.currentStepIndex}';
|
: '${previous.state.currentPassageIndex}:${previous.state.currentStepIndex}';
|
||||||
final nextKey = '${view.state.currentPassageIndex}:${view.state.currentStepIndex}';
|
final nextKey =
|
||||||
|
'${view.state.currentPassageIndex}:${view.state.currentStepIndex}';
|
||||||
if (previousKey != null && previousKey != nextKey) {
|
if (previousKey != null && previousKey != nextKey) {
|
||||||
_resetStepScoreInput();
|
_resetStepScoreInput();
|
||||||
}
|
}
|
||||||
@ -776,7 +780,8 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
|||||||
Future<void> _skipCurrentStep() async {
|
Future<void> _skipCurrentStep() async {
|
||||||
final confirmed = await _confirmStepSkip(
|
final confirmed = await _confirmStepSkip(
|
||||||
'Passer cette étape ?',
|
'Passer cette étape ?',
|
||||||
_stepProgress?.state.status == ActiveExerciseStepProgressStatus.runningTimer
|
_stepProgress?.state.status ==
|
||||||
|
ActiveExerciseStepProgressStatus.runningTimer
|
||||||
? 'Le chrono de cette étape sera arrêté.'
|
? 'Le chrono de cette étape sera arrêté.'
|
||||||
: 'Cette étape sera marquée comme passée.',
|
: 'Cette étape sera marquée comme passée.',
|
||||||
'Passer l’étape',
|
'Passer l’étape',
|
||||||
@ -1017,6 +1022,7 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
|||||||
setStates: setStates,
|
setStates: setStates,
|
||||||
activeRest: activeRest,
|
activeRest: activeRest,
|
||||||
activeUseCases: widget.activeUseCases,
|
activeUseCases: widget.activeUseCases,
|
||||||
|
stepUseCases: widget.stepUseCases,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -1602,9 +1608,9 @@ final class _TimedStepBody extends StatelessWidget {
|
|||||||
Center(
|
Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
remainingLabel,
|
remainingLabel,
|
||||||
style: AppTextStyles.timer(context).copyWith(
|
style: AppTextStyles.timer(
|
||||||
color: Theme.of(context).colorScheme.primary,
|
context,
|
||||||
),
|
).copyWith(color: Theme.of(context).colorScheme.primary),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (!running) ...[
|
if (!running) ...[
|
||||||
@ -1632,9 +1638,9 @@ final class _RepsStepBody extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'${step.defaultTargetValue}',
|
'${step.defaultTargetValue}',
|
||||||
style: AppTextStyles.timer(context).copyWith(
|
style: AppTextStyles.timer(
|
||||||
color: Theme.of(context).colorScheme.primary,
|
context,
|
||||||
),
|
).copyWith(color: Theme.of(context).colorScheme.primary),
|
||||||
),
|
),
|
||||||
Text('RÉPÉTITIONS', style: Theme.of(context).textTheme.labelLarge),
|
Text('RÉPÉTITIONS', style: Theme.of(context).textTheme.labelLarge),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
@ -1679,7 +1685,10 @@ final class _StepScoreInput extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Center(
|
Center(
|
||||||
child: Text(elapsedLabel, style: AppTextStyles.scoreNumber(context)),
|
child: Text(
|
||||||
|
elapsedLabel,
|
||||||
|
style: AppTextStyles.scoreNumber(context),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Wrap(
|
Wrap(
|
||||||
@ -1980,6 +1989,7 @@ final class _WorkoutPlanSheet extends StatefulWidget {
|
|||||||
required this.currentPosition,
|
required this.currentPosition,
|
||||||
required this.setStates,
|
required this.setStates,
|
||||||
required this.activeUseCases,
|
required this.activeUseCases,
|
||||||
|
this.stepUseCases,
|
||||||
this.activeRest,
|
this.activeRest,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1988,6 +1998,7 @@ final class _WorkoutPlanSheet extends StatefulWidget {
|
|||||||
final ExecutionPosition currentPosition;
|
final ExecutionPosition currentPosition;
|
||||||
final List<SetResultPositionState> setStates;
|
final List<SetResultPositionState> setStates;
|
||||||
final ActiveWorkoutSessionUseCases activeUseCases;
|
final ActiveWorkoutSessionUseCases activeUseCases;
|
||||||
|
final ActiveExerciseStepUseCases? stepUseCases;
|
||||||
final ActiveRestState? activeRest;
|
final ActiveRestState? activeRest;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -2099,6 +2110,7 @@ final class _WorkoutPlanSheetState extends State<_WorkoutPlanSheet> {
|
|||||||
)],
|
)],
|
||||||
onUpdated: _refreshAfterEdit,
|
onUpdated: _refreshAfterEdit,
|
||||||
activeUseCases: widget.activeUseCases,
|
activeUseCases: widget.activeUseCases,
|
||||||
|
stepUseCases: widget.stepUseCases,
|
||||||
sessionId: widget.sessionId,
|
sessionId: widget.sessionId,
|
||||||
activeRest: widget.activeRest,
|
activeRest: widget.activeRest,
|
||||||
),
|
),
|
||||||
@ -2524,7 +2536,7 @@ File _fileFromLocalUri(String localUri) {
|
|||||||
return File(localUri);
|
return File(localUri);
|
||||||
}
|
}
|
||||||
|
|
||||||
final class _WorkoutPlanSetTile extends StatelessWidget {
|
final class _WorkoutPlanSetTile extends StatefulWidget {
|
||||||
const _WorkoutPlanSetTile({
|
const _WorkoutPlanSetTile({
|
||||||
required this.position,
|
required this.position,
|
||||||
required this.currentPosition,
|
required this.currentPosition,
|
||||||
@ -2532,6 +2544,7 @@ final class _WorkoutPlanSetTile extends StatelessWidget {
|
|||||||
required this.onUpdated,
|
required this.onUpdated,
|
||||||
required this.activeUseCases,
|
required this.activeUseCases,
|
||||||
required this.sessionId,
|
required this.sessionId,
|
||||||
|
this.stepUseCases,
|
||||||
this.activeRest,
|
this.activeRest,
|
||||||
this.state,
|
this.state,
|
||||||
});
|
});
|
||||||
@ -2542,12 +2555,51 @@ final class _WorkoutPlanSetTile extends StatelessWidget {
|
|||||||
final SetResultPositionState? state;
|
final SetResultPositionState? state;
|
||||||
final Future<void> Function() onUpdated;
|
final Future<void> Function() onUpdated;
|
||||||
final ActiveWorkoutSessionUseCases activeUseCases;
|
final ActiveWorkoutSessionUseCases activeUseCases;
|
||||||
|
final ActiveExerciseStepUseCases? stepUseCases;
|
||||||
final String sessionId;
|
final String sessionId;
|
||||||
final ActiveRestState? activeRest;
|
final ActiveRestState? activeRest;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_WorkoutPlanSetTile> createState() => _WorkoutPlanSetTileState();
|
||||||
|
}
|
||||||
|
|
||||||
|
final class _WorkoutPlanSetTileState extends State<_WorkoutPlanSetTile> {
|
||||||
|
late final Future<ActiveExerciseStepProgressView?> _stepProgress;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_stepProgress = _loadStepProgress();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ActiveExerciseStepProgressView?> _loadStepProgress() async {
|
||||||
|
final stepUseCases = widget.stepUseCases;
|
||||||
|
if (stepUseCases == null || widget.exercise.steps.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final planState = _planSetState(
|
||||||
|
widget.position,
|
||||||
|
widget.currentPosition,
|
||||||
|
widget.state,
|
||||||
|
);
|
||||||
|
if (planState == _PlanSetStatus.todo) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return stepUseCases.readProgress(
|
||||||
|
sessionId: widget.sessionId,
|
||||||
|
programIndex: widget.position.programIndex,
|
||||||
|
exerciseIndex: widget.position.exerciseIndex,
|
||||||
|
setIndex: widget.position.setIndex,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final planState = _planSetState(position, currentPosition, state);
|
final planState = _planSetState(
|
||||||
|
widget.position,
|
||||||
|
widget.currentPosition,
|
||||||
|
widget.state,
|
||||||
|
);
|
||||||
final tappable = planState != _PlanSetStatus.todo;
|
final tappable = planState != _PlanSetStatus.todo;
|
||||||
final VoidCallback? onTap = tappable
|
final VoidCallback? onTap = tappable
|
||||||
? () => unawaited(_handleTap(context, planState))
|
? () => unawaited(_handleTap(context, planState))
|
||||||
@ -2565,8 +2617,13 @@ final class _WorkoutPlanSetTile extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
enabled: tappable,
|
enabled: tappable,
|
||||||
title: Text('Série ${position.setIndex + 1}'),
|
title: Text('Série ${widget.position.setIndex + 1}'),
|
||||||
subtitle: Text(_planSetSubtitle(planState, state?.result, exercise)),
|
subtitle: _WorkoutPlanSetSubtitle(
|
||||||
|
status: planState,
|
||||||
|
result: widget.state?.result,
|
||||||
|
exercise: widget.exercise,
|
||||||
|
stepProgress: _stepProgress,
|
||||||
|
),
|
||||||
trailing: _PlanStatusBadge(status: planState, onTap: onTap),
|
trailing: _PlanStatusBadge(status: planState, onTap: onTap),
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
),
|
),
|
||||||
@ -2583,19 +2640,59 @@ final class _WorkoutPlanSetTile extends StatelessWidget {
|
|||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
useSafeArea: true,
|
useSafeArea: true,
|
||||||
builder: (context) => _EditSetResultSheet(
|
builder: (context) => _EditSetResultSheet(
|
||||||
sessionId: sessionId,
|
sessionId: widget.sessionId,
|
||||||
position: position,
|
position: widget.position,
|
||||||
exercise: exercise,
|
exercise: widget.exercise,
|
||||||
state: state,
|
state: widget.state,
|
||||||
status: status,
|
status: status,
|
||||||
activeUseCases: activeUseCases,
|
activeUseCases: widget.activeUseCases,
|
||||||
activeRest: activeRest,
|
activeRest: widget.activeRest,
|
||||||
onSaved: onUpdated,
|
onSaved: widget.onUpdated,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final class _WorkoutPlanSetSubtitle extends StatelessWidget {
|
||||||
|
const _WorkoutPlanSetSubtitle({
|
||||||
|
required this.status,
|
||||||
|
required this.result,
|
||||||
|
required this.exercise,
|
||||||
|
required this.stepProgress,
|
||||||
|
});
|
||||||
|
|
||||||
|
final _PlanSetStatus status;
|
||||||
|
final ActiveSetResult? result;
|
||||||
|
final ExecutionExercise exercise;
|
||||||
|
final Future<ActiveExerciseStepProgressView?> stepProgress;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final resultLabel = _planSetSubtitle(status, result, exercise);
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (resultLabel.isNotEmpty) Text(resultLabel),
|
||||||
|
if (exercise.steps.isNotEmpty)
|
||||||
|
FutureBuilder<ActiveExerciseStepProgressView?>(
|
||||||
|
future: stepProgress,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
final progress = snapshot.data;
|
||||||
|
final label = progress == null
|
||||||
|
? 'Séquence : 0/${exercise.steps.length} étapes'
|
||||||
|
: _planStepSequenceSummary(progress);
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.only(top: resultLabel.isEmpty ? 0 : 4),
|
||||||
|
child: Text(label),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final class _EditSetResultSheet extends StatefulWidget {
|
final class _EditSetResultSheet extends StatefulWidget {
|
||||||
const _EditSetResultSheet({
|
const _EditSetResultSheet({
|
||||||
required this.sessionId,
|
required this.sessionId,
|
||||||
@ -2916,6 +3013,29 @@ String _planSetSubtitle(
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _planStepSequenceSummary(ActiveExerciseStepProgressView view) {
|
||||||
|
final state = view.state;
|
||||||
|
final stepsCount = view.steps.length;
|
||||||
|
final passageIndex =
|
||||||
|
state.status == ActiveExerciseStepProgressStatus.sequenceComplete
|
||||||
|
? (view.expectedPassages - 1).clamp(0, 9999).toInt()
|
||||||
|
: state.currentPassageIndex;
|
||||||
|
final completedInPassage =
|
||||||
|
state.status == ActiveExerciseStepProgressStatus.sequenceComplete
|
||||||
|
? stepsCount
|
||||||
|
: view.results
|
||||||
|
.where((result) => result.passageIndex == passageIndex)
|
||||||
|
.map((result) => result.stepIndex)
|
||||||
|
.toSet()
|
||||||
|
.length
|
||||||
|
.clamp(0, stepsCount)
|
||||||
|
.toInt();
|
||||||
|
final passageLabel = view.expectedPassages > 1
|
||||||
|
? ' · Passage ${passageIndex + 1}/${view.expectedPassages}'
|
||||||
|
: '';
|
||||||
|
return 'Séquence : $completedInPassage/$stepsCount étapes$passageLabel';
|
||||||
|
}
|
||||||
|
|
||||||
String _setResultSummary(
|
String _setResultSummary(
|
||||||
ActiveSetResult result, {
|
ActiveSetResult result, {
|
||||||
ExecutionExercise? exercise,
|
ExecutionExercise? exercise,
|
||||||
@ -3210,26 +3330,28 @@ List<String> _imageMediaIdsFromSnapshot(Map<String, dynamic> exercise) {
|
|||||||
List<ExerciseStep> _exerciseStepsFromSnapshot(Map<String, dynamic> exercise) {
|
List<ExerciseStep> _exerciseStepsFromSnapshot(Map<String, dynamic> exercise) {
|
||||||
final rawSteps = exercise['exerciseStepsSnapshot'];
|
final rawSteps = exercise['exerciseStepsSnapshot'];
|
||||||
if (rawSteps is! List) return const [];
|
if (rawSteps is! List) return const [];
|
||||||
return rawSteps.map((rawStep) {
|
return rawSteps
|
||||||
final step = rawStep as Map<String, dynamic>;
|
.map((rawStep) {
|
||||||
final typeName = step['type'] as String? ?? ExerciseStepType.time.name;
|
final step = rawStep as Map<String, dynamic>;
|
||||||
return ExerciseStep(
|
final typeName = step['type'] as String? ?? ExerciseStepType.time.name;
|
||||||
id: step['id'] as String,
|
return ExerciseStep(
|
||||||
position: step['position'] as int,
|
id: step['id'] as String,
|
||||||
name: step['name'] as String,
|
position: step['position'] as int,
|
||||||
type: ExerciseStepType.values.firstWhere(
|
name: step['name'] as String,
|
||||||
(type) => type.name == typeName,
|
type: ExerciseStepType.values.firstWhere(
|
||||||
orElse: () => ExerciseStepType.time,
|
(type) => type.name == typeName,
|
||||||
),
|
orElse: () => ExerciseStepType.time,
|
||||||
defaultTargetValue: step['defaultTargetValue'] as int,
|
),
|
||||||
hasScore: step['hasScore'] as bool? ?? false,
|
defaultTargetValue: step['defaultTargetValue'] as int,
|
||||||
scoreInputMode: _scoreInputModeFromSnapshot(step['scoreInputMode']),
|
hasScore: step['hasScore'] as bool? ?? false,
|
||||||
scoreLabel: step['scoreLabel'] as String?,
|
scoreInputMode: _scoreInputModeFromSnapshot(step['scoreInputMode']),
|
||||||
scoreUnit: step['scoreUnit'] as String?,
|
scoreLabel: step['scoreLabel'] as String?,
|
||||||
defaultTargetScore: (step['defaultTargetScore'] as num?)?.toDouble(),
|
scoreUnit: step['scoreUnit'] as String?,
|
||||||
defaultTargetScoreTimeMs: step['defaultTargetScoreTimeMs'] as int?,
|
defaultTargetScore: (step['defaultTargetScore'] as num?)?.toDouble(),
|
||||||
);
|
defaultTargetScoreTimeMs: step['defaultTargetScoreTimeMs'] as int?,
|
||||||
}).toList(growable: false);
|
);
|
||||||
|
})
|
||||||
|
.toList(growable: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
ScoreInputMode _scoreInputModeFromSnapshot(Object? value) {
|
ScoreInputMode _scoreInputModeFromSnapshot(Object? value) {
|
||||||
|
|||||||
@ -75,6 +75,33 @@ void main() {
|
|||||||
expect(find.textContaining('Score 38.7'), findsNothing);
|
expect(find.textContaining('Score 38.7'), findsNothing);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('le détail affiche les résultats d’étapes par passage', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: HistoryDetailScreen(
|
||||||
|
history: _history(id: 'history-1', withStepResults: true),
|
||||||
|
historyUseCases: _historyUseCases(_FakeWorkoutHistoryRepository()),
|
||||||
|
workoutTemplateUseCases: _workoutTemplateUseCases(),
|
||||||
|
activeUseCases: _activeUseCases(),
|
||||||
|
closeUseCase: _closeUseCase(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.text('Étapes réalisées'), findsOneWidget);
|
||||||
|
expect(find.text('Passage 1'), findsOneWidget);
|
||||||
|
expect(
|
||||||
|
find.text(
|
||||||
|
'Étape 1 · Dribble gauche · Temps 00:10 · Réussites : 4 paniers',
|
||||||
|
),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
expect(find.text('Étape 2 · Dribble droite'), findsOneWidget);
|
||||||
|
expect(find.text('Passée'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('la suppression demande une confirmation', (tester) async {
|
testWidgets('la suppression demande une confirmation', (tester) async {
|
||||||
final repository = _FakeWorkoutHistoryRepository();
|
final repository = _FakeWorkoutHistoryRepository();
|
||||||
|
|
||||||
@ -114,6 +141,7 @@ WorkoutHistory _history({
|
|||||||
required String id,
|
required String id,
|
||||||
DateTime? startedAt,
|
DateTime? startedAt,
|
||||||
bool stopwatchScore = false,
|
bool stopwatchScore = false,
|
||||||
|
bool withStepResults = false,
|
||||||
}) {
|
}) {
|
||||||
final start = startedAt ?? DateTime.utc(2026, 7, 17, 10);
|
final start = startedAt ?? DateTime.utc(2026, 7, 17, 10);
|
||||||
return WorkoutHistory(
|
return WorkoutHistory(
|
||||||
@ -129,6 +157,7 @@ WorkoutHistory _history({
|
|||||||
'sessionId': 'session-1',
|
'sessionId': 'session-1',
|
||||||
'resolvedTemplateSnapshotJson': _resolvedSnapshot(
|
'resolvedTemplateSnapshotJson': _resolvedSnapshot(
|
||||||
stopwatchScore: stopwatchScore,
|
stopwatchScore: stopwatchScore,
|
||||||
|
withSteps: withStepResults,
|
||||||
),
|
),
|
||||||
'results': [
|
'results': [
|
||||||
{
|
{
|
||||||
@ -150,10 +179,57 @@ WorkoutHistory _history({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
|
stepResults: withStepResults
|
||||||
|
? [
|
||||||
|
WorkoutHistoryStepResult(
|
||||||
|
metadata: _metadata('step-result-1'),
|
||||||
|
workoutHistoryId: id,
|
||||||
|
programSnapshotId: 'program-snapshot-1',
|
||||||
|
exerciseSnapshotId: 'exercise-snapshot-1',
|
||||||
|
programIndex: 0,
|
||||||
|
exerciseIndex: 0,
|
||||||
|
setIndex: 0,
|
||||||
|
passageIndex: 0,
|
||||||
|
stepIndex: 0,
|
||||||
|
stepSnapshotId: 'step-1',
|
||||||
|
stepNameSnapshot: 'Dribble gauche',
|
||||||
|
stepTypeSnapshot: ExerciseStepType.time,
|
||||||
|
targetValueSnapshot: 10,
|
||||||
|
hasScoreSnapshot: true,
|
||||||
|
scoreInputModeSnapshot: ScoreInputMode.manual,
|
||||||
|
scoreLabelSnapshot: 'Réussites',
|
||||||
|
scoreUnitSnapshot: 'paniers',
|
||||||
|
targetScoreSnapshot: 3,
|
||||||
|
status: SetResultStatus.completed,
|
||||||
|
actualTimeMs: 10000,
|
||||||
|
actualScore: 4,
|
||||||
|
),
|
||||||
|
WorkoutHistoryStepResult(
|
||||||
|
metadata: _metadata('step-result-2'),
|
||||||
|
workoutHistoryId: id,
|
||||||
|
programSnapshotId: 'program-snapshot-1',
|
||||||
|
exerciseSnapshotId: 'exercise-snapshot-1',
|
||||||
|
programIndex: 0,
|
||||||
|
exerciseIndex: 0,
|
||||||
|
setIndex: 0,
|
||||||
|
passageIndex: 0,
|
||||||
|
stepIndex: 1,
|
||||||
|
stepSnapshotId: 'step-2',
|
||||||
|
stepNameSnapshot: 'Dribble droite',
|
||||||
|
stepTypeSnapshot: ExerciseStepType.reps,
|
||||||
|
targetValueSnapshot: 8,
|
||||||
|
hasScoreSnapshot: false,
|
||||||
|
status: SetResultStatus.skipped,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: const [],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
String _resolvedSnapshot({bool stopwatchScore = false}) {
|
String _resolvedSnapshot({
|
||||||
|
bool stopwatchScore = false,
|
||||||
|
bool withSteps = false,
|
||||||
|
}) {
|
||||||
return jsonEncode({
|
return jsonEncode({
|
||||||
'name': 'Séance jambes',
|
'name': 'Séance jambes',
|
||||||
'programs': [
|
'programs': [
|
||||||
@ -179,6 +255,29 @@ String _resolvedSnapshot({bool stopwatchScore = false}) {
|
|||||||
'scoreLabelSnapshot': 'Charge',
|
'scoreLabelSnapshot': 'Charge',
|
||||||
if (!stopwatchScore) 'scoreUnitSnapshot': 'kg',
|
if (!stopwatchScore) 'scoreUnitSnapshot': 'kg',
|
||||||
'restSecondsOverride': 0,
|
'restSecondsOverride': 0,
|
||||||
|
if (withSteps)
|
||||||
|
'exerciseStepsSnapshot': [
|
||||||
|
{
|
||||||
|
'id': 'step-1',
|
||||||
|
'position': 0,
|
||||||
|
'name': 'Dribble gauche',
|
||||||
|
'type': ExerciseStepType.time.name,
|
||||||
|
'defaultTargetValue': 10,
|
||||||
|
'hasScore': true,
|
||||||
|
'scoreInputMode': ScoreInputMode.manual.name,
|
||||||
|
'scoreLabel': 'Réussites',
|
||||||
|
'scoreUnit': 'paniers',
|
||||||
|
'defaultTargetScore': 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'id': 'step-2',
|
||||||
|
'position': 1,
|
||||||
|
'name': 'Dribble droite',
|
||||||
|
'type': ExerciseStepType.reps.name,
|
||||||
|
'defaultTargetValue': 8,
|
||||||
|
'hasScore': false,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -837,6 +837,72 @@ void main() {
|
|||||||
expect(find.textContaining('Temps réalisé 00:38.7'), findsNothing);
|
expect(find.textContaining('Temps réalisé 00:38.7'), findsNothing);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('le plan affiche le résumé de séquence d’une série', (
|
||||||
|
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: 0,
|
||||||
|
resolvedTemplateSnapshotJson: _sessionSnapshot(
|
||||||
|
setsCount: 2,
|
||||||
|
targetReps: 2,
|
||||||
|
exerciseSteps: [
|
||||||
|
_step('step-1', 0, 'Gauche', ExerciseStepType.reps, 4),
|
||||||
|
_step('step-2', 1, 'Droite', ExerciseStepType.reps, 4),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
repository.session = session;
|
||||||
|
repository.stepResults.add(
|
||||||
|
ActiveExerciseStepResult(
|
||||||
|
metadata: _metadata('step-result-1'),
|
||||||
|
activeWorkoutSessionId: 'session-1',
|
||||||
|
programSnapshotId: 'template-program-1',
|
||||||
|
exerciseSnapshotId: 'exercise-1',
|
||||||
|
programIndex: 0,
|
||||||
|
exerciseIndex: 0,
|
||||||
|
setIndex: 0,
|
||||||
|
passageIndex: 0,
|
||||||
|
stepIndex: 0,
|
||||||
|
stepSnapshotId: 'step-1',
|
||||||
|
stepNameSnapshot: 'Gauche',
|
||||||
|
stepTypeSnapshot: ExerciseStepType.reps,
|
||||||
|
targetValueSnapshot: 4,
|
||||||
|
hasScoreSnapshot: false,
|
||||||
|
status: SetResultStatus.completed,
|
||||||
|
actualReps: 4,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: WorkoutExecutionScreen(
|
||||||
|
initialSession: session,
|
||||||
|
activeUseCases: _activeUseCases(repository, clock),
|
||||||
|
stepUseCases: _stepUseCases(repository, clock),
|
||||||
|
stepAudioCuePlayer: _FakeStepAudioCuePlayer(),
|
||||||
|
closeUseCase: _closeUseCase(repository, clock),
|
||||||
|
historyUseCases: _historyUseCases(clock),
|
||||||
|
workoutTemplateUseCases: _workoutTemplateUseCases(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.tap(find.byTooltip('Voir le plan'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Séquence : 1/2 étapes · Passage 1/2'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets(
|
testWidgets(
|
||||||
'modifier une série passée la termine sans changer la position courante',
|
'modifier une série passée la termine sans changer la position courante',
|
||||||
(tester) async {
|
(tester) async {
|
||||||
@ -1011,9 +1077,7 @@ void main() {
|
|||||||
currentSetIndex: 0,
|
currentSetIndex: 0,
|
||||||
resolvedTemplateSnapshotJson: _sessionSnapshot(
|
resolvedTemplateSnapshotJson: _sessionSnapshot(
|
||||||
targetReps: 1,
|
targetReps: 1,
|
||||||
exerciseSteps: [
|
exerciseSteps: [_step('step-1', 0, 'Pompes', ExerciseStepType.reps, 8)],
|
||||||
_step('step-1', 0, 'Pompes', ExerciseStepType.reps, 8),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
repository.session = session;
|
repository.session = session;
|
||||||
@ -1170,9 +1234,7 @@ void main() {
|
|||||||
currentSetIndex: 0,
|
currentSetIndex: 0,
|
||||||
resolvedTemplateSnapshotJson: _sessionSnapshot(
|
resolvedTemplateSnapshotJson: _sessionSnapshot(
|
||||||
targetReps: 2,
|
targetReps: 2,
|
||||||
exerciseSteps: [
|
exerciseSteps: [_step('step-1', 0, 'Pompes', ExerciseStepType.reps, 4)],
|
||||||
_step('step-1', 0, 'Pompes', ExerciseStepType.reps, 4),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
repository.session = session;
|
repository.session = session;
|
||||||
@ -1220,9 +1282,7 @@ void main() {
|
|||||||
setsCount: 2,
|
setsCount: 2,
|
||||||
restSeconds: 0,
|
restSeconds: 0,
|
||||||
targetReps: 2,
|
targetReps: 2,
|
||||||
exerciseSteps: [
|
exerciseSteps: [_step('step-1', 0, 'Pompes', ExerciseStepType.reps, 4)],
|
||||||
_step('step-1', 0, 'Pompes', ExerciseStepType.reps, 4),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
repository.session = session;
|
repository.session = session;
|
||||||
@ -1667,9 +1727,7 @@ final class _FakeActiveSessionRepository implements ActiveSessionRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> saveExerciseStepResult(
|
Future<void> saveExerciseStepResult(ActiveExerciseStepResult result) async {
|
||||||
ActiveExerciseStepResult result,
|
|
||||||
) async {
|
|
||||||
final index = stepResults.indexWhere(
|
final index = stepResults.indexWhere(
|
||||||
(saved) =>
|
(saved) =>
|
||||||
saved.activeWorkoutSessionId == result.activeWorkoutSessionId &&
|
saved.activeWorkoutSessionId == result.activeWorkoutSessionId &&
|
||||||
|
|||||||
Reference in New Issue
Block a user