import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:travel_mate/models/trip.dart'; import 'package:travel_mate/services/error_service.dart'; import '../../blocs/user/user_bloc.dart'; import '../../blocs/user/user_state.dart' as user_state; import '../../blocs/trip/trip_bloc.dart'; import '../../blocs/trip/trip_event.dart'; import '../../blocs/trip/trip_state.dart'; import '../../blocs/group/group_bloc.dart'; import '../../blocs/group/group_event.dart'; import '../../blocs/account/account_bloc.dart'; import '../../blocs/account/account_event.dart'; import '../../models/account.dart'; import '../../models/group.dart'; import '../../models/group_member.dart'; import '../../services/user_service.dart'; import '../../repositories/group_repository.dart'; import '../../repositories/account_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. /// /// This widget provides a comprehensive form interface for creating new trips /// or editing existing ones. Key features include: /// - Trip creation with validation /// - Location search with autocomplete /// - Date selection for trip duration /// - Budget planning and management /// - Group creation and member management /// - Account setup for expense tracking /// - Integration with mapping services for location selection /// /// The widget handles both creation and editing modes based on the /// provided tripToEdit parameter. class CreateTripContent extends StatefulWidget { /// Optional trip to edit. If null, creates a new trip final Trip? tripToEdit; /// Creates a create trip content widget. /// /// Args: /// [tripToEdit]: Optional trip to edit. If provided, the form will /// be populated with existing trip data for editing const CreateTripContent({ super.key, this.tripToEdit, }); @override State createState() => _CreateTripContentState(); } class _CreateTripContentState extends State { /// Service for handling and displaying errors final _errorService = ErrorService(); /// Form validation key final _formKey = GlobalKey(); /// Text controllers for form fields final _titleController = TextEditingController(); final _descriptionController = TextEditingController(); final _locationController = TextEditingController(); final _budgetController = TextEditingController(); final _participantController = TextEditingController(); /// Services for user and group operations final _userService = UserService(); final _groupRepository = GroupRepository(); final _accountRepository = AccountRepository(); final _placeImageService = PlaceImageService(); final _tripGeocodingService = TripGeocodingService(); /// Trip date variables DateTime? _startDate; DateTime? _endDate; /// Loading and state management variables bool _isLoading = false; String? _createdTripId; String? _selectedImageUrl; /// Google Maps API key for location services static final String _apiKey = dotenv.env['GOOGLE_MAPS_API_KEY'] ?? ''; /// Participant management final List _participants = []; /// Location autocomplete functionality List _placeSuggestions = []; bool _isLoadingSuggestions = false; OverlayEntry? _suggestionsOverlay; final LayerLink _layerLink = LayerLink(); /// Determines if the widget is in editing mode bool get isEditing => widget.tripToEdit != null; @override void initState() { super.initState(); _initializeFormWithTrip(); _locationController.addListener(_onLocationChanged); } Future _initializeFormWithTrip() async { if (widget.tripToEdit != null) { final trip = widget.tripToEdit!; setState(() { _titleController.text = trip.title; _descriptionController.text = trip.description; _locationController.text = trip.location; _budgetController.text = trip.budget?.toString() ?? ''; _startDate = trip.startDate; _endDate = trip.endDate; _selectedImageUrl = trip.imageUrl; // Charger l'image existante }); await _loadParticipantEmails(trip.participants); } } void _onLocationChanged() { final query = _locationController.text.trim(); if (query.length < 2) { _hideSuggestions(); return; } _fetchPlaceSuggestions(query); } Future _fetchPlaceSuggestions(String query) async { if (_apiKey.isEmpty) { return; } setState(() { _isLoadingSuggestions = true; }); try { final url = Uri.parse( 'https://maps.googleapis.com/maps/api/place/autocomplete/json' '?input=${Uri.encodeComponent(query)}' '&types=(cities)' '&language=fr' '&key=$_apiKey' ); final response = await http.get(url); if (response.statusCode == 200) { final data = json.decode(response.body); if (data['status'] == 'OK') { final predictions = data['predictions'] as List; setState(() { _placeSuggestions = predictions.map((prediction) { return PlaceSuggestion( placeId: prediction['place_id'], description: prediction['description'], ); }).toList(); _isLoadingSuggestions = false; }); if (_placeSuggestions.isNotEmpty) { _showSuggestions(); } else { _hideSuggestions(); } } else { setState(() { _placeSuggestions = []; _isLoadingSuggestions = false; }); _hideSuggestions(); } } else { setState(() { _placeSuggestions = []; _isLoadingSuggestions = false; }); _hideSuggestions(); } } catch (e) { setState(() { _placeSuggestions = []; _isLoadingSuggestions = false; }); _hideSuggestions(); } } // Nouvelle méthode pour afficher les suggestions void _showSuggestions() { _hideSuggestions(); // Masquer d'abord les suggestions existantes if (_placeSuggestions.isEmpty) return; _suggestionsOverlay = OverlayEntry( builder: (context) => Positioned( width: MediaQuery.of(context).size.width - 32, // Largeur du champ avec padding child: CompositedTransformFollower( link: _layerLink, showWhenUnlinked: false, offset: const Offset(0, 60), // Position sous le champ child: Material( elevation: 4, borderRadius: BorderRadius.circular(8), child: Container( constraints: const BoxConstraints(maxHeight: 200), decoration: BoxDecoration( color: Theme.of(context).scaffoldBackgroundColor, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.grey[300]!), ), child: ListView.builder( shrinkWrap: true, itemCount: _placeSuggestions.length, itemBuilder: (context, index) { final suggestion = _placeSuggestions[index]; return ListTile( leading: const Icon(Icons.location_on, color: Colors.grey), title: Text( suggestion.description, style: const TextStyle(fontSize: 14), ), dense: true, onTap: () => _selectSuggestion(suggestion), ); }, ), ), ), ), ), ); Overlay.of(context).insert(_suggestionsOverlay!); } void _hideSuggestions() { _suggestionsOverlay?.remove(); _suggestionsOverlay = null; } void _selectSuggestion(PlaceSuggestion suggestion) { _locationController.text = suggestion.description; _hideSuggestions(); setState(() { _placeSuggestions = []; }); // Charger l'image du lieu sélectionné _loadPlaceImage(suggestion.description); } /// Charge l'image du lieu depuis Google Places API Future _loadPlaceImage(String location) async { print('CreateTripContent: Chargement de l\'image pour: $location'); try { final imageUrl = await _placeImageService.getPlaceImageUrl(location); print('CreateTripContent: Image URL reçue: $imageUrl'); if (mounted) { setState(() { _selectedImageUrl = imageUrl; }); print('CreateTripContent: État mis à jour avec imageUrl: $_selectedImageUrl'); } } catch (e) { print('CreateTripContent: Erreur lors du chargement de l\'image: $e'); if (mounted) { _errorService.logError( 'create_trip_content.dart', 'Erreur lors du chargement de l\'image: $e', ); } } } Future _loadParticipantEmails(List participantIds) async { final userState = context.read().state; String? currentUserId; if (userState is user_state.UserLoaded) { currentUserId = userState.user.id; } for (String userId in participantIds) { if (userId == currentUserId) continue; try { final userDoc = await _userService.getUserById(userId); if (userDoc != null && userDoc.email.isNotEmpty) { setState(() { _participants.add(userDoc.email); }); } } catch (e) { _errorService.logError( 'create_trip_content.dart', 'Erreur chargement participant $userId: $e', ); } } } @override void dispose() { _titleController.dispose(); _descriptionController.dispose(); _locationController.dispose(); _budgetController.dispose(); _participantController.dispose(); _hideSuggestions(); super.dispose(); } // Nouveau widget pour les champs de texte modernes Widget _buildModernTextField({ required TextEditingController controller, required String label, required IconData icon, String? Function(String?)? validator, TextInputType? keyboardType, int maxLines = 1, Widget? suffixIcon, }) { final theme = Theme.of(context); final isDarkMode = theme.brightness == Brightness.dark; return TextFormField( controller: controller, validator: validator, keyboardType: keyboardType, maxLines: maxLines, style: theme.textTheme.bodyLarge?.copyWith( color: theme.colorScheme.onSurface, ), decoration: InputDecoration( hintText: label, hintStyle: theme.textTheme.bodyLarge?.copyWith( color: theme.colorScheme.onSurface.withOpacity(0.5), ), prefixIcon: Icon( icon, color: theme.colorScheme.onSurface.withOpacity(0.5), ), suffixIcon: suffixIcon, border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide( color: isDarkMode ? Colors.white.withOpacity(0.2) : Colors.black.withOpacity(0.2), ), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide( color: isDarkMode ? Colors.white.withOpacity(0.2) : Colors.black.withOpacity(0.2), ), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide( color: Colors.teal, width: 2, ), ), errorBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: const BorderSide( color: Colors.red, width: 2, ), ), filled: true, fillColor: theme.cardColor, contentPadding: const EdgeInsets.symmetric( horizontal: 16, vertical: 16, ), ), ); } // Nouveau widget pour les champs de date modernes Widget _buildDateField({ required DateTime? date, required VoidCallback onTap, }) { final theme = Theme.of(context); final isDarkMode = theme.brightness == Brightness.dark; return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(12), child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: theme.cardColor, borderRadius: BorderRadius.circular(12), border: Border.all( color: isDarkMode ? Colors.white.withOpacity(0.2) : Colors.black.withOpacity(0.2), ), ), child: Row( children: [ Icon( Icons.calendar_today, color: theme.colorScheme.onSurface.withOpacity(0.5), size: 20, ), const SizedBox(width: 12), Text( date != null ? '${date.day.toString().padLeft(2, '0')}/${date.month.toString().padLeft(2, '0')}/${date.year}' : 'mm/dd/yyyy', style: theme.textTheme.bodyLarge?.copyWith( color: date != null ? theme.colorScheme.onSurface : theme.colorScheme.onSurface.withOpacity(0.5), ), ), ], ), ), ); } @override Widget build(BuildContext context) { final theme = Theme.of(context); final isDarkMode = theme.brightness == Brightness.dark; return BlocListener( listener: (context, tripState) { if (tripState is TripCreated) { _createdTripId = tripState.tripId; _createGroupAndAccountForTrip(_createdTripId!); } else if (tripState is TripOperationSuccess) { if (mounted) { _errorService.showSnackbar(message: tripState.message, isError: false); setState(() { _isLoading = false; }); Navigator.pop(context, true); if (isEditing) { Navigator.pop(context, true); } } } else if (tripState is TripError) { if (mounted) { _errorService.showSnackbar(message: tripState.message, isError: true); setState(() { _isLoading = false; }); } } }, child: BlocBuilder( builder: (context, userState) { if (userState is! user_state.UserLoaded) { return Scaffold( backgroundColor: theme.scaffoldBackgroundColor, appBar: AppBar( title: Text(isEditing ? 'Modifier le voyage' : 'Créer un voyage'), backgroundColor: theme.appBarTheme.backgroundColor, foregroundColor: theme.appBarTheme.foregroundColor, ), body: Center( child: Text( 'Veuillez vous connecter', style: theme.textTheme.bodyLarge?.copyWith( color: theme.colorScheme.onSurface, ), ), ), ); } return Scaffold( backgroundColor: theme.scaffoldBackgroundColor, appBar: AppBar( title: Text( isEditing ? 'Modifier le voyage' : 'Créer un voyage', style: theme.textTheme.titleLarge?.copyWith( color: theme.colorScheme.onSurface, fontWeight: FontWeight.w600, ), ), backgroundColor: Colors.transparent, elevation: 0, leading: IconButton( icon: Icon(Icons.arrow_back, color: theme.colorScheme.onSurface), onPressed: () => Navigator.pop(context), ), ), body: GestureDetector( onTap: _hideSuggestions, child: Container( margin: const EdgeInsets.all(16), decoration: BoxDecoration( color: theme.cardColor, borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(isDarkMode ? 0.3 : 0.1), blurRadius: 10, offset: const Offset(0, 5), ), ], ), child: SingleChildScrollView( padding: const EdgeInsets.all(24), child: Form( key: _formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Titre principal Text( 'Nouveau Voyage', style: theme.textTheme.headlineMedium?.copyWith( fontWeight: FontWeight.bold, color: theme.colorScheme.onSurface, ), ), const SizedBox(height: 8), Text( 'Donne un nom à ton voyage', style: theme.textTheme.bodyMedium?.copyWith( color: theme.colorScheme.onSurface.withOpacity(0.7), ), ), const SizedBox(height: 24), // Champ nom du voyage _buildModernTextField( controller: _titleController, label: 'Ex : Week-end à Lisbonne', icon: Icons.edit, validator: (value) { if (value == null || value.trim().isEmpty) { return 'Nom du voyage requis'; } return null; }, ), const SizedBox(height: 20), // Destination Text( 'Destination', style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, color: theme.colorScheme.onSurface, ), ), const SizedBox(height: 12), CompositedTransformTarget( link: _layerLink, child: _buildModernTextField( controller: _locationController, label: 'Rechercher une ville ou un pays', icon: Icons.public, validator: (value) { if (value == null || value.trim().isEmpty) { return 'Destination requise'; } return null; }, suffixIcon: _isLoadingSuggestions ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2), ) : null, ), ), const SizedBox(height: 20), // Description Text( 'Description (Optionnel)', style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, color: theme.colorScheme.onSurface, ), ), const SizedBox(height: 12), _buildModernTextField( controller: _descriptionController, label: 'Décris ton voyage en quelques mots', icon: Icons.description, maxLines: 4, ), const SizedBox(height: 20), // Dates Column( children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Début du voyage', style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, color: theme.colorScheme.onSurface, ), ), const SizedBox(height: 12), _buildDateField( date: _startDate, onTap: () => _selectStartDate(context), ), ], ), const SizedBox(height: 20), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Fin du voyage', style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, color: theme.colorScheme.onSurface, ), ), const SizedBox(height: 12), _buildDateField( date: _endDate, onTap: () => _selectEndDate(context), ), ], ), ], ), const SizedBox(height: 20), // Budget Text( 'Budget estimé par personne (€)', style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, color: theme.colorScheme.onSurface, ), ), const SizedBox(height: 12), _buildModernTextField( controller: _budgetController, label: 'Ex : 500', icon: Icons.euro, keyboardType: TextInputType.numberWithOptions(decimal: true), ), const SizedBox(height: 20), // Inviter des amis - seulement en mode création if (!isEditing) ...[ Text( 'Invite tes amis', style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, color: theme.colorScheme.onSurface, ), ), const SizedBox(height: 12), Row( children: [ Expanded( child: _buildModernTextField( controller: _participantController, label: 'adresse@email.com', icon: Icons.alternate_email, keyboardType: TextInputType.emailAddress, ), ), const SizedBox(width: 12), Container( height: 56, width: 56, decoration: BoxDecoration( color: Colors.teal, borderRadius: BorderRadius.circular(12), ), child: IconButton( onPressed: _addParticipant, icon: const Icon(Icons.add, color: Colors.white), ), ), ], ), const SizedBox(height: 16), // Participants ajoutés if (_participants.isNotEmpty) ...[ Wrap( spacing: 8, runSpacing: 8, children: _participants.map((email) { return Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 8, ), decoration: BoxDecoration( color: Colors.teal.withOpacity(0.1), borderRadius: BorderRadius.circular(20), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text( email, style: theme.textTheme.bodySmall?.copyWith( color: Colors.teal, fontWeight: FontWeight.w500, ), ), const SizedBox(width: 8), GestureDetector( onTap: () => _removeParticipant(email), child: const Icon( Icons.close, size: 16, color: Colors.teal, ), ), ], ), ); }).toList(), ), const SizedBox(height: 20), ], ], const SizedBox(height: 32), // Bouton créer SizedBox( width: double.infinity, height: 56, child: ElevatedButton( onPressed: _isLoading ? null : () => _saveTrip(userState.user), style: ElevatedButton.styleFrom( backgroundColor: Colors.teal, foregroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), elevation: 0, ), child: _isLoading ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation(Colors.white), ), ) : Text( isEditing ? 'Modifier le voyage' : 'Créer le voyage', style: theme.textTheme.titleMedium?.copyWith( color: Colors.white, fontWeight: FontWeight.w600, ), ), ), ), const SizedBox(height: 20), ], ), ), ), ), ), ); }, ), ); } Future _selectStartDate(BuildContext context) async { final DateTime? picked = await showDatePicker( context: context, initialDate: _startDate ?? DateTime.now(), firstDate: DateTime.now(), lastDate: DateTime.now().add(Duration(days: 365 * 2)), ); if (picked != null) { setState(() { _startDate = picked; if (_endDate != null && _endDate!.isBefore(picked)) { _endDate = null; } }); } } Future _selectEndDate(BuildContext context) async { if (_startDate == null) { if (mounted) { _errorService.showSnackbar( message: 'Veuillez d\'abord sélectionner la date de début', isError: true, ); } return; } final DateTime? picked = await showDatePicker( context: context, initialDate: _endDate ?? _startDate!.add(Duration(days: 1)), firstDate: _startDate!, lastDate: DateTime.now().add(Duration(days: 365 * 2)), ); if (picked != null && mounted) { setState(() { _endDate = picked; }); } } void _addParticipant() { final email = _participantController.text.trim(); if (email.isEmpty) return; final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$'); if (!emailRegex.hasMatch(email)) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Email invalide'))); } return; } if (_participants.contains(email)) { if (mounted) { ScaffoldMessenger.of(context) .showSnackBar(SnackBar(content: Text('Ce participant est déjà ajouté'))); } return; } setState(() { _participants.add(email); _participantController.clear(); }); } void _removeParticipant(String email) { setState(() { _participants.remove(email); }); } // Mettre à jour le groupe ET le compte avec les nouveaux membres Future _updateGroupAndAccountMembers( String tripId, user_state.UserModel currentUser, List> participantsData, ) async { final groupBloc = context.read(); final accountBloc = context.read(); try { final group = await _groupRepository.getGroupByTripId(tripId); if (group == null) { _errorService.logError( 'create_trip_content.dart', 'Groupe non trouvé pour le voyage $tripId', ); return; } // Récupérer le compte associé au voyage final account = await _accountRepository.getAccountByTripId(tripId); final newMembers = await _createMembers(); final currentMembers = await _groupRepository.getGroupMembers(group.id); final currentMemberIds = currentMembers.map((m) => m.userId).toSet(); final newMemberIds = newMembers.map((m) => m.userId).toSet(); final membersToAdd = newMembers.where((m) => !currentMemberIds.contains(m.userId)).toList(); final membersToRemove = currentMembers .where((m) => !newMemberIds.contains(m.userId) && m.role != 'admin') .toList(); // Ajouter les nouveaux membres au groupe ET au compte for (final member in membersToAdd) { if (mounted) { groupBloc.add(AddMemberToGroup(group.id, member)); if (account != null) { accountBloc.add(AddMemberToAccount(account.id, member)); } } } // Supprimer les membres supprimés du groupe ET du compte for (final member in membersToRemove) { if (mounted) { groupBloc.add(RemoveMemberFromGroup(group.id, member.userId)); if (account != null) { accountBloc.add(RemoveMemberFromAccount(account.id, member.userId)); } } } if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Groupe et compte mis à jour avec succès !'), backgroundColor: Colors.green, ), ); setState(() { _isLoading = false; }); } } catch (e) { _errorService.logError( 'create_trip_content.dart', 'Erreur lors de la mise à jour du groupe et du compte: $e', ); } } Future> _createMembers() async { final userState = context.read().state; if (userState is! user_state.UserLoaded) return []; final currentUser = userState.user; final participantsData = await _getParticipantsData(_participants); final groupMembers = [ GroupMember( userId: currentUser.id, firstName: currentUser.prenom, lastName: currentUser.nom, pseudo: currentUser.prenom, role: 'admin', profilePictureUrl: currentUser.profilePictureUrl, ), ...participantsData.map((p) => GroupMember( userId: p['id'] as String, firstName: p['firstName'] as String, lastName: p['lastName'] as String? ?? '', pseudo: p['firstName'] as String, role: 'member', profilePictureUrl: p['profilePictureUrl'] as String?, )), ]; return groupMembers; } Future _createGroupAndAccountForTrip(String tripId) async { final groupBloc = context.read(); final accountBloc = context.read(); try { final userState = context.read().state; if (userState is! user_state.UserLoaded) { throw Exception('Utilisateur non connecté'); } final currentUser = userState.user; final group = Group( id: '', name: _titleController.text.trim(), tripId: tripId, createdBy: currentUser.id, ); final groupMembers = await _createMembers(); if (groupMembers.isEmpty) { throw Exception('Erreur lors de la création des membres du groupe'); } groupBloc.add(CreateGroupWithMembers( group: group, members: groupMembers, )); final account = Account( id: '', tripId: tripId, name: _titleController.text.trim(), ); accountBloc.add(CreateAccountWithMembers( account: account, members: groupMembers, )); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Voyage, groupe et compte créés avec succès !'), backgroundColor: Colors.green, ), ); setState(() { _isLoading = false; }); Navigator.pop(context); } } catch (e) { _errorService.logError( 'create_trip_content.dart', 'Erreur lors de la création du groupe et compte: $e', ); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Erreur: $e'), backgroundColor: Colors.red, ), ); setState(() { _isLoading = false; }); } } } Future _saveTrip(user_state.UserModel currentUser) async { if (!_formKey.currentState!.validate()) { return; } if (_startDate == null || _endDate == null) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Veuillez sélectionner les dates')), ); } return; } setState(() { _isLoading = true; }); final tripBloc = context.read(); try { final participantsData = await _getParticipantsData(_participants); List participantIds = participantsData.map((p) => p['id'] as String).toList(); if (!participantIds.contains(currentUser.id)) { participantIds.insert(0, currentUser.id); } final trip = Trip( id: isEditing ? widget.tripToEdit!.id : '', title: _titleController.text.trim(), description: _descriptionController.text.trim(), location: _locationController.text.trim(), startDate: _startDate!, endDate: _endDate!, budget: double.tryParse(_budgetController.text) ?? 0.0, createdBy: currentUser.id, participants: participantIds, createdAt: isEditing ? widget.tripToEdit!.createdAt : DateTime.now(), updatedAt: DateTime.now(), imageUrl: _selectedImageUrl, // Ajouter l'URL de l'image ); // Géolocaliser le voyage avant de le sauvegarder Trip tripWithCoordinates; try { tripWithCoordinates = await _tripGeocodingService.geocodeTrip(trip); } catch (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: tripWithCoordinates)); // Mettre à jour le groupe ET les comptes avec les nouveaux participants if (widget.tripToEdit != null && widget.tripToEdit!.id != null && widget.tripToEdit!.id!.isNotEmpty) { print('🔄 [CreateTrip] Mise à jour du groupe et du compte pour le voyage ID: ${widget.tripToEdit!.id}'); print('👥 Participants: ${participantsData.map((p) => p['id']).toList()}'); await _updateGroupAndAccountMembers( widget.tripToEdit!.id!, currentUser, participantsData, ); } } else { // Mode création - Le groupe sera créé dans le listener TripCreated tripBloc.add(TripCreateRequested(trip: tripWithCoordinates)); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Erreur: $e'), backgroundColor: Colors.red, ), ); setState(() { _isLoading = false; }); } } } Future>> _getParticipantsData(List emails) async { List> participantsData = []; for (String email in emails) { try { final userId = await _userService.getUserIdByEmail(email); if (userId != null) { final userDoc = await _userService.getUserById(userId); final firstName = userDoc?.prenom ?? 'Utilisateur'; final lastName = userDoc?.nom ?? ''; final profilePictureUrl = userDoc?.profilePictureUrl; participantsData.add({ 'id': userId, 'firstName': firstName, 'lastName': lastName, 'profilePictureUrl': profilePictureUrl, }); } else { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Utilisateur non trouvé: $email'), backgroundColor: Colors.orange, ), ); } } } catch (e) { _errorService.logError( 'create_trip_content.dart', 'Erreur lors de la récupération de l\'utilisateur $email: $e', ); } } return participantsData; } } class PlaceSuggestion { final String placeId; final String description; PlaceSuggestion({ required this.placeId, required this.description, }); }