feat(ui): integre affichage stats par scope et selection des types metier d'exercice cote telephone

UI telephone pour #179 (graphes/statistiques par etape, serie, exercice, seance) et #183 (choix des types metier d'exercice, hors pont Health Services).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-30 14:55:52 +02:00
parent fdaa518fa9
commit 36691643d7
8 changed files with 447 additions and 59 deletions

View File

@ -361,7 +361,7 @@ final class ExerciseListTile extends StatelessWidget {
spacing: 6,
runSpacing: 6,
children: [
_CategoryBadge(category: exercise.category),
_BusinessTypeBadge(types: exercise.effectiveBusinessTypes),
...exercise.availableMeasures.map(
(measure) => MeasureBadge(
measure: measure,
@ -398,17 +398,19 @@ final class ExerciseListTile extends StatelessWidget {
}
}
final class _CategoryBadge extends StatelessWidget {
const _CategoryBadge({required this.category});
final class _BusinessTypeBadge extends StatelessWidget {
const _BusinessTypeBadge({required this.types});
final ExerciseCategory category;
final List<BusinessExerciseType> types;
@override
Widget build(BuildContext context) {
return Chip(
visualDensity: VisualDensity.compact,
label: Text(category.label),
);
final label = types.isEmpty
? 'Libre / autre'
: types.length == 1
? types.single.label
: '${types.first.label} +${types.length - 1}';
return Chip(visualDensity: VisualDensity.compact, label: Text(label));
}
}
@ -487,6 +489,7 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
var _hasReps = false;
var _hasScore = false;
var _scoreInputMode = ScoreInputMode.manual;
var _businessTypes = <BusinessExerciseType>[];
var _saving = false;
var _importingImage = false;
var _importingVideo = false;
@ -529,6 +532,9 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
_hasReps = exercise?.hasRepsMeasure ?? false;
_hasScore = exercise?.hasScoreMeasure ?? false;
_scoreInputMode = exercise?.scoreInputMode ?? ScoreInputMode.manual;
_businessTypes = List<BusinessExerciseType>.of(
exercise?.effectiveBusinessTypes ?? const [],
);
_stepsEnabled = exercise?.steps.isNotEmpty ?? false;
_autoStartNextTimedStep = exercise?.autoStartNextTimedStep ?? true;
_tags = List<String>.of(exercise?.tags ?? const []);
@ -781,6 +787,8 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
],
const SizedBox(height: 24),
_buildExerciseStepsSection(context),
const SizedBox(height: 16),
_buildBusinessTypesSection(context),
if (_warning != null) ...[
const SizedBox(height: 16),
MaterialBanner(
@ -811,6 +819,51 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
);
}
Widget _buildBusinessTypesSection(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Type dexercice', style: Theme.of(context).textTheme.titleSmall),
const SizedBox(height: 8),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
FilterChip(
label: const Text('Libre / autre'),
selected: _businessTypes.isEmpty,
onSelected: (_) => setState(() => _businessTypes = []),
),
const SizedBox(width: 8),
for (final type in BusinessExerciseType.values) ...[
FilterChip(
label: Text(type.label),
selected: _businessTypes.contains(type),
onSelected: (selected) {
setState(() {
final next = List<BusinessExerciseType>.of(
_businessTypes,
);
if (selected) {
if (!next.contains(type)) {
next.add(type);
}
} else {
next.remove(type);
}
_businessTypes = next;
});
},
),
const SizedBox(width: 8),
],
],
),
),
],
);
}
Widget _buildExerciseStepsSection(BuildContext context) {
final reachedLimit = _stepDrafts.length >= 8;
return Column(
@ -1467,6 +1520,7 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
defaultTargetScore: defaultTargetScore,
defaultTargetScoreTimeMs: defaultTargetScoreTimeMs,
autoStartNextTimedStep: _autoStartNextTimedStep,
businessTypes: _businessTypes,
steps: steps,
tags: _tags,
);
@ -1489,6 +1543,7 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
defaultTargetScore: defaultTargetScore,
defaultTargetScoreTimeMs: defaultTargetScoreTimeMs,
autoStartNextTimedStep: _autoStartNextTimedStep,
businessTypes: _businessTypes,
steps: steps,
tags: _tags,
);
@ -2022,17 +2077,18 @@ extension on WorkoutMeasure {
}
}
extension on ExerciseCategory {
extension on BusinessExerciseType {
String get label {
return switch (this) {
ExerciseCategory.shoot => 'Tir',
ExerciseCategory.freeThrows => 'Lancers francs',
ExerciseCategory.dribble => 'Dribble',
ExerciseCategory.finishing => 'Finition',
ExerciseCategory.conditioning => 'Condition physique',
ExerciseCategory.defense => 'Défense',
ExerciseCategory.mobility => 'Mobilité',
ExerciseCategory.uncategorized => 'Non catégorisé',
BusinessExerciseType.shoot => 'Tir',
BusinessExerciseType.freeThrows => 'Lancers francs',
BusinessExerciseType.dribble => 'Dribble',
BusinessExerciseType.finishing => 'Finition',
BusinessExerciseType.conditioning => 'Condition physique',
BusinessExerciseType.defense => 'Défense',
BusinessExerciseType.mobility => 'Mobilité',
BusinessExerciseType.highIntensity => 'Haute intensité',
BusinessExerciseType.recovery => 'Récupération',
};
}
}