diff --git a/lib/blocs/activity/activity_bloc.dart b/lib/blocs/activity/activity_bloc.dart index 1ea7318..d224070 100644 --- a/lib/blocs/activity/activity_bloc.dart +++ b/lib/blocs/activity/activity_bloc.dart @@ -23,6 +23,7 @@ class ActivityBloc extends Bloc { super(const ActivityInitial()) { on(_onLoadActivities); + on(_onLoadTripActivitiesPreservingSearch); on(_onSearchActivities); on(_onSearchActivitiesWithCoordinates); on(_onSearchActivitiesByText); @@ -36,6 +37,8 @@ class ActivityBloc extends Bloc { on(_onUpdateActivity); on(_onToggleActivityFavorite); on(_onRestoreCachedSearchResults); + on(_onRemoveFromSearchResults); + on(_onAddActivityAndRemoveFromSearch); } /// Handles loading activities for a trip @@ -58,6 +61,31 @@ class ActivityBloc extends Bloc { } } + /// Handles loading trip activities while preserving search results state + Future _onLoadTripActivitiesPreservingSearch( + LoadTripActivitiesPreservingSearch event, + Emitter emit, + ) async { + try { + final activities = await _repository.getActivitiesByTrip(event.tripId); + + // Si on a un état de recherche actif, on le préserve + if (state is ActivitySearchResults) { + // On garde l'état de recherche inchangé, pas besoin d'émettre + return; + } + + // Sinon, on charge normalement + emit(ActivityLoaded( + activities: activities, + filteredActivities: activities, + )); + } catch (e) { + _errorService.logError('activity_bloc', 'Erreur chargement activités: $e'); + emit(const ActivityError('Impossible de charger les activités')); + } + } + /// Handles searching activities using Google Places API Future _onSearchActivities( SearchActivities event, @@ -200,12 +228,74 @@ class ActivityBloc extends Bloc { final activityId = await _repository.addActivity(event.activity); if (activityId != null) { + // Si on est en état de recherche (suggestions Google), préserver cet état + if (state is ActivitySearchResults) { + // On ne change rien à l'état de recherche, on le garde tel quel + // La suppression de l'activité des résultats se fait dans _onRemoveFromSearchResults + return; + } + + // Sinon, émettre l'état d'ajout réussi emit(ActivityAdded( activity: event.activity.copyWith(id: activityId), message: 'Activité ajoutée avec succès', )); - // Reload activities - add(LoadActivities(event.activity.tripId)); + // Reload activities while preserving search results + add(LoadTripActivitiesPreservingSearch(event.activity.tripId)); + } else { + emit(const ActivityError('Impossible d\'ajouter l\'activité')); + } + } catch (e) { + _errorService.logError('activity_bloc', 'Erreur ajout activité: $e'); + emit(const ActivityError('Impossible d\'ajouter l\'activité')); + } + } + + /// Handles adding an activity and removing it from search results in one action + Future _onAddActivityAndRemoveFromSearch( + AddActivityAndRemoveFromSearch event, + Emitter emit, + ) async { + try { + // Check if activity already exists + if (event.activity.placeId != null) { + final existing = await _repository.findExistingActivity( + event.activity.tripId, + event.activity.placeId!, + ); + + if (existing != null) { + emit(const ActivityError('Cette activité a déjà été ajoutée')); + return; + } + } + + final activityId = await _repository.addActivity(event.activity); + + if (activityId != null) { + // Si on est en état de recherche (suggestions Google), préserver cet état + // en supprimant l'activité des résultats + if (state is ActivitySearchResults) { + final currentState = state as ActivitySearchResults; + final updatedResults = currentState.searchResults + .where((activity) => activity.id != event.googleActivityId) + .toList(); + + emit(ActivitySearchResults( + searchResults: updatedResults, + query: currentState.query, + isLoading: false, + )); + return; + } + + // Sinon, émettre l'état d'ajout réussi + emit(ActivityAdded( + activity: event.activity.copyWith(id: activityId), + message: 'Activité ajoutée avec succès', + )); + // Reload activities while preserving search results + add(LoadTripActivitiesPreservingSearch(event.activity.tripId)); } else { emit(const ActivityError('Impossible d\'ajouter l\'activité')); } @@ -482,6 +572,29 @@ class ActivityBloc extends Bloc { return filtered; } + /// Removes an activity from search results + Future _onRemoveFromSearchResults( + RemoveFromSearchResults event, + Emitter emit, + ) async { + // Si on est actuellement dans un état de résultats de recherche + if (state is ActivitySearchResults) { + final currentState = state as ActivitySearchResults; + + // Filtrer l'activité à retirer + final updatedResults = currentState.searchResults + .where((activity) => activity.id != event.activityId) + .toList(); + + // Émettre le nouvel état avec l'activité retirée + emit(ActivitySearchResults( + searchResults: updatedResults, + query: currentState.query, + isLoading: false, + )); + } + } + /// Restores cached search results Future _onRestoreCachedSearchResults( RestoreCachedSearchResults event, diff --git a/lib/blocs/activity/activity_event.dart b/lib/blocs/activity/activity_event.dart index 44b54eb..b17f2fb 100644 --- a/lib/blocs/activity/activity_event.dart +++ b/lib/blocs/activity/activity_event.dart @@ -19,6 +19,16 @@ class LoadActivities extends ActivityEvent { List get props => [tripId]; } +/// Event to load trip activities without resetting search results +class LoadTripActivitiesPreservingSearch extends ActivityEvent { + final String tripId; + + const LoadTripActivitiesPreservingSearch(this.tripId); + + @override + List get props => [tripId]; +} + /// Event to search activities using Google Places API class SearchActivities extends ActivityEvent { final String tripId; @@ -194,4 +204,28 @@ class RestoreCachedSearchResults extends ActivityEvent { @override List get props => [searchResults]; +} + +/// Event to remove an activity from search results +class RemoveFromSearchResults extends ActivityEvent { + final String activityId; + + const RemoveFromSearchResults({required this.activityId}); + + @override + List get props => [activityId]; +} + +/// Event to add an activity and remove it from search results in one action +class AddActivityAndRemoveFromSearch extends ActivityEvent { + final Activity activity; + final String googleActivityId; + + const AddActivityAndRemoveFromSearch({ + required this.activity, + required this.googleActivityId, + }); + + @override + List get props => [activity, googleActivityId]; } \ No newline at end of file diff --git a/lib/components/activities/activities_page.dart b/lib/components/activities/activities_page.dart index b9ee78f..3595da4 100644 --- a/lib/components/activities/activities_page.dart +++ b/lib/components/activities/activities_page.dart @@ -7,6 +7,7 @@ import '../../models/trip.dart'; import '../../models/activity.dart'; import '../../services/activity_cache_service.dart'; import '../activities/add_activity_bottom_sheet.dart'; +import '../loading/laoding_content.dart'; class ActivitiesPage extends StatefulWidget { final Trip trip; @@ -138,6 +139,37 @@ class _ActivitiesPageState extends State ); } + if (state is ActivityAdded) { + // Fermer le dialog de loading + if (Navigator.of(context).canPop()) { + Navigator.of(context).pop(); + } + + // Ajouter l'activité à la liste locale des activités du voyage + WidgetsBinding.instance.addPostFrameCallback((_) { + setState(() { + _tripActivities.add(state.activity); + }); + + // Afficher un feedback de succès + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('${state.activity.name} ajoutée au voyage !'), + duration: const Duration(seconds: 2), + backgroundColor: Colors.green, + action: SnackBarAction( + label: 'Voir', + textColor: Colors.white, + onPressed: () { + // Revenir à l'onglet des activités du voyage + _tabController.animateTo(0); + }, + ), + ), + ); + }); + } + if (state is ActivityLoaded) { // Stocker les activités localement WidgetsBinding.instance.addPostFrameCallback((_) { @@ -172,11 +204,6 @@ class _ActivitiesPageState extends State // Recharger les activités du voyage pour mettre à jour les votes _loadActivities(); } - - if (state is ActivityAdded) { - // Recharger automatiquement les activités du voyage - _loadActivities(); - } }, child: Scaffold( backgroundColor: theme.colorScheme.surface, @@ -754,21 +781,6 @@ class _ActivitiesPageState extends State ), ), ), - // Bouton de debug temporaire - Container( - width: double.infinity, - padding: const EdgeInsets.symmetric(horizontal: 16), - child: OutlinedButton.icon( - onPressed: () { - _checkAndLoadMoreActivitiesIfNeeded(); - }, - icon: const Icon(Icons.bug_report, size: 16), - label: const Text('🧪 Test Auto-Reload'), - style: OutlinedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 8), - ), - ), - ), ], ), ], @@ -1264,23 +1276,32 @@ class _ActivitiesPageState extends State id: DateTime.now().millisecondsSinceEpoch.toString(), ); - context.read().add(AddActivity(newActivity)); - - // Afficher un feedback à l'utilisateur - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('${activity.name} ajoutée au voyage !'), - duration: const Duration(seconds: 2), - backgroundColor: Colors.green, - action: SnackBarAction( - label: 'Voir', - textColor: Colors.white, - onPressed: () { - // Revenir à l'onglet des activités du voyage - _tabController.animateTo(0); + // Afficher le LoadingContent avec la tâche d'ajout + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext dialogContext) { + return LoadingContent( + loadingText: 'Ajout de ${activity.name}...', + onBackgroundTask: () async { + // Ajouter l'activité au voyage + context.read().add( + AddActivityAndRemoveFromSearch( + activity: newActivity, + googleActivityId: activity.id, + ), + ); + // Attendre que l'ajout soit complété + await Future.delayed(const Duration(milliseconds: 1000)); }, - ), - ), + onComplete: () { + // Fermer le dialog quand l'ajout est complété + if (Navigator.of(context).canPop()) { + Navigator.of(context).pop(); + } + }, + ); + }, ); } diff --git a/lib/components/home/show_trip_details_content.dart b/lib/components/home/show_trip_details_content.dart index f8df749..906699c 100644 --- a/lib/components/home/show_trip_details_content.dart +++ b/lib/components/home/show_trip_details_content.dart @@ -42,7 +42,7 @@ class _ShowTripDetailsContentState extends State { return; // Utiliser le cache } - // Sinon, lancer la recherche + // Sinon, lancer la recherche avec le maximum d'activités context.read().add( widget.trip.hasCoordinates ? SearchActivitiesWithCoordinates( @@ -50,14 +50,14 @@ class _ShowTripDetailsContentState extends State { latitude: widget.trip.latitude!, longitude: widget.trip.longitude!, category: null, - maxResults: 6, + maxResults: 100, // Charger le maximum d'activités possible reset: true, ) : SearchActivities( tripId: widget.trip.id!, destination: widget.trip.location, category: null, - maxResults: 6, + maxResults: 100, // Charger le maximum d'activités possible reset: true, ), );