feat: Add calendar page, enhance activity search and approval logic, and refactor activity filtering UI.

This commit is contained in:
Van Leemput Dayron
2025-11-26 12:15:13 +01:00
parent 258f10b42b
commit f7eeb7c6f1
11 changed files with 952 additions and 700 deletions

View File

@@ -17,13 +17,14 @@ class ActivityBloc extends Bloc<ActivityEvent, ActivityState> {
required ActivityRepository repository,
required ActivityPlacesService placesService,
required ErrorService errorService,
}) : _repository = repository,
_placesService = placesService,
_errorService = errorService,
super(const ActivityInitial()) {
}) : _repository = repository,
_placesService = placesService,
_errorService = errorService,
super(const ActivityInitial()) {
on<LoadActivities>(_onLoadActivities);
on<LoadTripActivitiesPreservingSearch>(_onLoadTripActivitiesPreservingSearch);
on<LoadTripActivitiesPreservingSearch>(
_onLoadTripActivitiesPreservingSearch,
);
on<SearchActivities>(_onSearchActivities);
on<SearchActivitiesWithCoordinates>(_onSearchActivitiesWithCoordinates);
on<SearchActivitiesByText>(_onSearchActivitiesByText);
@@ -39,6 +40,7 @@ class ActivityBloc extends Bloc<ActivityEvent, ActivityState> {
on<RestoreCachedSearchResults>(_onRestoreCachedSearchResults);
on<RemoveFromSearchResults>(_onRemoveFromSearchResults);
on<AddActivityAndRemoveFromSearch>(_onAddActivityAndRemoveFromSearch);
on<UpdateActivityDate>(_onUpdateActivityDate);
}
/// Handles loading activities for a trip
@@ -48,15 +50,17 @@ class ActivityBloc extends Bloc<ActivityEvent, ActivityState> {
) async {
try {
emit(const ActivityLoading());
final activities = await _repository.getActivitiesByTrip(event.tripId);
emit(ActivityLoaded(
activities: activities,
filteredActivities: activities,
));
emit(
ActivityLoaded(activities: activities, filteredActivities: activities),
);
} catch (e) {
_errorService.logError('activity_bloc', 'Erreur chargement activités: $e');
_errorService.logError(
'activity_bloc',
'Erreur chargement activités: $e',
);
emit(const ActivityError('Impossible de charger les activités'));
}
}
@@ -68,24 +72,52 @@ class ActivityBloc extends Bloc<ActivityEvent, ActivityState> {
) 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,
));
emit(
ActivityLoaded(activities: activities, filteredActivities: activities),
);
} catch (e) {
_errorService.logError('activity_bloc', 'Erreur chargement activités: $e');
_errorService.logError(
'activity_bloc',
'Erreur chargement activités: $e',
);
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) {
_errorService.logError('activity_bloc', 'Erreur mise à jour date: $e');
emit(const ActivityError('Impossible de mettre à jour la date'));
}
}
/// Handles searching activities using Google Places API
Future<void> _onSearchActivities(
SearchActivities event,
@@ -99,17 +131,19 @@ class ActivityBloc extends Bloc<ActivityEvent, ActivityState> {
} 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
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;
@@ -117,15 +151,17 @@ class ActivityBloc extends Bloc<ActivityEvent, ActivityState> {
} 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,
));
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'));
@@ -145,7 +181,7 @@ class ActivityBloc extends Bloc<ActivityEvent, ActivityState> {
} else {
emit(const ActivitySearching());
}
final searchResults = await _placesService.searchActivitiesPaginated(
latitude: event.latitude,
longitude: event.longitude,
@@ -153,10 +189,10 @@ class ActivityBloc extends Bloc<ActivityEvent, ActivityState> {
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;
@@ -164,17 +200,22 @@ class ActivityBloc extends Bloc<ActivityEvent, ActivityState> {
} 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,
));
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');
_errorService.logError(
'activity_bloc',
'Erreur recherche activités avec coordonnées: $e',
);
emit(const ActivityError('Impossible de rechercher les activités'));
}
}
@@ -186,20 +227,19 @@ class ActivityBloc extends Bloc<ActivityEvent, ActivityState> {
) 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,
));
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'));
@@ -218,7 +258,7 @@ class ActivityBloc extends Bloc<ActivityEvent, ActivityState> {
event.activity.tripId,
event.activity.placeId!,
);
if (existing != null) {
emit(const ActivityError('Cette activité a déjà été ajoutée'));
return;
@@ -226,20 +266,27 @@ class ActivityBloc extends Bloc<ActivityEvent, ActivityState> {
}
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
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',
));
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 {
@@ -263,7 +310,7 @@ class ActivityBloc extends Bloc<ActivityEvent, ActivityState> {
event.activity.tripId,
event.activity.placeId!,
);
if (existing != null) {
emit(const ActivityError('Cette activité a déjà été ajoutée'));
return;
@@ -271,7 +318,7 @@ class ActivityBloc extends Bloc<ActivityEvent, ActivityState> {
}
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
@@ -280,20 +327,24 @@ class ActivityBloc extends Bloc<ActivityEvent, ActivityState> {
final updatedResults = currentState.searchResults
.where((activity) => activity.id != event.googleActivityId)
.toList();
emit(ActivitySearchResults(
searchResults: updatedResults,
query: currentState.query,
isLoading: false,
));
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',
));
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 {
@@ -313,35 +364,39 @@ class ActivityBloc extends Bloc<ActivityEvent, ActivityState> {
try {
// Filter out existing activities
final filteredActivities = <Activity>[];
emit(ActivityBatchAdding(
activitiesToAdd: event.activities,
progress: 0,
total: event.activities.length,
));
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,
));
emit(
ActivityBatchAdding(
activitiesToAdd: event.activities,
progress: i + 1,
total: event.activities.length,
),
);
}
if (filteredActivities.isEmpty) {
@@ -350,12 +405,14 @@ class ActivityBloc extends Bloc<ActivityEvent, ActivityState> {
}
final addedIds = await _repository.addActivitiesBatch(filteredActivities);
if (addedIds.isNotEmpty) {
emit(ActivityOperationSuccess(
'${addedIds.length} activité(s) ajoutée(s) avec succès',
operationType: 'batch_add',
));
emit(
ActivityOperationSuccess(
'${addedIds.length} activité(s) ajoutée(s) avec succès',
operationType: 'batch_add',
),
);
// Reload activities
add(LoadActivities(event.activities.first.tripId));
} else {
@@ -376,10 +433,12 @@ class ActivityBloc extends Bloc<ActivityEvent, ActivityState> {
// Show voting state
if (state is ActivityLoaded) {
final currentState = state as ActivityLoaded;
emit(ActivityVoting(
activityId: event.activityId,
activities: currentState.activities,
));
emit(
ActivityVoting(
activityId: event.activityId,
activities: currentState.activities,
),
);
}
final success = await _repository.voteForActivity(
@@ -387,31 +446,35 @@ class ActivityBloc extends Bloc<ActivityEvent, ActivityState> {
event.userId,
event.vote,
);
if (success) {
emit(ActivityVoteRecorded(
activityId: event.activityId,
vote: event.vote,
userId: event.userId,
));
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,
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'));
@@ -429,13 +492,15 @@ class ActivityBloc extends Bloc<ActivityEvent, ActivityState> {
) async {
try {
final success = await _repository.deleteActivity(event.activityId);
if (success) {
emit(ActivityDeleted(
activityId: event.activityId,
message: 'Activité supprimée avec succès',
));
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;
@@ -459,7 +524,7 @@ class ActivityBloc extends Bloc<ActivityEvent, ActivityState> {
) async {
if (state is ActivityLoaded) {
final currentState = state as ActivityLoaded;
final filteredActivities = _applyFilters(
currentState.activities,
event.category,
@@ -467,13 +532,15 @@ class ActivityBloc extends Bloc<ActivityEvent, ActivityState> {
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,
));
emit(
currentState.copyWith(
filteredActivities: filteredActivities,
activeFilter: event.category,
minRating: event.minRating,
showVotedOnly: event.showVotedOnly ?? false,
),
);
}
}
@@ -503,20 +570,24 @@ class ActivityBloc extends Bloc<ActivityEvent, ActivityState> {
try {
if (state is ActivityLoaded) {
final currentState = state as ActivityLoaded;
emit(ActivityUpdating(
activityId: event.activity.id,
activities: currentState.activities,
));
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',
));
emit(
const ActivityOperationSuccess(
'Activité mise à jour avec succès',
operationType: 'update',
),
);
// Reload activities
add(LoadActivities(event.activity.tripId));
} else {
@@ -536,11 +607,13 @@ class ActivityBloc extends Bloc<ActivityEvent, ActivityState> {
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,
));
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'));
@@ -558,7 +631,25 @@ class ActivityBloc extends Bloc<ActivityEvent, ActivityState> {
var filtered = activities;
if (category != null) {
filtered = filtered.where((a) => a.category == category).toList();
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) {
@@ -580,18 +671,20 @@ class ActivityBloc extends Bloc<ActivityEvent, ActivityState> {
// 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,
));
emit(
ActivitySearchResults(
searchResults: updatedResults,
query: currentState.query,
isLoading: false,
),
);
}
}
@@ -600,10 +693,12 @@ class ActivityBloc extends Bloc<ActivityEvent, ActivityState> {
RestoreCachedSearchResults event,
Emitter<ActivityState> emit,
) async {
emit(ActivitySearchResults(
searchResults: event.searchResults,
query: 'cached',
isLoading: false,
));
emit(
ActivitySearchResults(
searchResults: event.searchResults,
query: 'cached',
isLoading: false,
),
);
}
}