diff --git a/lib/blocs/activity/activity_bloc.dart b/lib/blocs/activity/activity_bloc.dart index 6308075..494574c 100644 --- a/lib/blocs/activity/activity_bloc.dart +++ b/lib/blocs/activity/activity_bloc.dart @@ -23,6 +23,7 @@ class ActivityBloc extends Bloc { on(_onLoadActivities); on(_onSearchActivities); + on(_onSearchActivitiesWithCoordinates); on(_onSearchActivitiesByText); on(_onAddActivity); on(_onAddActivitiesBatch); @@ -98,6 +99,50 @@ class ActivityBloc extends Bloc { } } + /// 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; + } + + 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, diff --git a/lib/blocs/activity/activity_event.dart b/lib/blocs/activity/activity_event.dart index e9dbf41..858da9e 100644 --- a/lib/blocs/activity/activity_event.dart +++ b/lib/blocs/activity/activity_event.dart @@ -43,6 +43,32 @@ class SearchActivities extends ActivityEvent { List get props => [tripId, destination, category, maxResults, offset, reset, appendToExisting]; } +/// Event to search activities using coordinates directly (bypasses geocoding) +class SearchActivitiesWithCoordinates extends ActivityEvent { + final String tripId; + final double latitude; + final double longitude; + final ActivityCategory? category; + final int? maxResults; + final int? offset; + final bool reset; + final bool appendToExisting; + + const SearchActivitiesWithCoordinates({ + required this.tripId, + required this.latitude, + required this.longitude, + this.category, + this.maxResults, + this.offset, + this.reset = false, + this.appendToExisting = false, + }); + + @override + List get props => [tripId, latitude, longitude, category, maxResults, offset, reset, appendToExisting]; +} + /// Event to search activities by text query class SearchActivitiesByText extends ActivityEvent { final String tripId; diff --git a/lib/blocs/activity/google_activities/google_activity_bloc.dart b/lib/blocs/activity/google_activities/google_activity_bloc.dart index 249817a..35ca248 100644 --- a/lib/blocs/activity/google_activities/google_activity_bloc.dart +++ b/lib/blocs/activity/google_activities/google_activity_bloc.dart @@ -39,6 +39,8 @@ class GoogleActivityBloc extends Bloc final result = await _placesService.searchActivitiesPaginated( destination: event.destination, + latitude: event.latitude, + longitude: event.longitude, tripId: event.tripId, category: event.category, pageSize: 6, @@ -74,6 +76,8 @@ class GoogleActivityBloc extends Bloc final result = await _placesService.searchActivitiesPaginated( destination: event.destination, + latitude: event.latitude, + longitude: event.longitude, tripId: event.tripId, category: event.category, pageSize: 6, @@ -143,6 +147,8 @@ class GoogleActivityBloc extends Bloc add(LoadGoogleActivities( tripId: event.tripId, destination: event.destination, + latitude: event.latitude, + longitude: event.longitude, category: event.category, )); } diff --git a/lib/blocs/activity/google_activities/google_activity_event.dart b/lib/blocs/activity/google_activities/google_activity_event.dart index f1bc44f..55f7aa4 100644 --- a/lib/blocs/activity/google_activities/google_activity_event.dart +++ b/lib/blocs/activity/google_activities/google_activity_event.dart @@ -12,35 +12,43 @@ abstract class GoogleActivityEvent extends Equatable { /// Charger les activités Google Places class LoadGoogleActivities extends GoogleActivityEvent { final String tripId; - final String destination; + final String? destination; + final double? latitude; + final double? longitude; final ActivityCategory? category; const LoadGoogleActivities({ required this.tripId, - required this.destination, + this.destination, + this.latitude, + this.longitude, this.category, }); @override - List get props => [tripId, destination, category]; + List get props => [tripId, destination, latitude, longitude, category]; } /// Charger plus d'activités Google (pagination) class LoadMoreGoogleActivities extends GoogleActivityEvent { final String tripId; - final String destination; + final String? destination; + final double? latitude; + final double? longitude; final ActivityCategory? category; final String? nextPageToken; const LoadMoreGoogleActivities({ required this.tripId, - required this.destination, + this.destination, + this.latitude, + this.longitude, this.category, this.nextPageToken, }); @override - List get props => [tripId, destination, category, nextPageToken]; + List get props => [tripId, destination, latitude, longitude, category, nextPageToken]; } /// Mettre à jour les activités Google @@ -74,17 +82,21 @@ class AddGoogleActivityToDb extends GoogleActivityEvent { /// Rechercher des activités Google par catégorie class SearchGoogleActivitiesByCategory extends GoogleActivityEvent { final String tripId; - final String destination; + final String? destination; + final double? latitude; + final double? longitude; final ActivityCategory category; const SearchGoogleActivitiesByCategory({ required this.tripId, - required this.destination, + this.destination, + this.latitude, + this.longitude, required this.category, }); @override - List get props => [tripId, destination, category]; + List get props => [tripId, destination, latitude, longitude, category]; } /// Effacer les résultats Google diff --git a/lib/components/activities/activities_page.dart b/lib/components/activities/activities_page.dart index 9edab65..e00c213 100644 --- a/lib/components/activities/activities_page.dart +++ b/lib/components/activities/activities_page.dart @@ -1282,13 +1282,28 @@ class _ActivitiesPageState extends State _totalGoogleActivitiesRequested = 6; // Reset du compteur _autoReloadInProgress = false; // Reset des protections _lastAutoReloadTriggerCount = 0; - context.read().add(SearchActivities( - tripId: widget.trip.id!, - destination: widget.trip.location, - category: null, // Rechercher dans toutes les catégories - maxResults: 6, // Charger 6 résultats à la fois - reset: true, // Nouveau flag pour reset - )); + + // Utiliser les coordonnées pré-géolocalisées du voyage si disponibles + if (widget.trip.hasCoordinates) { + print('🌍 [Google Search] Using pre-geocoded coordinates: ${widget.trip.latitude}, ${widget.trip.longitude}'); + context.read().add(SearchActivitiesWithCoordinates( + tripId: widget.trip.id!, + latitude: widget.trip.latitude!, + longitude: widget.trip.longitude!, + category: null, // Rechercher dans toutes les catégories + maxResults: 6, // Charger 6 résultats à la fois + reset: true, // Nouveau flag pour reset + )); + } else { + print('⚠️ [Google Search] No coordinates available, falling back to destination geocoding'); + context.read().add(SearchActivities( + tripId: widget.trip.id!, + destination: widget.trip.location, + category: null, // Rechercher dans toutes les catégories + maxResults: 6, // Charger 6 résultats à la fois + reset: true, // Nouveau flag pour reset + )); + } _googleSearchPerformed = true; } @@ -1297,13 +1312,28 @@ class _ActivitiesPageState extends State _totalGoogleActivitiesRequested = 6; // Reset du compteur _autoReloadInProgress = false; // Reset des protections _lastAutoReloadTriggerCount = 0; - context.read().add(SearchActivities( - tripId: widget.trip.id!, - destination: widget.trip.location, - category: null, - maxResults: 6, - reset: true, - )); + + // Utiliser les coordonnées pré-géolocalisées du voyage si disponibles + if (widget.trip.hasCoordinates) { + print('🌍 [Google Search] Using pre-geocoded coordinates: ${widget.trip.latitude}, ${widget.trip.longitude}'); + context.read().add(SearchActivitiesWithCoordinates( + tripId: widget.trip.id!, + latitude: widget.trip.latitude!, + longitude: widget.trip.longitude!, + category: null, + maxResults: 6, + reset: true, + )); + } else { + print('⚠️ [Google Search] No coordinates available, falling back to destination geocoding'); + context.read().add(SearchActivities( + tripId: widget.trip.id!, + destination: widget.trip.location, + category: null, + maxResults: 6, + reset: true, + )); + } _googleSearchPerformed = true; } @@ -1317,13 +1347,28 @@ class _ActivitiesPageState extends State print('📊 [Google Search] Current results count: $currentCount, requesting total: $newTotal'); _totalGoogleActivitiesRequested = newTotal; - context.read().add(SearchActivities( - tripId: widget.trip.id!, - destination: widget.trip.location, - category: null, - maxResults: newTotal, // Demander le total cumulé - reset: true, // Reset pour avoir tous les résultats d'un coup - )); + + // Utiliser les coordonnées pré-géolocalisées du voyage si disponibles + if (widget.trip.hasCoordinates) { + print('🌍 [Google Search] Using pre-geocoded coordinates for more results'); + context.read().add(SearchActivitiesWithCoordinates( + tripId: widget.trip.id!, + latitude: widget.trip.latitude!, + longitude: widget.trip.longitude!, + category: null, + maxResults: newTotal, // Demander le total cumulé + reset: true, // Reset pour avoir tous les résultats d'un coup + )); + } else { + print('⚠️ [Google Search] No coordinates available, falling back to destination geocoding'); + context.read().add(SearchActivities( + tripId: widget.trip.id!, + destination: widget.trip.location, + category: null, + maxResults: newTotal, // Demander le total cumulé + reset: true, // Reset pour avoir tous les résultats d'un coup + )); + } } } @@ -1339,26 +1384,54 @@ class _ActivitiesPageState extends State print('📊 [Google Search] Current: $currentCount, Total demandé: $totalToRequest, Additional: $additionalNeeded'); if (additionalNeeded > 0) { - context.read().add(SearchActivities( - tripId: widget.trip.id!, - destination: widget.trip.location, - category: null, - maxResults: additionalNeeded, - offset: currentCount, - appendToExisting: true, // Ajouter aux résultats existants - )); + // Utiliser les coordonnées pré-géolocalisées du voyage si disponibles + if (widget.trip.hasCoordinates) { + print('🌍 [Google Search] Using pre-geocoded coordinates for additional results'); + context.read().add(SearchActivitiesWithCoordinates( + tripId: widget.trip.id!, + latitude: widget.trip.latitude!, + longitude: widget.trip.longitude!, + category: null, + maxResults: additionalNeeded, + offset: currentCount, + appendToExisting: true, // Ajouter aux résultats existants + )); + } else { + print('⚠️ [Google Search] No coordinates available, falling back to destination geocoding'); + context.read().add(SearchActivities( + tripId: widget.trip.id!, + destination: widget.trip.location, + category: null, + maxResults: additionalNeeded, + offset: currentCount, + appendToExisting: true, // Ajouter aux résultats existants + )); + } } else { print('⚠️ [Google Search] Pas besoin de charger plus (déjà suffisant)'); } } else { // Si pas de résultats existants, faire une recherche complète - context.read().add(SearchActivities( - tripId: widget.trip.id!, - destination: widget.trip.location, - category: null, - maxResults: totalToRequest, - reset: true, - )); + if (widget.trip.hasCoordinates) { + print('🌍 [Google Search] Using pre-geocoded coordinates for fresh search'); + context.read().add(SearchActivitiesWithCoordinates( + tripId: widget.trip.id!, + latitude: widget.trip.latitude!, + longitude: widget.trip.longitude!, + category: null, + maxResults: totalToRequest, + reset: true, + )); + } else { + print('⚠️ [Google Search] No coordinates available, falling back to destination geocoding'); + context.read().add(SearchActivities( + tripId: widget.trip.id!, + destination: widget.trip.location, + category: null, + maxResults: totalToRequest, + reset: true, + )); + } } } } diff --git a/lib/components/home/create_trip_content.dart b/lib/components/home/create_trip_content.dart index 1995f95..63978f8 100644 --- a/lib/components/home/create_trip_content.dart +++ b/lib/components/home/create_trip_content.dart @@ -20,6 +20,7 @@ import '../../repositories/group_repository.dart'; import 'package:http/http.dart' as http; import 'package:flutter_dotenv/flutter_dotenv.dart'; import '../../services/place_image_service.dart'; +import '../../services/trip_geocoding_service.dart'; /// Create trip content widget for trip creation and editing functionality. /// @@ -71,6 +72,7 @@ class _CreateTripContentState extends State { final _userService = UserService(); final _groupRepository = GroupRepository(); final _placeImageService = PlaceImageService(); + final _tripGeocodingService = TripGeocodingService(); /// Trip date variables DateTime? _startDate; @@ -1060,9 +1062,30 @@ class _CreateTripContentState extends State { imageUrl: _selectedImageUrl, // Ajouter l'URL de l'image ); + // Géolocaliser le voyage avant de le sauvegarder + Trip tripWithCoordinates; + try { + print('🌍 [CreateTrip] Géolocalisation en cours pour: ${trip.location}'); + tripWithCoordinates = await _tripGeocodingService.geocodeTrip(trip); + print('✅ [CreateTrip] Géolocalisation réussie: ${tripWithCoordinates.latitude}, ${tripWithCoordinates.longitude}'); + } catch (e) { + print('⚠️ [CreateTrip] Erreur de géolocalisation: $e'); + // Continuer sans coordonnées en cas d'erreur + tripWithCoordinates = trip; + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Voyage créé sans géolocalisation (pas d\'impact sur les fonctionnalités)'), + backgroundColor: Colors.orange, + duration: Duration(seconds: 2), + ), + ); + } + } + if (isEditing) { // Mode mise à jour - tripBloc.add(TripUpdateRequested(trip: trip)); + tripBloc.add(TripUpdateRequested(trip: tripWithCoordinates)); await _updateGroupMembers( widget.tripToEdit!.id!, @@ -1072,7 +1095,7 @@ class _CreateTripContentState extends State { } else { // Mode création - Le groupe sera créé dans le listener TripCreated - tripBloc.add(TripCreateRequested(trip: trip)); + tripBloc.add(TripCreateRequested(trip: tripWithCoordinates)); } } catch (e) { if (mounted) { diff --git a/lib/models/trip.dart b/lib/models/trip.dart index 514d0f6..7c0dcd5 100644 --- a/lib/models/trip.dart +++ b/lib/models/trip.dart @@ -19,6 +19,15 @@ class Trip { /// Trip destination or location. final String location; + /// Latitude coordinate of the trip location. + final double? latitude; + + /// Longitude coordinate of the trip location. + final double? longitude; + + /// Timestamp when the location was last geocoded. + final DateTime? lastGeocodingUpdate; + /// Trip start date and time. final DateTime startDate; @@ -54,6 +63,9 @@ class Trip { required this.title, required this.description, required this.location, + this.latitude, + this.longitude, + this.lastGeocodingUpdate, required this.startDate, required this.endDate, this.budget, @@ -101,6 +113,11 @@ class Trip { title: map['title'] as String? ?? '', description: map['description'] as String? ?? '', location: map['location'] as String? ?? '', + latitude: (map['latitude'] as num?)?.toDouble(), + longitude: (map['longitude'] as num?)?.toDouble(), + lastGeocodingUpdate: map['lastGeocodingUpdate'] != null + ? _parseDateTime(map['lastGeocodingUpdate']) + : null, startDate: _parseDateTime(map['startDate']), endDate: _parseDateTime(map['endDate']), budget: (map['budget'] as num?)?.toDouble(), @@ -122,6 +139,11 @@ class Trip { 'title': title, 'description': description, 'location': location, + 'latitude': latitude, + 'longitude': longitude, + 'lastGeocodingUpdate': lastGeocodingUpdate != null + ? Timestamp.fromDate(lastGeocodingUpdate!) + : null, 'startDate': Timestamp.fromDate(startDate), 'endDate': Timestamp.fromDate(endDate), 'budget': budget, @@ -145,6 +167,9 @@ class Trip { String? title, String? description, String? location, + double? latitude, + double? longitude, + DateTime? lastGeocodingUpdate, DateTime? startDate, DateTime? endDate, double? budget, @@ -160,6 +185,9 @@ class Trip { title: title ?? this.title, description: description ?? this.description, location: location ?? this.location, + latitude: latitude ?? this.latitude, + longitude: longitude ?? this.longitude, + lastGeocodingUpdate: lastGeocodingUpdate ?? this.lastGeocodingUpdate, startDate: startDate ?? this.startDate, endDate: endDate ?? this.endDate, budget: budget ?? this.budget, @@ -230,9 +258,28 @@ class Trip { } } + /// Vérifie si le voyage a des coordonnées géographiques valides + bool get hasCoordinates => latitude != null && longitude != null; + + /// Vérifie si les coordonnées sont récentes (moins de 30 jours) + bool get hasRecentCoordinates { + if (!hasCoordinates || lastGeocodingUpdate == null) return false; + final thirtyDaysAgo = DateTime.now().subtract(const Duration(days: 30)); + return lastGeocodingUpdate!.isAfter(thirtyDaysAgo); + } + + /// Retourne un Map avec les coordonnées pour les services externes + Map? get coordinatesMap { + if (!hasCoordinates) return null; + return { + 'lat': latitude!, + 'lng': longitude!, + }; + } + @override String toString() { - return 'Trip(id: $id, title: $title, location: $location, status: $status)'; + return 'Trip(id: $id, title: $title, location: $location, coordinates: ${hasCoordinates ? "($latitude, $longitude)" : "N/A"}, status: $status)'; } @override diff --git a/lib/services/activity_places_service.dart b/lib/services/activity_places_service.dart index 465a0af..615c611 100644 --- a/lib/services/activity_places_service.dart +++ b/lib/services/activity_places_service.dart @@ -377,7 +377,9 @@ class ActivityPlacesService { /// Recherche d'activités avec pagination (6 par page) Future> searchActivitiesPaginated({ - required String destination, + String? destination, + double? latitude, + double? longitude, required String tripId, ActivityCategory? category, int pageSize = 6, @@ -385,16 +387,29 @@ class ActivityPlacesService { int radius = 5000, }) async { try { - print('ActivityPlacesService: Recherche paginée pour: $destination (page: ${nextPageToken ?? "première"})'); + double lat, lng; + + // Utiliser les coordonnées fournies ou géocoder la destination + if (latitude != null && longitude != null) { + lat = latitude; + lng = longitude; + print('ActivityPlacesService: Utilisation des coordonnées pré-géolocalisées: $lat, $lng'); + } else if (destination != null) { + print('ActivityPlacesService: Géolocalisation de la destination: $destination'); + final coordinates = await _geocodeDestination(destination); + lat = coordinates['lat']!; + lng = coordinates['lng']!; + } else { + throw Exception('Destination ou coordonnées requises'); + } - // 1. Géocoder la destination - final coordinates = await _geocodeDestination(destination); + print('ActivityPlacesService: Recherche paginée aux coordonnées: $lat, $lng (page: ${nextPageToken ?? "première"})'); // 2. Rechercher les activités par catégorie avec pagination if (category != null) { return await _searchByCategoryPaginated( - coordinates['lat']!, - coordinates['lng']!, + lat, + lng, category, tripId, radius, @@ -404,8 +419,8 @@ class ActivityPlacesService { } else { // Pour toutes les catégories, faire une recherche générale paginée return await _searchAllCategoriesPaginated( - coordinates['lat']!, - coordinates['lng']!, + lat, + lng, tripId, radius, pageSize, diff --git a/lib/services/trip_geocoding_service.dart b/lib/services/trip_geocoding_service.dart new file mode 100644 index 0000000..417d54e --- /dev/null +++ b/lib/services/trip_geocoding_service.dart @@ -0,0 +1,118 @@ +import 'dart:convert'; +import 'package:http/http.dart' as http; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import '../models/trip.dart'; +import 'error_service.dart'; + +/// Service pour géocoder les destinations des voyages +class TripGeocodingService { + static final TripGeocodingService _instance = TripGeocodingService._internal(); + factory TripGeocodingService() => _instance; + TripGeocodingService._internal(); + + final ErrorService _errorService = ErrorService(); + static final String _apiKey = dotenv.env['GOOGLE_MAPS_API_KEY'] ?? ''; + + /// Géocode la destination d'un voyage et retourne un Trip mis à jour + Future geocodeTrip(Trip trip) async { + try { + print('🌍 [TripGeocoding] Géocodage de "${trip.location}"'); + + // Vérifier si on a déjà des coordonnées récentes + if (trip.hasRecentCoordinates) { + print('✅ [TripGeocoding] Coordonnées récentes trouvées, pas de géocodage nécessaire'); + return trip; + } + + if (_apiKey.isEmpty) { + print('❌ [TripGeocoding] Clé API Google Maps manquante'); + throw Exception('Clé API Google Maps non configurée'); + } + + final coordinates = await _geocodeDestination(trip.location); + + if (coordinates != null) { + print('✅ [TripGeocoding] Coordonnées trouvées: ${coordinates['lat']}, ${coordinates['lng']}'); + + return trip.copyWith( + latitude: coordinates['lat'], + longitude: coordinates['lng'], + lastGeocodingUpdate: DateTime.now(), + ); + } else { + print('⚠️ [TripGeocoding] Impossible de géocoder "${trip.location}"'); + return trip; + } + } catch (e) { + print('❌ [TripGeocoding] Erreur lors du géocodage: $e'); + _errorService.logError('trip_geocoding_service', e); + return trip; // Retourner le voyage original en cas d'erreur + } + } + + /// Géocode une destination et retourne les coordonnées + Future?> _geocodeDestination(String destination) async { + try { + final url = 'https://maps.googleapis.com/maps/api/geocode/json' + '?address=${Uri.encodeComponent(destination)}' + '&key=$_apiKey'; + + print('🌐 [TripGeocoding] URL = $url'); + + final response = await http.get(Uri.parse(url)); + print('📡 [TripGeocoding] Status code = ${response.statusCode}'); + + if (response.statusCode == 200) { + final data = json.decode(response.body); + print('📋 [TripGeocoding] Réponse géocodage = ${data['status']}'); + + if (data['status'] == 'OK' && data['results'].isNotEmpty) { + final location = data['results'][0]['geometry']['location']; + final coordinates = { + 'lat': (location['lat'] as num).toDouble(), + 'lng': (location['lng'] as num).toDouble(), + }; + print('📍 [TripGeocoding] Coordonnées trouvées = $coordinates'); + return coordinates; + } else { + print('⚠️ [TripGeocoding] Erreur API = ${data['error_message'] ?? data['status']}'); + return null; + } + } else { + print('❌ [TripGeocoding] Erreur HTTP ${response.statusCode}'); + return null; + } + } catch (e) { + print('❌ [TripGeocoding] Exception lors du géocodage: $e'); + _errorService.logError('trip_geocoding_service', e); + return null; + } + } + + /// Vérifie si un voyage a besoin d'être géocodé + bool needsGeocoding(Trip trip) { + return !trip.hasRecentCoordinates; + } + + /// Géocode plusieurs voyages en batch + Future> geocodeTrips(List trips) async { + print('🔄 [TripGeocoding] Géocodage de ${trips.length} voyages'); + + final List geocodedTrips = []; + + for (final trip in trips) { + if (needsGeocoding(trip)) { + final geocodedTrip = await geocodeTrip(trip); + geocodedTrips.add(geocodedTrip); + + // Petit délai pour éviter de saturer l'API Google + await Future.delayed(const Duration(milliseconds: 200)); + } else { + geocodedTrips.add(trip); + } + } + + print('✅ [TripGeocoding] Géocodage terminé'); + return geocodedTrips; + } +} \ No newline at end of file