feat(core): consolide le chainage type metier d'exercice -> strategie Health Services -> payload montre -> consommation Kotlin

Finalise #183 : modele/persistance des types metier d'exercice, mapping vers la strategie Health Services, propagation du payload dans le contrat watch bridge et consommation cote Kotlin montre. Perimetre complet QA vert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-30 16:17:31 +02:00
parent 30e657a67f
commit 2ae716206a
10 changed files with 444 additions and 22 deletions

View File

@ -33,6 +33,17 @@ enum BusinessExerciseType {
recovery,
}
enum HealthServicesExerciseType {
running('RUNNING'),
walking('WALKING'),
highIntensityIntervalTraining('HIGH_INTENSITY_INTERVAL_TRAINING'),
workout('WORKOUT');
const HealthServicesExerciseType(this.wireName);
final String wireName;
}
enum ActiveWorkoutStatus { running, paused, savedExit, completed, abandoned }
enum SetResultStatus { completed, skipped }
@ -439,6 +450,11 @@ final class Exercise {
? _businessTypesFromCategory(category)
: businessTypes;
List<HealthServicesExerciseType> get healthServicesExerciseTypeStrategy =>
healthServicesExerciseTypeStrategyForBusinessTypes(
effectiveBusinessTypes,
);
Exercise archive(DateTime now) {
return copyWith(archivedAt: now, metadata: metadata.touch(now));
}
@ -639,6 +655,9 @@ final class ProgramExercise {
this.exerciseImageMediaIdSnapshot,
List<String> exerciseImageMediaIdsSnapshot = const [],
this.exerciseVideoMediaIdSnapshot,
List<HealthServicesExerciseType>
healthServicesExerciseTypeStrategySnapshot =
const [],
List<ExerciseStep> exerciseStepsSnapshot = const [],
this.autoStartNextTimedStepSnapshot = true,
this.autoStartNextTimedStepOverride,
@ -668,6 +687,12 @@ final class ProgramExercise {
? [exerciseImageMediaIdSnapshot]
: exerciseImageMediaIdsSnapshot,
),
healthServicesExerciseTypeStrategySnapshot =
_validatedHealthServicesExerciseTypes(
healthServicesExerciseTypeStrategySnapshot.isEmpty
? legacyHealthServicesExerciseTypeStrategy
: healthServicesExerciseTypeStrategySnapshot,
),
exerciseStepsSnapshot = _validatedExerciseSteps(exerciseStepsSnapshot) {
_requireNonNegative(position, 'Position');
_requirePositive(setsCount, 'Sets count');
@ -712,6 +737,8 @@ final class ProgramExercise {
final String? exerciseImageMediaIdSnapshot;
final List<String> exerciseImageMediaIdsSnapshot;
final String? exerciseVideoMediaIdSnapshot;
final List<HealthServicesExerciseType>
healthServicesExerciseTypeStrategySnapshot;
final List<ExerciseStep> exerciseStepsSnapshot;
final bool autoStartNextTimedStepSnapshot;
final bool? autoStartNextTimedStepOverride;
@ -761,6 +788,8 @@ final class ProgramExercise {
exerciseImageMediaIdSnapshot: exercise.imageMediaId,
exerciseImageMediaIdsSnapshot: exercise.imageMediaIds,
exerciseVideoMediaIdSnapshot: exercise.videoMediaId,
healthServicesExerciseTypeStrategySnapshot:
exercise.healthServicesExerciseTypeStrategy,
exerciseStepsSnapshot: exercise.steps,
autoStartNextTimedStepSnapshot: exercise.autoStartNextTimedStep,
exerciseArchivedSnapshot: exercise.archivedAt != null,
@ -792,6 +821,10 @@ final class ProgramExercise {
'exerciseImageMediaIdsSnapshot': exerciseImageMediaIdsSnapshot,
'imageMediaIdsSnapshot': exerciseImageMediaIdsSnapshot,
'exerciseVideoMediaIdSnapshot': exerciseVideoMediaIdSnapshot,
'healthServicesExerciseTypeStrategy':
healthServicesExerciseTypeStrategySnapshot
.map((type) => type.wireName)
.toList(),
'exerciseStepsSnapshot': exerciseStepsSnapshot
.map((step) => step.toSnapshotJson())
.toList(),
@ -2021,6 +2054,25 @@ List<BusinessExerciseType> _validatedBusinessExerciseTypes(
return List.unmodifiable(unique);
}
List<HealthServicesExerciseType> _validatedHealthServicesExerciseTypes(
List<HealthServicesExerciseType> types,
) {
final unique = <HealthServicesExerciseType>[];
for (final type in types) {
if (!unique.contains(type)) {
unique.add(type);
}
}
return List.unmodifiable(unique);
}
const legacyHealthServicesExerciseTypeStrategy = [
HealthServicesExerciseType.running,
HealthServicesExerciseType.walking,
HealthServicesExerciseType.highIntensityIntervalTraining,
HealthServicesExerciseType.workout,
];
List<BusinessExerciseType> _businessTypesFromCategory(
ExerciseCategory category,
) {
@ -2036,6 +2088,49 @@ List<BusinessExerciseType> _businessTypesFromCategory(
};
}
List<HealthServicesExerciseType>
healthServicesExerciseTypeStrategyForBusinessTypes(
List<BusinessExerciseType> types,
) {
if (types.isEmpty) {
return legacyHealthServicesExerciseTypeStrategy;
}
final output = <HealthServicesExerciseType>[];
void add(HealthServicesExerciseType type) {
if (!output.contains(type)) {
output.add(type);
}
}
for (final type in types) {
switch (type) {
case BusinessExerciseType.dribble:
case BusinessExerciseType.finishing:
add(HealthServicesExerciseType.running);
add(HealthServicesExerciseType.highIntensityIntervalTraining);
add(HealthServicesExerciseType.workout);
case BusinessExerciseType.conditioning:
case BusinessExerciseType.highIntensity:
case BusinessExerciseType.defense:
add(HealthServicesExerciseType.highIntensityIntervalTraining);
add(HealthServicesExerciseType.running);
add(HealthServicesExerciseType.workout);
case BusinessExerciseType.mobility:
case BusinessExerciseType.recovery:
add(HealthServicesExerciseType.walking);
add(HealthServicesExerciseType.running);
add(HealthServicesExerciseType.workout);
case BusinessExerciseType.shoot:
case BusinessExerciseType.freeThrows:
add(HealthServicesExerciseType.highIntensityIntervalTraining);
add(HealthServicesExerciseType.running);
add(HealthServicesExerciseType.walking);
add(HealthServicesExerciseType.workout);
}
}
return List.unmodifiable(output);
}
void _requireAtLeastOneMeasure({
required bool hasTime,
required bool hasReps,