Refactor ActivityCard UI and improve voting functionality

- Updated ActivityCard layout for better visual consistency and responsiveness.
- Simplified the category badge and adjusted styles for better readability.
- Enhanced the voting section with a progress bar and improved button designs.
- Added a new method in Activity model to check if all trip participants approved an activity.
- Improved error handling and validation in ActivityRepository for voting and fetching activities.
- Implemented pagination in ActivityPlacesService for activity searches.
- Removed outdated scripts for cleaning up duplicate images.
This commit is contained in:
Dayron
2025-11-04 20:21:54 +01:00
parent 8ff9e12fd4
commit f6c8432335
19 changed files with 2902 additions and 961 deletions

View File

@@ -61,17 +61,36 @@ class ActivityBloc extends Bloc<ActivityEvent, ActivityState> {
Emitter<ActivityState> emit,
) async {
try {
emit(const ActivitySearching());
// 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;
}
emit(ActivitySearchResults(
searchResults: searchResults,
searchResults: finalResults,
query: event.category?.displayName ?? 'Toutes les activités',
isLoading: false,
));
} catch (e) {
_errorService.logError('activity_bloc', 'Erreur recherche activités: $e');

View File

@@ -24,15 +24,23 @@ class SearchActivities extends ActivityEvent {
final String tripId;
final String destination;
final ActivityCategory? category;
final int? maxResults;
final int? offset;
final bool reset;
final bool appendToExisting;
const SearchActivities({
required this.tripId,
required this.destination,
this.category,
this.maxResults,
this.offset,
this.reset = false,
this.appendToExisting = false,
});
@override
List<Object?> get props => [tripId, destination, category];
List<Object?> get props => [tripId, destination, category, maxResults, offset, reset, appendToExisting];
}
/// Event to search activities by text query

View File

@@ -0,0 +1,158 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../models/activity.dart';
import '../../../repositories/activity_repository.dart';
import '../../../services/error_service.dart';
import 'approved_activity_event.dart';
import 'approved_activity_state.dart';
/// BLoC pour gérer les activités approuvées par tous les participants
class ApprovedActivityBloc extends Bloc<ApprovedActivityEvent, ApprovedActivityState> {
final ActivityRepository _repository;
final ErrorService _errorService;
ApprovedActivityBloc({
required ActivityRepository repository,
required ErrorService errorService,
}) : _repository = repository,
_errorService = errorService,
super(const ApprovedActivityInitial()) {
on<LoadApprovedActivities>(_onLoadApprovedActivities);
on<SearchApprovedActivities>(_onSearchApprovedActivities);
on<FilterApprovedActivities>(_onFilterApprovedActivities);
on<RefreshApprovedActivities>(_onRefreshApprovedActivities);
on<ClearApprovedSearchResults>(_onClearApprovedSearchResults);
}
/// Charger les activités approuvées par tous les participants
Future<void> _onLoadApprovedActivities(
LoadApprovedActivities event,
Emitter<ApprovedActivityState> emit,
) async {
try {
emit(const ApprovedActivityLoading());
final allActivities = await _repository.getActivitiesByTrip(event.tripId);
// Filtrer les activités qui ont reçu des votes positifs de TOUS les participants
final approvedActivities = allActivities.where((activity) {
// Une activité est approuvée si tous les participants ont voté positivement
final positiveVoters = activity.votes.entries
.where((entry) => entry.value > 0)
.map((entry) => entry.key)
.toSet();
// Vérifier que tous les participants ont voté positivement
return event.tripParticipants.every((participant) =>
positiveVoters.contains(participant));
}).toList();
// Trier par nombre total de votes puis par rating
approvedActivities.sort((a, b) {
final aVotes = a.totalVotes;
final bVotes = b.totalVotes;
if (aVotes != bVotes) {
return bVotes.compareTo(aVotes);
}
return (b.rating ?? 0).compareTo(a.rating ?? 0);
});
emit(ApprovedActivityLoaded(
approvedActivities: approvedActivities,
tripParticipants: event.tripParticipants,
));
} catch (e) {
_errorService.logError('approved_activity_bloc', 'Erreur chargement activités approuvées: $e');
emit(const ApprovedActivityError('Impossible de charger les activités approuvées'));
}
}
/// Rechercher dans les activités approuvées
Future<void> _onSearchApprovedActivities(
SearchApprovedActivities event,
Emitter<ApprovedActivityState> emit,
) async {
try {
emit(const ApprovedActivitySearching());
// Charger d'abord toutes les activités approuvées
final allActivities = await _repository.getActivitiesByTrip(event.tripId);
// Filtrer les approuvées puis rechercher
final approvedActivities = allActivities.where((activity) {
final positiveVoters = activity.votes.entries
.where((entry) => entry.value > 0)
.map((entry) => entry.key)
.toSet();
return event.tripParticipants.every((participant) =>
positiveVoters.contains(participant));
}).toList();
// Rechercher dans les activités approuvées
final results = approvedActivities
.where((activity) =>
activity.name.toLowerCase().contains(event.query.toLowerCase()) ||
activity.description.toLowerCase().contains(event.query.toLowerCase()) ||
activity.category.toLowerCase().contains(event.query.toLowerCase()))
.toList();
emit(ApprovedActivitySearchResults(
results: results,
query: event.query,
tripParticipants: event.tripParticipants,
));
} catch (e) {
_errorService.logError('approved_activity_bloc', 'Erreur recherche approuvées: $e');
emit(const ApprovedActivityError('Erreur lors de la recherche'));
}
}
/// Filtrer les activités approuvées
Future<void> _onFilterApprovedActivities(
FilterApprovedActivities event,
Emitter<ApprovedActivityState> emit,
) async {
if (state is! ApprovedActivityLoaded) return;
final currentState = state as ApprovedActivityLoaded;
List<Activity> filteredActivities = List.from(currentState.approvedActivities);
// Filtrer par catégorie
if (event.category != null && event.category!.isNotEmpty) {
filteredActivities = filteredActivities
.where((activity) => activity.category == event.category)
.toList();
}
// Filtrer par rating minimum
if (event.minRating != null) {
filteredActivities = filteredActivities
.where((activity) => (activity.rating ?? 0) >= event.minRating!)
.toList();
}
emit(currentState.copyWith(approvedActivities: filteredActivities));
}
/// Rafraîchir les activités approuvées
Future<void> _onRefreshApprovedActivities(
RefreshApprovedActivities event,
Emitter<ApprovedActivityState> emit,
) async {
add(LoadApprovedActivities(
tripId: event.tripId,
tripParticipants: event.tripParticipants,
));
}
/// Effacer les résultats de recherche
Future<void> _onClearApprovedSearchResults(
ClearApprovedSearchResults event,
Emitter<ApprovedActivityState> emit,
) async {
if (state is ApprovedActivitySearchResults) {
emit(const ApprovedActivityInitial());
}
}
}

View File

@@ -0,0 +1,72 @@
import 'package:equatable/equatable.dart';
/// Events pour les activités approuvées par tous les participants
abstract class ApprovedActivityEvent extends Equatable {
const ApprovedActivityEvent();
@override
List<Object?> get props => [];
}
/// Charger les activités approuvées par tous
class LoadApprovedActivities extends ApprovedActivityEvent {
final String tripId;
final List<String> tripParticipants;
const LoadApprovedActivities({
required this.tripId,
required this.tripParticipants,
});
@override
List<Object?> get props => [tripId, tripParticipants];
}
/// Rechercher dans les activités approuvées
class SearchApprovedActivities extends ApprovedActivityEvent {
final String tripId;
final String query;
final List<String> tripParticipants;
const SearchApprovedActivities({
required this.tripId,
required this.query,
required this.tripParticipants,
});
@override
List<Object?> get props => [tripId, query, tripParticipants];
}
/// Filtrer les activités approuvées
class FilterApprovedActivities extends ApprovedActivityEvent {
final String? category;
final double? minRating;
const FilterApprovedActivities({
this.category,
this.minRating,
});
@override
List<Object?> get props => [category, minRating];
}
/// Rafraîchir les activités approuvées
class RefreshApprovedActivities extends ApprovedActivityEvent {
final String tripId;
final List<String> tripParticipants;
const RefreshApprovedActivities({
required this.tripId,
required this.tripParticipants,
});
@override
List<Object?> get props => [tripId, tripParticipants];
}
/// Effacer les résultats de recherche approuvées
class ClearApprovedSearchResults extends ApprovedActivityEvent {
const ClearApprovedSearchResults();
}

View File

@@ -0,0 +1,76 @@
import 'package:equatable/equatable.dart';
import '../../../models/activity.dart';
/// States pour les activités approuvées par tous les participants
abstract class ApprovedActivityState extends Equatable {
const ApprovedActivityState();
@override
List<Object?> get props => [];
}
/// État initial
class ApprovedActivityInitial extends ApprovedActivityState {
const ApprovedActivityInitial();
}
/// État de chargement
class ApprovedActivityLoading extends ApprovedActivityState {
const ApprovedActivityLoading();
}
/// État de recherche
class ApprovedActivitySearching extends ApprovedActivityState {
const ApprovedActivitySearching();
}
/// État avec les activités approuvées chargées
class ApprovedActivityLoaded extends ApprovedActivityState {
final List<Activity> approvedActivities;
final List<String> tripParticipants;
const ApprovedActivityLoaded({
required this.approvedActivities,
required this.tripParticipants,
});
@override
List<Object?> get props => [approvedActivities, tripParticipants];
/// Créer une copie avec des modifications
ApprovedActivityLoaded copyWith({
List<Activity>? approvedActivities,
List<String>? tripParticipants,
}) {
return ApprovedActivityLoaded(
approvedActivities: approvedActivities ?? this.approvedActivities,
tripParticipants: tripParticipants ?? this.tripParticipants,
);
}
}
/// État avec résultats de recherche approuvées
class ApprovedActivitySearchResults extends ApprovedActivityState {
final List<Activity> results;
final String query;
final List<String> tripParticipants;
const ApprovedActivitySearchResults({
required this.results,
required this.query,
required this.tripParticipants,
});
@override
List<Object?> get props => [results, query, tripParticipants];
}
/// État d'erreur
class ApprovedActivityError extends ApprovedActivityState {
final String message;
const ApprovedActivityError(this.message);
@override
List<Object?> get props => [message];
}

View File

@@ -0,0 +1,267 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../models/activity.dart';
import '../../../repositories/activity_repository.dart';
import '../../../services/error_service.dart';
import 'db_activity_event.dart';
import 'db_activity_state.dart';
/// BLoC pour gérer les activités de la base de données
class DbActivityBloc extends Bloc<DbActivityEvent, DbActivityState> {
final ActivityRepository _repository;
final ErrorService _errorService;
DbActivityBloc({
required ActivityRepository repository,
required ErrorService errorService,
}) : _repository = repository,
_errorService = errorService,
super(const DbActivityInitial()) {
on<LoadDbActivities>(_onLoadDbActivities);
on<SearchDbActivities>(_onSearchDbActivities);
on<AddDbActivity>(_onAddDbActivity);
on<AddDbActivitiesBatch>(_onAddDbActivitiesBatch);
on<VoteForDbActivity>(_onVoteForDbActivity);
on<DeleteDbActivity>(_onDeleteDbActivity);
on<FilterDbActivities>(_onFilterDbActivities);
on<RefreshDbActivities>(_onRefreshDbActivities);
on<UpdateDbActivity>(_onUpdateDbActivity);
on<ToggleDbActivityFavorite>(_onToggleDbActivityFavorite);
on<ClearDbSearchResults>(_onClearDbSearchResults);
}
/// Charger les activités d'un voyage
Future<void> _onLoadDbActivities(
LoadDbActivities event,
Emitter<DbActivityState> emit,
) async {
try {
emit(const DbActivityLoading());
final activities = await _repository.getActivitiesByTrip(event.tripId);
emit(DbActivityLoaded(
activities: activities,
tripParticipants: event.tripParticipants,
));
} catch (e) {
_errorService.logError('db_activity_bloc', 'Erreur chargement activités DB: $e');
emit(const DbActivityError('Impossible de charger les activités du voyage'));
}
}
/// Rechercher des activités
Future<void> _onSearchDbActivities(
SearchDbActivities event,
Emitter<DbActivityState> emit,
) async {
try {
emit(const DbActivitySearching());
// Pour l'instant, on fait une recherche simple en filtrant les activités existantes
final allActivities = await _repository.getActivitiesByTrip(event.tripId);
final results = allActivities
.where((activity) =>
activity.name.toLowerCase().contains(event.query.toLowerCase()) ||
activity.description.toLowerCase().contains(event.query.toLowerCase()))
.toList();
emit(DbActivitySearchResults(
results: results,
query: event.query,
tripParticipants: event.tripParticipants,
));
} catch (e) {
_errorService.logError('db_activity_bloc', 'Erreur recherche activités: $e');
emit(const DbActivityError('Erreur lors de la recherche'));
}
}
/// Ajouter une activité
Future<void> _onAddDbActivity(
AddDbActivity event,
Emitter<DbActivityState> emit,
) async {
try {
await _repository.addActivity(event.activity);
// Recharger les activités si on est dans l'état chargé
if (state is DbActivityLoaded) {
final currentState = state as DbActivityLoaded;
add(LoadDbActivities(
tripId: event.activity.tripId,
tripParticipants: currentState.tripParticipants,
));
}
emit(const DbActivityOperationSuccess('Activité ajoutée avec succès'));
} catch (e) {
_errorService.logError('db_activity_bloc', 'Erreur ajout activité: $e');
emit(const DbActivityError('Impossible d\'ajouter l\'activité'));
}
}
/// Ajouter plusieurs activités
Future<void> _onAddDbActivitiesBatch(
AddDbActivitiesBatch event,
Emitter<DbActivityState> emit,
) async {
try {
await _repository.addActivitiesBatch(event.activities);
// Recharger les activités si on est dans l'état chargé
if (state is DbActivityLoaded && event.activities.isNotEmpty) {
final currentState = state as DbActivityLoaded;
add(LoadDbActivities(
tripId: event.activities.first.tripId,
tripParticipants: currentState.tripParticipants,
));
}
emit(DbActivityOperationSuccess('${event.activities.length} activités ajoutées'));
} catch (e) {
_errorService.logError('db_activity_bloc', 'Erreur ajout batch: $e');
emit(const DbActivityError('Impossible d\'ajouter les activités'));
}
}
/// Voter pour une activité
Future<void> _onVoteForDbActivity(
VoteForDbActivity event,
Emitter<DbActivityState> emit,
) async {
try {
await _repository.voteForActivity(
event.activityId,
event.userId,
event.isUpvote ? 1 : -1,
);
// Recharger les activités pour refléter le nouveau vote
if (state is DbActivityLoaded) {
final currentState = state as DbActivityLoaded;
final activity = currentState.activities.firstWhere(
(a) => a.id == event.activityId,
);
add(LoadDbActivities(
tripId: activity.tripId,
tripParticipants: currentState.tripParticipants,
));
}
emit(const DbActivityOperationSuccess('Vote enregistré'));
} catch (e) {
_errorService.logError('db_activity_bloc', 'Erreur vote: $e');
emit(const DbActivityError('Impossible d\'enregistrer le vote'));
}
}
/// Supprimer une activité
Future<void> _onDeleteDbActivity(
DeleteDbActivity event,
Emitter<DbActivityState> emit,
) async {
try {
await _repository.deleteActivity(event.activityId);
// Mettre à jour la liste locale si on est dans l'état chargé
if (state is DbActivityLoaded) {
final currentState = state as DbActivityLoaded;
final updatedActivities = currentState.activities
.where((activity) => activity.id != event.activityId)
.toList();
emit(currentState.copyWith(activities: updatedActivities));
}
emit(const DbActivityOperationSuccess('Activité supprimée'));
} catch (e) {
_errorService.logError('db_activity_bloc', 'Erreur suppression: $e');
emit(const DbActivityError('Impossible de supprimer l\'activité'));
}
}
/// Filtrer les activités
Future<void> _onFilterDbActivities(
FilterDbActivities event,
Emitter<DbActivityState> emit,
) async {
if (state is! DbActivityLoaded) return;
final currentState = state as DbActivityLoaded;
List<Activity> filteredActivities = List.from(currentState.activities);
// Filtrer par catégorie
if (event.category != null && event.category!.isNotEmpty) {
filteredActivities = filteredActivities
.where((activity) => activity.category == event.category)
.toList();
}
// Note: isFavorite n'existe pas dans le modèle Activity actuel
// if (event.showFavorites == true) {
// filteredActivities = filteredActivities
// .where((activity) => activity.isFavorite == true)
// .toList();
// }
emit(currentState.copyWith(activities: filteredActivities));
}
/// Rafraîchir les activités
Future<void> _onRefreshDbActivities(
RefreshDbActivities event,
Emitter<DbActivityState> emit,
) async {
add(LoadDbActivities(
tripId: event.tripId,
tripParticipants: event.tripParticipants,
));
}
/// Mettre à jour une activité
Future<void> _onUpdateDbActivity(
UpdateDbActivity event,
Emitter<DbActivityState> emit,
) async {
try {
await _repository.updateActivity(event.activity);
// Mettre à jour la liste locale
if (state is DbActivityLoaded) {
final currentState = state as DbActivityLoaded;
final updatedActivities = currentState.activities.map((activity) {
return activity.id == event.activity.id ? event.activity : activity;
}).toList();
emit(currentState.copyWith(activities: updatedActivities));
}
emit(const DbActivityOperationSuccess('Activité mise à jour'));
} catch (e) {
_errorService.logError('db_activity_bloc', 'Erreur mise à jour: $e');
emit(const DbActivityError('Impossible de mettre à jour l\'activité'));
}
}
/// Basculer le favori d'une activité
Future<void> _onToggleDbActivityFavorite(
ToggleDbActivityFavorite event,
Emitter<DbActivityState> emit,
) async {
// Note: La fonctionnalité favori n'existe pas encore dans le repository
// Cette méthode peut être implémentée plus tard
emit(const DbActivityError('Fonctionnalité favori non disponible'));
}
/// Effacer les résultats de recherche
Future<void> _onClearDbSearchResults(
ClearDbSearchResults event,
Emitter<DbActivityState> emit,
) async {
if (state is DbActivitySearchResults) {
// Retourner à l'état précédent ou initial
emit(const DbActivityInitial());
}
}
}

View File

@@ -0,0 +1,143 @@
import 'package:equatable/equatable.dart';
import '../../../models/activity.dart';
/// Events pour les activités de la base de données
abstract class DbActivityEvent extends Equatable {
const DbActivityEvent();
@override
List<Object?> get props => [];
}
/// Charger les activités d'un voyage depuis la DB
class LoadDbActivities extends DbActivityEvent {
final String tripId;
final List<String> tripParticipants;
const LoadDbActivities({
required this.tripId,
required this.tripParticipants,
});
@override
List<Object?> get props => [tripId, tripParticipants];
}
/// Rechercher des activités dans la DB
class SearchDbActivities extends DbActivityEvent {
final String tripId;
final String query;
final List<String> tripParticipants;
const SearchDbActivities({
required this.tripId,
required this.query,
required this.tripParticipants,
});
@override
List<Object?> get props => [tripId, query, tripParticipants];
}
/// Ajouter une activité à la DB
class AddDbActivity extends DbActivityEvent {
final Activity activity;
const AddDbActivity({required this.activity});
@override
List<Object?> get props => [activity];
}
/// Ajouter plusieurs activités à la DB
class AddDbActivitiesBatch extends DbActivityEvent {
final List<Activity> activities;
const AddDbActivitiesBatch({required this.activities});
@override
List<Object?> get props => [activities];
}
/// Voter pour une activité
class VoteForDbActivity extends DbActivityEvent {
final String activityId;
final String userId;
final bool isUpvote;
const VoteForDbActivity({
required this.activityId,
required this.userId,
required this.isUpvote,
});
@override
List<Object?> get props => [activityId, userId, isUpvote];
}
/// Supprimer une activité
class DeleteDbActivity extends DbActivityEvent {
final String activityId;
const DeleteDbActivity({required this.activityId});
@override
List<Object?> get props => [activityId];
}
/// Filtrer les activités
class FilterDbActivities extends DbActivityEvent {
final String? category;
final bool? showFavorites;
const FilterDbActivities({
this.category,
this.showFavorites,
});
@override
List<Object?> get props => [category, showFavorites];
}
/// Rafraîchir les activités
class RefreshDbActivities extends DbActivityEvent {
final String tripId;
final List<String> tripParticipants;
const RefreshDbActivities({
required this.tripId,
required this.tripParticipants,
});
@override
List<Object?> get props => [tripId, tripParticipants];
}
/// Mettre à jour une activité
class UpdateDbActivity extends DbActivityEvent {
final Activity activity;
const UpdateDbActivity({required this.activity});
@override
List<Object?> get props => [activity];
}
/// Basculer le favori d'une activité
class ToggleDbActivityFavorite extends DbActivityEvent {
final String activityId;
final String userId;
const ToggleDbActivityFavorite({
required this.activityId,
required this.userId,
});
@override
List<Object?> get props => [activityId, userId];
}
/// Effacer les résultats de recherche
class ClearDbSearchResults extends DbActivityEvent {
const ClearDbSearchResults();
}

View File

@@ -0,0 +1,86 @@
import 'package:equatable/equatable.dart';
import '../../../models/activity.dart';
/// States pour les activités de la base de données
abstract class DbActivityState extends Equatable {
const DbActivityState();
@override
List<Object?> get props => [];
}
/// État initial
class DbActivityInitial extends DbActivityState {
const DbActivityInitial();
}
/// État de chargement
class DbActivityLoading extends DbActivityState {
const DbActivityLoading();
}
/// État de recherche
class DbActivitySearching extends DbActivityState {
const DbActivitySearching();
}
/// État avec les activités chargées
class DbActivityLoaded extends DbActivityState {
final List<Activity> activities;
final List<String> tripParticipants;
const DbActivityLoaded({
required this.activities,
required this.tripParticipants,
});
@override
List<Object?> get props => [activities, tripParticipants];
/// Créer une copie avec des modifications
DbActivityLoaded copyWith({
List<Activity>? activities,
List<String>? tripParticipants,
}) {
return DbActivityLoaded(
activities: activities ?? this.activities,
tripParticipants: tripParticipants ?? this.tripParticipants,
);
}
}
/// État avec résultats de recherche
class DbActivitySearchResults extends DbActivityState {
final List<Activity> results;
final String query;
final List<String> tripParticipants;
const DbActivitySearchResults({
required this.results,
required this.query,
required this.tripParticipants,
});
@override
List<Object?> get props => [results, query, tripParticipants];
}
/// État de succès d'opération
class DbActivityOperationSuccess extends DbActivityState {
final String message;
const DbActivityOperationSuccess(this.message);
@override
List<Object?> get props => [message];
}
/// État d'erreur
class DbActivityError extends DbActivityState {
final String message;
const DbActivityError(this.message);
@override
List<Object?> get props => [message];
}

View File

@@ -0,0 +1,157 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../models/activity.dart';
import '../../../repositories/activity_repository.dart';
import '../../../services/activity_places_service.dart';
import '../../../services/error_service.dart';
import 'google_activity_event.dart';
import 'google_activity_state.dart';
/// BLoC pour gérer les activités Google Places
class GoogleActivityBloc extends Bloc<GoogleActivityEvent, GoogleActivityState> {
final ActivityPlacesService _placesService;
final ActivityRepository _repository;
final ErrorService _errorService;
GoogleActivityBloc({
required ActivityPlacesService placesService,
required ActivityRepository repository,
required ErrorService errorService,
}) : _placesService = placesService,
_repository = repository,
_errorService = errorService,
super(const GoogleActivityInitial()) {
on<LoadGoogleActivities>(_onLoadGoogleActivities);
on<LoadMoreGoogleActivities>(_onLoadMoreGoogleActivities);
on<UpdateGoogleActivities>(_onUpdateGoogleActivities);
on<AddGoogleActivityToDb>(_onAddGoogleActivityToDb);
on<SearchGoogleActivitiesByCategory>(_onSearchGoogleActivitiesByCategory);
on<ClearGoogleActivities>(_onClearGoogleActivities);
}
/// Charger les activités Google Places avec pagination (6 par page)
Future<void> _onLoadGoogleActivities(
LoadGoogleActivities event,
Emitter<GoogleActivityState> emit,
) async {
try {
emit(const GoogleActivitySearching());
final result = await _placesService.searchActivitiesPaginated(
destination: event.destination,
tripId: event.tripId,
category: event.category,
pageSize: 6,
);
emit(GoogleActivityLoaded(
googleActivities: result['activities'] as List<Activity>,
nextPageToken: result['nextPageToken'] as String?,
hasMoreData: result['hasMoreData'] as bool? ?? false,
query: event.category?.displayName ?? 'Toutes les activités',
));
} catch (e) {
_errorService.logError('google_activity_bloc', 'Erreur chargement activités Google: $e');
emit(const GoogleActivityError('Impossible de charger les activités Google'));
}
}
/// Charger plus d'activités (pagination)
Future<void> _onLoadMoreGoogleActivities(
LoadMoreGoogleActivities event,
Emitter<GoogleActivityState> emit,
) async {
if (state is! GoogleActivityLoaded) return;
final currentState = state as GoogleActivityLoaded;
if (!currentState.hasMoreData || currentState.nextPageToken == null) return;
try {
emit(GoogleActivityLoadingMore(
currentActivities: currentState.googleActivities,
query: currentState.query,
));
final result = await _placesService.searchActivitiesPaginated(
destination: event.destination,
tripId: event.tripId,
category: event.category,
pageSize: 6,
nextPageToken: currentState.nextPageToken,
);
final newActivities = result['activities'] as List<Activity>;
final allActivities = [...currentState.googleActivities, ...newActivities];
emit(GoogleActivityLoaded(
googleActivities: allActivities,
nextPageToken: result['nextPageToken'] as String?,
hasMoreData: result['hasMoreData'] as bool? ?? false,
query: currentState.query,
));
} catch (e) {
_errorService.logError('google_activity_bloc', 'Erreur chargement plus activités: $e');
emit(const GoogleActivityError('Impossible de charger plus d\'activités'));
}
}
/// Mettre à jour les activités Google
Future<void> _onUpdateGoogleActivities(
UpdateGoogleActivities event,
Emitter<GoogleActivityState> emit,
) async {
emit(GoogleActivityLoaded(
googleActivities: event.activities,
nextPageToken: event.nextPageToken,
hasMoreData: event.hasMoreData,
query: event.query,
));
}
/// Ajouter une activité Google à la base de données
Future<void> _onAddGoogleActivityToDb(
AddGoogleActivityToDb event,
Emitter<GoogleActivityState> emit,
) async {
try {
// Vérifier si l'activité existe déjà
if (event.activity.placeId != null) {
final existingActivity = await _repository.findExistingActivity(
event.activity.tripId,
event.activity.placeId!,
);
if (existingActivity != null) {
emit(const GoogleActivityOperationSuccess('Cette activité est déjà dans votre voyage'));
return;
}
}
await _repository.addActivity(event.activity);
emit(const GoogleActivityOperationSuccess('Activité ajoutée au voyage'));
} catch (e) {
_errorService.logError('google_activity_bloc', 'Erreur ajout activité: $e');
emit(const GoogleActivityError('Impossible d\'ajouter l\'activité'));
}
}
/// Rechercher par catégorie
Future<void> _onSearchGoogleActivitiesByCategory(
SearchGoogleActivitiesByCategory event,
Emitter<GoogleActivityState> emit,
) async {
add(LoadGoogleActivities(
tripId: event.tripId,
destination: event.destination,
category: event.category,
));
}
/// Effacer les activités Google
Future<void> _onClearGoogleActivities(
ClearGoogleActivities event,
Emitter<GoogleActivityState> emit,
) async {
emit(const GoogleActivityInitial());
}
}

View File

@@ -0,0 +1,93 @@
import 'package:equatable/equatable.dart';
import '../../../models/activity.dart';
/// Events pour les activités Google Places
abstract class GoogleActivityEvent extends Equatable {
const GoogleActivityEvent();
@override
List<Object?> get props => [];
}
/// Charger les activités Google Places
class LoadGoogleActivities extends GoogleActivityEvent {
final String tripId;
final String destination;
final ActivityCategory? category;
const LoadGoogleActivities({
required this.tripId,
required this.destination,
this.category,
});
@override
List<Object?> get props => [tripId, destination, category];
}
/// Charger plus d'activités Google (pagination)
class LoadMoreGoogleActivities extends GoogleActivityEvent {
final String tripId;
final String destination;
final ActivityCategory? category;
final String? nextPageToken;
const LoadMoreGoogleActivities({
required this.tripId,
required this.destination,
this.category,
this.nextPageToken,
});
@override
List<Object?> get props => [tripId, destination, category, nextPageToken];
}
/// Mettre à jour les activités Google
class UpdateGoogleActivities extends GoogleActivityEvent {
final List<Activity> activities;
final String? nextPageToken;
final bool hasMoreData;
final String query;
const UpdateGoogleActivities({
required this.activities,
this.nextPageToken,
required this.hasMoreData,
required this.query,
});
@override
List<Object?> get props => [activities, nextPageToken, hasMoreData, query];
}
/// Ajouter une activité Google à la DB
class AddGoogleActivityToDb extends GoogleActivityEvent {
final Activity activity;
const AddGoogleActivityToDb({required this.activity});
@override
List<Object?> get props => [activity];
}
/// Rechercher des activités Google par catégorie
class SearchGoogleActivitiesByCategory extends GoogleActivityEvent {
final String tripId;
final String destination;
final ActivityCategory category;
const SearchGoogleActivitiesByCategory({
required this.tripId,
required this.destination,
required this.category,
});
@override
List<Object?> get props => [tripId, destination, category];
}
/// Effacer les résultats Google
class ClearGoogleActivities extends GoogleActivityEvent {
const ClearGoogleActivities();
}

View File

@@ -0,0 +1,87 @@
import 'package:equatable/equatable.dart';
import '../../../models/activity.dart';
/// States pour les activités Google Places
abstract class GoogleActivityState extends Equatable {
const GoogleActivityState();
@override
List<Object?> get props => [];
}
/// État initial
class GoogleActivityInitial extends GoogleActivityState {
const GoogleActivityInitial();
}
/// État de recherche
class GoogleActivitySearching extends GoogleActivityState {
const GoogleActivitySearching();
}
/// État avec les activités Google chargées
class GoogleActivityLoaded extends GoogleActivityState {
final List<Activity> googleActivities;
final String? nextPageToken;
final bool hasMoreData;
final String query;
const GoogleActivityLoaded({
required this.googleActivities,
this.nextPageToken,
required this.hasMoreData,
required this.query,
});
@override
List<Object?> get props => [googleActivities, nextPageToken, hasMoreData, query];
/// Créer une copie avec des modifications
GoogleActivityLoaded copyWith({
List<Activity>? googleActivities,
String? nextPageToken,
bool? hasMoreData,
String? query,
}) {
return GoogleActivityLoaded(
googleActivities: googleActivities ?? this.googleActivities,
nextPageToken: nextPageToken ?? this.nextPageToken,
hasMoreData: hasMoreData ?? this.hasMoreData,
query: query ?? this.query,
);
}
}
/// État de chargement de plus d'activités (pagination)
class GoogleActivityLoadingMore extends GoogleActivityState {
final List<Activity> currentActivities;
final String query;
const GoogleActivityLoadingMore({
required this.currentActivities,
required this.query,
});
@override
List<Object?> get props => [currentActivities, query];
}
/// État de succès d'opération
class GoogleActivityOperationSuccess extends GoogleActivityState {
final String message;
const GoogleActivityOperationSuccess(this.message);
@override
List<Object?> get props => [message];
}
/// État d'erreur
class GoogleActivityError extends GoogleActivityState {
final String message;
const GoogleActivityError(this.message);
@override
List<Object?> get props => [message];
}