Files
TravelMate/lib/blocs/activity/activity_bloc.dart

740 lines
22 KiB
Dart

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<ActivityEvent, ActivityState> {
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<LoadActivities>(_onLoadActivities);
on<LoadTripActivitiesPreservingSearch>(
_onLoadTripActivitiesPreservingSearch,
);
on<SearchActivities>(_onSearchActivities);
on<SearchActivitiesWithCoordinates>(_onSearchActivitiesWithCoordinates);
on<SearchActivitiesByText>(_onSearchActivitiesByText);
on<AddActivity>(_onAddActivity);
on<AddActivitiesBatch>(_onAddActivitiesBatch);
on<VoteForActivity>(_onVoteForActivity);
on<DeleteActivity>(_onDeleteActivity);
on<FilterActivities>(_onFilterActivities);
on<RefreshActivities>(_onRefreshActivities);
on<ClearSearchResults>(_onClearSearchResults);
on<UpdateActivity>(_onUpdateActivity);
on<ToggleActivityFavorite>(_onToggleActivityFavorite);
on<RestoreCachedSearchResults>(_onRestoreCachedSearchResults);
on<RemoveFromSearchResults>(_onRemoveFromSearchResults);
on<AddActivityAndRemoveFromSearch>(_onAddActivityAndRemoveFromSearch);
on<UpdateActivityDate>(_onUpdateActivityDate);
}
/// Handles loading activities for a trip
Future<void> _onLoadActivities(
LoadActivities event,
Emitter<ActivityState> emit,
) async {
try {
emit(const ActivityLoading());
final activities = await _repository.getActivitiesByTrip(event.tripId);
emit(
ActivityLoaded(activities: activities, filteredActivities: activities),
);
} catch (e, stackTrace) {
_errorService.logError(
'activity_bloc',
'Erreur chargement activités: $e',
stackTrace,
);
emit(const ActivityError('Impossible de charger les activités'));
}
}
/// Handles loading trip activities while preserving search results state
Future<void> _onLoadTripActivitiesPreservingSearch(
LoadTripActivitiesPreservingSearch event,
Emitter<ActivityState> 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, stackTrace) {
_errorService.logError(
'activity_bloc',
'Erreur chargement activités: $e',
stackTrace,
);
emit(const ActivityError('Impossible de charger les activités'));
}
}
Future<void> _onUpdateActivityDate(
UpdateActivityDate event,
Emitter<ActivityState> emit,
) async {
try {
final activity = await _repository.getActivity(
event.tripId,
event.activityId,
);
if (activity != null) {
final updatedActivity = activity.copyWith(
date: event.date,
clearDate: event.date == null,
);
await _repository.updateActivity(updatedActivity);
// Recharger les activités pour mettre à jour l'UI
add(LoadActivities(event.tripId));
}
} catch (e, stackTrace) {
_errorService.logError(
'activity_bloc',
'Erreur mise à jour date: $e',
stackTrace,
);
emit(const ActivityError('Impossible de mettre à jour la date'));
}
}
/// Handles searching activities using Google Places API
Future<void> _onSearchActivities(
SearchActivities event,
Emitter<ActivityState> 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<Activity> 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, stackTrace) {
_errorService.logError(
'activity_bloc',
'Erreur recherche activités: $e',
stackTrace,
);
emit(const ActivityError('Impossible de rechercher les activités'));
}
}
/// Handles searching activities using coordinates directly (bypasses geocoding)
Future<void> _onSearchActivitiesWithCoordinates(
SearchActivitiesWithCoordinates event,
Emitter<ActivityState> 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<Activity>;
List<Activity> 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, stackTrace) {
_errorService.logError(
'activity_bloc',
'Erreur recherche activités avec coordonnées: $e',
stackTrace,
);
emit(const ActivityError('Impossible de rechercher les activités'));
}
}
/// Handles text-based activity search
Future<void> _onSearchActivitiesByText(
SearchActivitiesByText event,
Emitter<ActivityState> 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, stackTrace) {
_errorService.logError(
'activity_bloc',
'Erreur recherche textuelle: $e',
stackTrace,
);
emit(const ActivityError('Impossible de rechercher les activités'));
}
}
/// Handles adding a single activity
Future<void> _onAddActivity(
AddActivity event,
Emitter<ActivityState> 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) {
final currentState = state as ActivitySearchResults;
// On garde l'état de recherche inchangé mais on ajoute l'info de l'activité ajoutée
emit(
currentState.copyWith(
newlyAddedActivity: event.activity.copyWith(id: activityId),
),
);
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, stackTrace) {
_errorService.logError(
'activity_bloc',
'Erreur ajout activité: $e',
stackTrace,
);
emit(const ActivityError('Impossible d\'ajouter l\'activité'));
}
}
/// Handles adding an activity and removing it from search results in one action
Future<void> _onAddActivityAndRemoveFromSearch(
AddActivityAndRemoveFromSearch event,
Emitter<ActivityState> 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, stackTrace) {
_errorService.logError(
'activity_bloc',
'Erreur ajout activité: $e',
stackTrace,
);
emit(const ActivityError('Impossible d\'ajouter l\'activité'));
}
}
/// Handles adding multiple activities in batch
Future<void> _onAddActivitiesBatch(
AddActivitiesBatch event,
Emitter<ActivityState> emit,
) async {
try {
// Filter out existing activities
final filteredActivities = <Activity>[];
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, stackTrace) {
_errorService.logError(
'activity_bloc',
'Erreur ajout en lot: $e',
stackTrace,
);
emit(const ActivityError('Impossible d\'ajouter les activités'));
}
}
/// Handles voting for an activity
Future<void> _onVoteForActivity(
VoteForActivity event,
Emitter<ActivityState> 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, stackTrace) {
_errorService.logError('activity_bloc', 'Erreur vote: $e', stackTrace);
emit(const ActivityError('Impossible d\'enregistrer le vote'));
}
}
/// Handles deleting an activity
Future<void> _onDeleteActivity(
DeleteActivity event,
Emitter<ActivityState> 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, stackTrace) {
_errorService.logError(
'activity_bloc',
'Erreur suppression: $e',
stackTrace,
);
emit(const ActivityError('Impossible de supprimer l\'activité'));
}
}
/// Handles filtering activities
Future<void> _onFilterActivities(
FilterActivities event,
Emitter<ActivityState> 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<void> _onRefreshActivities(
RefreshActivities event,
Emitter<ActivityState> emit,
) async {
add(LoadActivities(event.tripId));
}
/// Handles clearing search results
Future<void> _onClearSearchResults(
ClearSearchResults event,
Emitter<ActivityState> emit,
) async {
if (state is ActivitySearchResults) {
emit(const ActivityInitial());
}
}
/// Handles updating an activity
Future<void> _onUpdateActivity(
UpdateActivity event,
Emitter<ActivityState> 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, stackTrace) {
_errorService.logError(
'activity_bloc',
'Erreur mise à jour: $e',
stackTrace,
);
emit(const ActivityError('Impossible de mettre à jour l\'activité'));
}
}
/// Handles toggling activity favorite status
Future<void> _onToggleActivityFavorite(
ToggleActivityFavorite event,
Emitter<ActivityState> 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, stackTrace) {
_errorService.logError('activity_bloc', 'Erreur favori: $e', stackTrace);
emit(const ActivityError('Impossible de modifier les favoris'));
}
}
/// Applies filters to the activities list
List<Activity> _applyFilters(
List<Activity> activities,
String? category,
double? minRating,
bool showVotedOnly,
String userId,
) {
var filtered = activities;
if (category != null) {
filtered = filtered.where((a) {
// Check exact match (internal value)
if (a.category == category) return true;
// Check display name match
// Find the enum that matches the filter category (which is a display name)
try {
final categoryEnum = ActivityCategory.values.firstWhere(
(e) => e.displayName == category,
);
// Check if activity category matches the enum's google type or display name
return a.category == categoryEnum.googlePlaceType ||
a.category == categoryEnum.displayName ||
a.category.toLowerCase() == categoryEnum.name.toLowerCase();
} catch (_) {
// If no matching enum found, fallback to simple string comparison
return a.category.toLowerCase() == category.toLowerCase();
}
}).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<void> _onRemoveFromSearchResults(
RemoveFromSearchResults event,
Emitter<ActivityState> 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<void> _onRestoreCachedSearchResults(
RestoreCachedSearchResults event,
Emitter<ActivityState> emit,
) async {
emit(
ActivitySearchResults(
searchResults: event.searchResults,
query: 'cached',
isLoading: false,
),
);
}
}