import 'package:flutter_bloc/flutter_bloc.dart'; import '../../models/activity.dart'; import '../../repositories/activity_repository.dart'; import '../../services/activity_places_service.dart'; import '../../services/activity_cache_service.dart'; import '../../services/error_service.dart'; import 'activity_event.dart'; import 'activity_state.dart'; /// BLoC for managing activity-related state and operations class ActivityBloc extends Bloc { final ActivityRepository _repository; final ActivityPlacesService _placesService; final ErrorService _errorService; ActivityBloc({ required ActivityRepository repository, required ActivityPlacesService placesService, required ErrorService errorService, }) : _repository = repository, _placesService = placesService, _errorService = errorService, super(const ActivityInitial()) { on(_onLoadActivities); on(_onLoadTripActivitiesPreservingSearch); on(_onSearchActivities); on(_onSearchActivitiesWithCoordinates); on(_onSearchActivitiesByText); on(_onAddActivity); on(_onAddActivitiesBatch); on(_onVoteForActivity); on(_onDeleteActivity); on(_onFilterActivities); on(_onRefreshActivities); on(_onClearSearchResults); on(_onUpdateActivity); on(_onToggleActivityFavorite); on(_onRestoreCachedSearchResults); on(_onRemoveFromSearchResults); on(_onAddActivityAndRemoveFromSearch); } /// Handles loading activities for a trip Future _onLoadActivities( LoadActivities event, Emitter emit, ) async { try { emit(const ActivityLoading()); final activities = await _repository.getActivitiesByTrip(event.tripId); 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 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, Emitter emit, ) async { try { // Si c'est un append (charger plus), on garde l'état actuel et on met isLoading à true if (event.appendToExisting && state is ActivitySearchResults) { final currentState = state as ActivitySearchResults; emit(currentState.copyWith(isLoading: true)); } else { emit(const ActivitySearching()); } final searchResults = await _placesService.searchActivities( destination: event.destination, tripId: event.tripId, category: event.category, maxResults: event.maxResults ?? 20, // Par défaut 20, ou utiliser la valeur spécifiée offset: event.offset ?? 0, // Par défaut 0 ); List finalResults; // Si on doit ajouter aux résultats existants if (event.appendToExisting && state is ActivitySearchResults) { final currentState = state as ActivitySearchResults; finalResults = [...currentState.searchResults, ...searchResults]; } else { finalResults = searchResults; } // Mettre en cache les résultats ActivityCacheService().setCachedActivities(event.tripId, finalResults); emit(ActivitySearchResults( searchResults: finalResults, query: event.category?.displayName ?? 'Toutes les activités', isLoading: false, )); } catch (e) { _errorService.logError('activity_bloc', 'Erreur recherche activités: $e'); emit(const ActivityError('Impossible de rechercher les activités')); } } /// Handles searching activities using coordinates directly (bypasses geocoding) Future _onSearchActivitiesWithCoordinates( SearchActivitiesWithCoordinates event, Emitter emit, ) async { try { // Si c'est un append (charger plus), on garde l'état actuel et on met isLoading à true if (event.appendToExisting && state is ActivitySearchResults) { final currentState = state as ActivitySearchResults; emit(currentState.copyWith(isLoading: true)); } else { emit(const ActivitySearching()); } final searchResults = await _placesService.searchActivitiesPaginated( latitude: event.latitude, longitude: event.longitude, tripId: event.tripId, category: event.category, pageSize: event.maxResults ?? 20, ); final activities = searchResults['activities'] as List; List finalResults; // Si on doit ajouter aux résultats existants if (event.appendToExisting && state is ActivitySearchResults) { final currentState = state as ActivitySearchResults; finalResults = [...currentState.searchResults, ...activities]; } else { finalResults = activities; } // Mettre en cache les résultats ActivityCacheService().setCachedActivities(event.tripId, finalResults); emit(ActivitySearchResults( searchResults: finalResults, query: event.category?.displayName ?? 'Toutes les activités', isLoading: false, )); } catch (e) { _errorService.logError('activity_bloc', 'Erreur recherche activités avec coordonnées: $e'); emit(const ActivityError('Impossible de rechercher les activités')); } } /// Handles text-based activity search Future _onSearchActivitiesByText( SearchActivitiesByText event, Emitter emit, ) async { try { emit(const ActivitySearching()); final searchResults = await _placesService.searchActivitiesByText( query: event.query, destination: event.destination, tripId: event.tripId, ); // Mettre en cache les résultats ActivityCacheService().setCachedActivities(event.tripId, searchResults); emit(ActivitySearchResults( searchResults: searchResults, query: event.query, )); } catch (e) { _errorService.logError('activity_bloc', 'Erreur recherche textuelle: $e'); emit(const ActivityError('Impossible de rechercher les activités')); } } /// Handles adding a single activity Future _onAddActivity( AddActivity 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 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 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é')); } } catch (e) { _errorService.logError('activity_bloc', 'Erreur ajout activité: $e'); emit(const ActivityError('Impossible d\'ajouter l\'activité')); } } /// Handles adding multiple activities in batch Future _onAddActivitiesBatch( AddActivitiesBatch event, Emitter emit, ) async { try { // Filter out existing activities final filteredActivities = []; emit(ActivityBatchAdding( activitiesToAdd: event.activities, progress: 0, total: event.activities.length, )); for (int i = 0; i < event.activities.length; i++) { final activity = event.activities[i]; if (activity.placeId != null) { final existing = await _repository.findExistingActivity( activity.tripId, activity.placeId!, ); if (existing == null) { filteredActivities.add(activity); } } else { filteredActivities.add(activity); } // Update progress emit(ActivityBatchAdding( activitiesToAdd: event.activities, progress: i + 1, total: event.activities.length, )); } if (filteredActivities.isEmpty) { emit(const ActivityError('Toutes les activités ont déjà été ajoutées')); return; } final addedIds = await _repository.addActivitiesBatch(filteredActivities); if (addedIds.isNotEmpty) { emit(ActivityOperationSuccess( '${addedIds.length} activité(s) ajoutée(s) avec succès', operationType: 'batch_add', )); // Reload activities add(LoadActivities(event.activities.first.tripId)); } else { emit(const ActivityError('Impossible d\'ajouter les activités')); } } catch (e) { _errorService.logError('activity_bloc', 'Erreur ajout en lot: $e'); emit(const ActivityError('Impossible d\'ajouter les activités')); } } /// Handles voting for an activity Future _onVoteForActivity( VoteForActivity event, Emitter emit, ) async { try { // Show voting state if (state is ActivityLoaded) { final currentState = state as ActivityLoaded; emit(ActivityVoting( activityId: event.activityId, activities: currentState.activities, )); } final success = await _repository.voteForActivity( event.activityId, event.userId, event.vote, ); if (success) { emit(ActivityVoteRecorded( activityId: event.activityId, vote: event.vote, userId: event.userId, )); // Reload activities to reflect the new vote if (state is ActivityLoaded) { final currentState = state as ActivityLoaded; final activities = await _repository.getActivitiesByTrip( currentState.activities.first.tripId, ); emit(currentState.copyWith( activities: activities, filteredActivities: _applyFilters( activities, currentState.activeFilter, currentState.minRating, currentState.showVotedOnly, event.userId, ), )); } } else { emit(const ActivityError('Impossible d\'enregistrer le vote')); } } catch (e) { _errorService.logError('activity_bloc', 'Erreur vote: $e'); emit(const ActivityError('Impossible d\'enregistrer le vote')); } } /// Handles deleting an activity Future _onDeleteActivity( DeleteActivity event, Emitter emit, ) async { try { final success = await _repository.deleteActivity(event.activityId); if (success) { emit(ActivityDeleted( activityId: event.activityId, message: 'Activité supprimée avec succès', )); // Reload if we're on the activity list if (state is ActivityLoaded) { final currentState = state as ActivityLoaded; if (currentState.activities.isNotEmpty) { add(LoadActivities(currentState.activities.first.tripId)); } } } else { emit(const ActivityError('Impossible de supprimer l\'activité')); } } catch (e) { _errorService.logError('activity_bloc', 'Erreur suppression: $e'); emit(const ActivityError('Impossible de supprimer l\'activité')); } } /// Handles filtering activities Future _onFilterActivities( FilterActivities event, Emitter emit, ) async { if (state is ActivityLoaded) { final currentState = state as ActivityLoaded; final filteredActivities = _applyFilters( currentState.activities, event.category, event.minRating, event.showVotedOnly ?? false, '', // UserId would be needed for showVotedOnly filter ); emit(currentState.copyWith( filteredActivities: filteredActivities, activeFilter: event.category, minRating: event.minRating, showVotedOnly: event.showVotedOnly ?? false, )); } } /// Handles refreshing activities Future _onRefreshActivities( RefreshActivities event, Emitter emit, ) async { add(LoadActivities(event.tripId)); } /// Handles clearing search results Future _onClearSearchResults( ClearSearchResults event, Emitter emit, ) async { if (state is ActivitySearchResults) { emit(const ActivityInitial()); } } /// Handles updating an activity Future _onUpdateActivity( UpdateActivity event, Emitter emit, ) async { try { if (state is ActivityLoaded) { final currentState = state as ActivityLoaded; emit(ActivityUpdating( activityId: event.activity.id, activities: currentState.activities, )); } final success = await _repository.updateActivity(event.activity); if (success) { emit(const ActivityOperationSuccess( 'Activité mise à jour avec succès', operationType: 'update', )); // Reload activities add(LoadActivities(event.activity.tripId)); } else { emit(const ActivityError('Impossible de mettre à jour l\'activité')); } } catch (e) { _errorService.logError('activity_bloc', 'Erreur mise à jour: $e'); emit(const ActivityError('Impossible de mettre à jour l\'activité')); } } /// Handles toggling activity favorite status Future _onToggleActivityFavorite( ToggleActivityFavorite event, Emitter emit, ) async { try { // This would require extending the Activity model to include favorites // For now, we'll use the voting system as a favorite system add(VoteForActivity( activityId: event.activityId, userId: event.userId, vote: 1, )); } catch (e) { _errorService.logError('activity_bloc', 'Erreur favori: $e'); emit(const ActivityError('Impossible de modifier les favoris')); } } /// Applies filters to the activities list List _applyFilters( List activities, String? category, double? minRating, bool showVotedOnly, String userId, ) { var filtered = activities; if (category != null) { filtered = filtered.where((a) => a.category == category).toList(); } if (minRating != null) { filtered = filtered.where((a) => (a.rating ?? 0) >= minRating).toList(); } if (showVotedOnly && userId.isNotEmpty) { filtered = filtered.where((a) => a.hasUserVoted(userId)).toList(); } 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, Emitter emit, ) async { emit(ActivitySearchResults( searchResults: event.searchResults, query: 'cached', isLoading: false, )); } }