import 'dart:convert'; import 'package:flutter/material.dart'; import '../application/application.dart'; import '../domain/domain.dart'; import 'workout_execution_screen.dart'; final class HistoryListScreen extends StatefulWidget { const HistoryListScreen({ required this.historyUseCases, required this.workoutTemplateUseCases, required this.activeUseCases, required this.closeUseCase, super.key, }); final WorkoutHistoryUseCases historyUseCases; final WorkoutTemplateUseCases workoutTemplateUseCases; final ActiveWorkoutSessionUseCases activeUseCases; final CloseWorkoutSessionUseCase closeUseCase; @override State createState() => _HistoryListScreenState(); } final class _HistoryListScreenState extends State { late Future> _histories; @override void initState() { super.initState(); _histories = widget.historyUseCases.listActive(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Historique')), body: FutureBuilder>( future: _histories, builder: (context, snapshot) { if (snapshot.connectionState != ConnectionState.done) { return const Center(child: CircularProgressIndicator()); } final histories = snapshot.data ?? const []; if (histories.isEmpty) { return const _CenteredMessage( title: 'Aucune séance terminée', message: 'Les séances terminées apparaîtront ici.', ); } final groups = groupHistoriesByPeriod(histories, DateTime.now()); return ListView( children: [ for (final group in groups.entries) ...[ Padding( padding: const EdgeInsets.fromLTRB(16, 20, 16, 8), child: Text( group.key, style: Theme.of(context).textTheme.titleMedium, ), ), for (final history in group.value) ListTile( title: Text(history.nameSnapshot), subtitle: Text( '${_formatDateTime(history.startedAt)} · ' '${_formatDurationMs(history.totalActiveMs)} · ' '${_historySummary(history)}', ), trailing: const Icon(Icons.chevron_right), onTap: () => _openDetail(history), ), ], ], ); }, ), ); } Future _openDetail(WorkoutHistory history) async { final deleted = await Navigator.of(context).push( MaterialPageRoute( builder: (context) => HistoryDetailScreen( history: history, historyUseCases: widget.historyUseCases, workoutTemplateUseCases: widget.workoutTemplateUseCases, activeUseCases: widget.activeUseCases, closeUseCase: widget.closeUseCase, ), ), ); if (deleted == true) { setState(() { _histories = widget.historyUseCases.listActive(); }); } } } final class HistoryDetailScreen extends StatelessWidget { const HistoryDetailScreen({ required this.history, required this.historyUseCases, required this.workoutTemplateUseCases, required this.activeUseCases, required this.closeUseCase, super.key, }); final WorkoutHistory history; final WorkoutHistoryUseCases historyUseCases; final WorkoutTemplateUseCases workoutTemplateUseCases; final ActiveWorkoutSessionUseCases activeUseCases; final CloseWorkoutSessionUseCase closeUseCase; @override Widget build(BuildContext context) { final detail = HistoryDetailData.fromHistory(history); return Scaffold( appBar: AppBar(title: Text(history.nameSnapshot)), body: ListView( padding: const EdgeInsets.all(16), children: [ Text(_formatDateTime(history.startedAt)), const SizedBox(height: 4), Text('Durée : ${_formatDurationMs(history.totalActiveMs)}'), const SizedBox(height: 16), for (final program in detail.programs) ...[ Text(program.name, style: Theme.of(context).textTheme.titleMedium), const SizedBox(height: 8), for (final exercise in program.exercises) ...[ Text( exercise.name, style: Theme.of(context).textTheme.titleSmall, ), for (final set in exercise.sets) ListTile( dense: true, title: Text('Série ${set.setIndex + 1}'), subtitle: Text(set.summary), ), ], const SizedBox(height: 12), ], const SizedBox(height: 16), FilledButton( onPressed: () => _restart(context), child: const Text('Relancer cette séance'), ), const SizedBox(height: 8), OutlinedButton( onPressed: () => _confirmDelete(context), child: const Text("Supprimer de l'historique"), ), ], ), ); } Future _restart(BuildContext context) async { ActiveWorkoutSession session; final sourceId = history.sourceWorkoutTemplateId; if (sourceId != null && await workoutTemplateUseCases.findById(sourceId) != null) { session = await activeUseCases.startFromTemplate(sourceId); } else { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text( "La séance originale n'existe plus. Une copie va être utilisée.", ), ), ); } session = await activeUseCases.startFromHistory(history); } if (!context.mounted) return; await Navigator.of(context).push( MaterialPageRoute( builder: (context) => WorkoutExecutionScreen( initialSession: session, activeUseCases: activeUseCases, closeUseCase: closeUseCase, historyUseCases: historyUseCases, workoutTemplateUseCases: workoutTemplateUseCases, ), ), ); } Future _confirmDelete(BuildContext context) async { final confirmed = await showDialog( context: context, builder: (context) => AlertDialog( title: const Text("Supprimer de l'historique ?"), content: const Text('Cette action est définitive.'), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(false), child: const Text('Annuler'), ), FilledButton( onPressed: () => Navigator.of(context).pop(true), child: const Text('Supprimer'), ), ], ), ); if (confirmed != true) return; await historyUseCases.delete(history.metadata.id); if (!context.mounted) return; Navigator.of(context).pop(true); } } final class HistoryDetailData { const HistoryDetailData({required this.programs}); factory HistoryDetailData.fromHistory(WorkoutHistory history) { final snapshot = jsonDecode(history.historySnapshotJson) as Map; final resolved = jsonDecode(snapshot['resolvedTemplateSnapshotJson'] as String) as Map; final rawResults = history.results.isNotEmpty ? history.results.map(_resultFromDomain).toList() : (snapshot['results'] as List? ?? const []) .cast>(); final programs = []; final rawPrograms = resolved['programs'] as List? ?? const []; for ( var programIndex = 0; programIndex < rawPrograms.length; programIndex++ ) { final rawProgram = rawPrograms[programIndex] as Map; final programSnapshot = jsonDecode(rawProgram['programSnapshotJson'] as String) as Map; final exercises = []; final rawExercises = programSnapshot['exercises'] as List? ?? const []; for ( var exerciseIndex = 0; exerciseIndex < rawExercises.length; exerciseIndex++ ) { final rawExercise = rawExercises[exerciseIndex] as Map; final exerciseResults = rawResults.where((result) { return result['programIndex'] == programIndex && result['exerciseIndex'] == exerciseIndex; }).toList(); exercises.add( HistoryExerciseDetail( name: rawExercise['exerciseNameSnapshot'] as String, sets: [ for (final result in exerciseResults) HistorySetDetail.fromSnapshot(rawExercise, result), ], ), ); } programs.add( HistoryProgramDetail( name: rawProgram['programNameSnapshot'] as String, exercises: exercises, ), ); } return HistoryDetailData(programs: programs); } final List programs; } final class HistoryProgramDetail { const HistoryProgramDetail({required this.name, required this.exercises}); final String name; final List exercises; } final class HistoryExerciseDetail { const HistoryExerciseDetail({required this.name, required this.sets}); final String name; final List sets; } final class HistorySetDetail { const HistorySetDetail({required this.setIndex, required this.summary}); factory HistorySetDetail.fromSnapshot( Map exercise, Map result, ) { final parts = []; if (exercise['timeEnabled'] == true && result['actualTimeMs'] != null) { parts.add('Temps ${_formatDurationMs(result['actualTimeMs'] as int)}'); } if (exercise['repsEnabled'] == true && result['actualReps'] != null) { parts.add('${result['actualReps']} répétitions'); } if (exercise['scoreEnabled'] == true && result['actualScore'] != null) { final unit = exercise['scoreUnitSnapshot'] as String?; final value = _formatScoreValue(result['actualScore'] as num); parts.add(unit == null ? 'Score $value' : 'Score $value $unit'); } return HistorySetDetail( setIndex: result['setIndex'] as int, summary: parts.isEmpty ? 'Aucune mesure saisie' : parts.join(' · '), ); } final int setIndex; final String summary; } Map> groupHistoriesByPeriod( List histories, DateTime now, ) { final today = DateTime(now.year, now.month, now.day); final weekStart = today.subtract(Duration(days: today.weekday - 1)); final groups = >{ "Aujourd'hui": [], 'Cette semaine': [], 'Plus ancien': [], }; for (final history in histories) { final startedDay = DateTime( history.startedAt.year, history.startedAt.month, history.startedAt.day, ); if (startedDay == today) { groups["Aujourd'hui"]!.add(history); } else if (startedDay.isAfter(weekStart) || startedDay == weekStart) { groups['Cette semaine']!.add(history); } else { groups['Plus ancien']!.add(history); } } groups.removeWhere((key, value) => value.isEmpty); return groups; } Map _resultFromDomain(WorkoutHistorySetResult result) { return { 'programIndex': result.programIndex, 'exerciseIndex': result.exerciseIndex, 'setIndex': result.setIndex, 'actualTimeMs': result.actualTimeMs, 'actualReps': result.actualReps, 'actualScore': result.actualScore, }; } String _historySummary(WorkoutHistory history) { final snapshot = jsonDecode(history.historySnapshotJson) as Map; final rawResults = history.results.isNotEmpty ? history.results.map(_resultFromDomain).toList() : (snapshot['results'] as List? ?? const []) .cast>(); final scoreCount = rawResults .where((result) => result['actualScore'] != null) .length; return '${rawResults.length} séries · $scoreCount score${scoreCount > 1 ? 's' : ''}'; } String _formatScoreValue(num value) { if (value is int || value == value.roundToDouble()) { return value.round().toString(); } return value.toString(); } String _formatDateTime(DateTime value) { final day = value.day.toString().padLeft(2, '0'); final month = value.month.toString().padLeft(2, '0'); final hour = value.hour.toString().padLeft(2, '0'); final minute = value.minute.toString().padLeft(2, '0'); return '$day/$month/${value.year} $hour:$minute'; } String _formatDurationMs(int milliseconds) { final duration = Duration(milliseconds: milliseconds); final minutes = duration.inMinutes.remainder(60).toString().padLeft(2, '0'); final seconds = duration.inSeconds.remainder(60).toString().padLeft(2, '0'); final hours = duration.inHours; if (hours > 0) { return '$hours:$minutes:$seconds'; } return '$minutes:$seconds'; } final class _CenteredMessage extends StatelessWidget { const _CenteredMessage({required this.title, this.message}); final String title; final String? message; @override Widget build(BuildContext context) { return Center( child: Padding( padding: const EdgeInsets.all(24), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( title, textAlign: TextAlign.center, style: Theme.of(context).textTheme.titleMedium, ), if (message != null) ...[ const SizedBox(height: 8), Text(message!, textAlign: TextAlign.center), ], ], ), ), ); } }