diff --git a/lib/blocs/account/account_bloc.dart b/lib/blocs/account/account_bloc.dart index 4ea2bba..ea59753 100644 --- a/lib/blocs/account/account_bloc.dart +++ b/lib/blocs/account/account_bloc.dart @@ -40,6 +40,8 @@ class AccountBloc extends Bloc { on<_AccountsUpdated>(_onAccountsUpdated); on(_onCreateAccount); on(_onCreateAccountWithMembers); + on(_onAddMemberToAccount); + on(_onRemoveMemberFromAccount); } Future _onLoadAccountsByUserId( @@ -109,6 +111,34 @@ class AccountBloc extends Bloc { } } + Future _onAddMemberToAccount( + AddMemberToAccount event, + Emitter emit, + ) async { + try { + emit(AccountLoading()); + await _repository.addMemberToAccount(event.accountId, event.member); + emit(AccountOperationSuccess('Membre ajouté avec succès')); + } catch (e, stackTrace) { + _errorService.logError(e.toString(), stackTrace); + emit(AccountError('Erreur lors de l\'ajout du membre: ${e.toString()}')); + } + } + + Future _onRemoveMemberFromAccount( + RemoveMemberFromAccount event, + Emitter emit, + ) async { + try { + emit(AccountLoading()); + await _repository.removeMemberFromAccount(event.accountId, event.memberId); + emit(AccountOperationSuccess('Membre supprimé avec succès')); + } catch (e, stackTrace) { + _errorService.logError(e.toString(), stackTrace); + emit(AccountError('Erreur lors de la suppression du membre: ${e.toString()}')); + } + } + @override Future close() { _accountsSubscription?.cancel(); diff --git a/lib/blocs/account/account_event.dart b/lib/blocs/account/account_event.dart index 181bff9..c8f4081 100644 --- a/lib/blocs/account/account_event.dart +++ b/lib/blocs/account/account_event.dart @@ -85,4 +85,32 @@ class CreateAccountWithMembers extends AccountEvent { @override List get props => [account, members]; +} + +/// Event to add a member to an existing account. +/// +/// This event is dispatched when a new member needs to be added to +/// an account, typically when editing a trip and adding new participants. +class AddMemberToAccount extends AccountEvent { + final String accountId; + final GroupMember member; + + const AddMemberToAccount(this.accountId, this.member); + + @override + List get props => [accountId, member]; +} + +/// Event to remove a member from an existing account. +/// +/// This event is dispatched when a member needs to be removed from +/// an account, typically when editing a trip and removing participants. +class RemoveMemberFromAccount extends AccountEvent { + final String accountId; + final String memberId; + + const RemoveMemberFromAccount(this.accountId, this.memberId); + + @override + List get props => [accountId, memberId]; } \ No newline at end of file diff --git a/lib/components/home/create_trip_content.dart b/lib/components/home/create_trip_content.dart index 39b9f0d..65e94ee 100644 --- a/lib/components/home/create_trip_content.dart +++ b/lib/components/home/create_trip_content.dart @@ -17,6 +17,7 @@ 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'; @@ -71,6 +72,7 @@ class _CreateTripContentState extends State { /// Services for user and group operations final _userService = UserService(); final _groupRepository = GroupRepository(); + final _accountRepository = AccountRepository(); final _placeImageService = PlaceImageService(); final _tripGeocodingService = TripGeocodingService(); @@ -611,46 +613,42 @@ class _CreateTripContentState extends State { const SizedBox(height: 20), // Dates - Row( + Column( children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Début du voyage', - style: theme.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w600, - color: theme.colorScheme.onSurface, - ), + 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: 12), + _buildDateField( + date: _startDate, + onTap: () => _selectStartDate(context), + ), + ], ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Fin du voyage', - style: theme.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w600, - color: theme.colorScheme.onSurface, - ), + 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: 12), + _buildDateField( + date: _endDate, + onTap: () => _selectEndDate(context), + ), + ], ), ], ), @@ -673,82 +671,84 @@ class _CreateTripContentState extends State { ), const SizedBox(height: 20), - // Inviter des amis - Text( - 'Invite tes amis', - style: theme.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w600, - color: theme.colorScheme.onSurface, + // 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, + 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.withOpacity(0.1), - borderRadius: BorderRadius.circular(20), + color: Colors.teal, + borderRadius: BorderRadius.circular(12), ), - 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, - ), - ), - ], + child: IconButton( + onPressed: _addParticipant, + icon: const Icon(Icons.add, color: Colors.white), ), - ); - }).toList(), + ), + ], ), - const SizedBox(height: 20), + 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), @@ -871,13 +871,15 @@ class _CreateTripContentState extends State { }); } - // Mettre à jour le groupe avec les nouveaux membres - Future _updateGroupMembers( + // 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); @@ -889,6 +891,9 @@ class _CreateTripContentState extends State { 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); @@ -901,21 +906,41 @@ class _CreateTripContentState extends State { .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: $e', + 'Erreur lors de la mise à jour du groupe et du compte: $e', ); } } @@ -1067,11 +1092,8 @@ class _CreateTripContentState extends State { // 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) { @@ -1089,14 +1111,16 @@ class _CreateTripContentState extends State { // Mode mise à jour tripBloc.add(TripUpdateRequested(trip: tripWithCoordinates)); - // Vérifier que l'ID du voyage existe avant de mettre à jour le groupe + // Mettre à jour le groupe ET les comptes avec les nouveaux participants if (widget.tripToEdit != null && widget.tripToEdit!.id != null && widget.tripToEdit!.id!.isNotEmpty) { - await _updateGroupMembers( + 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 diff --git a/lib/components/home/show_trip_details_content.dart b/lib/components/home/show_trip_details_content.dart index 3e5499d..478c6f3 100644 --- a/lib/components/home/show_trip_details_content.dart +++ b/lib/components/home/show_trip_details_content.dart @@ -11,6 +11,9 @@ import 'package:travel_mate/components/map/map_content.dart'; import 'package:travel_mate/services/error_service.dart'; import 'package:travel_mate/services/activity_cache_service.dart'; import 'package:travel_mate/repositories/group_repository.dart'; +import 'package:travel_mate/repositories/user_repository.dart'; +import 'package:travel_mate/repositories/account_repository.dart'; +import 'package:travel_mate/models/group_member.dart'; import 'package:travel_mate/components/activities/activities_page.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -26,6 +29,8 @@ class _ShowTripDetailsContentState extends State { final ErrorService _errorService = ErrorService(); final ActivityCacheService _cacheService = ActivityCacheService(); final GroupRepository _groupRepository = GroupRepository(); + final UserRepository _userRepository = UserRepository(); + final AccountRepository _accountRepository = AccountRepository(); @override void initState() { @@ -683,6 +688,11 @@ class _ShowTripDetailsContentState extends State { ); }, ), + // Bouton "+" pour ajouter un participant + Padding( + padding: const EdgeInsets.only(right: 12), + child: _buildAddParticipantButton(), + ), ], ), ); @@ -738,6 +748,196 @@ class _ShowTripDetailsContentState extends State { ); } + /// Construire le bouton pour ajouter un participant + Widget _buildAddParticipantButton() { + final theme = Theme.of(context); + + return Tooltip( + message: 'Ajouter un participant', + child: Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: theme.colorScheme.primary.withValues(alpha: 0.3), + width: 2, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.1), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: GestureDetector( + onTap: _showAddParticipantDialog, + child: CircleAvatar( + radius: 28, + backgroundColor: theme.colorScheme.primary.withValues(alpha: 0.1), + child: Icon( + Icons.add, + color: theme.colorScheme.primary, + size: 28, + ), + ), + ), + ), + ); + } + + /// Afficher le dialogue pour ajouter un participant + void _showAddParticipantDialog() { + final theme = Theme.of(context); + final TextEditingController emailController = TextEditingController(); + + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + backgroundColor: theme.dialogBackgroundColor, + title: Text( + 'Ajouter un participant', + style: theme.textTheme.titleLarge?.copyWith( + color: theme.colorScheme.onSurface, + ), + ), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Entrez l\'email du participant à ajouter :', + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurface, + ), + ), + const SizedBox(height: 16), + TextField( + controller: emailController, + keyboardType: TextInputType.emailAddress, + decoration: InputDecoration( + hintText: 'participant@example.com', + hintStyle: TextStyle( + color: theme.colorScheme.onSurface.withValues(alpha: 0.5), + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + ), + style: TextStyle(color: theme.colorScheme.onSurface), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text( + 'Annuler', + style: TextStyle(color: theme.colorScheme.primary), + ), + ), + TextButton( + onPressed: () { + if (emailController.text.isNotEmpty) { + _addParticipantByEmail(emailController.text); + Navigator.pop(context); + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Veuillez entrer un email valide'), + backgroundColor: Colors.red, + ), + ); + } + }, + child: Text( + 'Ajouter', + style: TextStyle(color: theme.colorScheme.primary), + ), + ), + ], + ); + }, + ); + } + + /// Ajouter un participant par email + Future _addParticipantByEmail(String email) async { + try { + // Chercher l'utilisateur par email + final user = await _userRepository.getUserByEmail(email); + + if (user == null) { + _errorService.showError( + message: 'Utilisateur non trouvé avec cet email', + ); + return; + } + + if (user.id == null) { + _errorService.showError( + message: 'ID utilisateur invalide', + ); + return; + } + + // Ajouter l'utilisateur au groupe + if (widget.trip.id != null) { + final group = await _groupRepository.getGroupByTripId(widget.trip.id!); + if (group != null) { + // Créer un GroupMember à partir du User + final newMember = GroupMember( + userId: user.id!, + firstName: user.prenom, + pseudo: user.prenom, + profilePictureUrl: user.profilePictureUrl, + ); + + // Ajouter le membre au groupe + await _groupRepository.addMember(group.id, newMember); + + // Ajouter le membre au compte + final account = await _accountRepository.getAccountByTripId(widget.trip.id!); + if (account != null) { + await _accountRepository.addMemberToAccount(account.id, newMember); + } + + // Mettre à jour la liste des participants du voyage + final newParticipants = [ + ...widget.trip.participants, + user.id!, + ]; + final updatedTrip = widget.trip.copyWith( + participants: newParticipants, + ); + + if (mounted) { + context.read().add( + TripUpdateRequested(trip: updatedTrip), + ); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('${user.prenom} a été ajouté au voyage'), + backgroundColor: Colors.green, + ), + ); + + // Rafraîchir la page + setState(() {}); + } + } + } + } catch (e) { + _errorService.showError( + message: 'Erreur lors de l\'ajout du participant: $e', + ); + } + } + void _navigateToActivities() { Navigator.push( context, diff --git a/lib/repositories/account_repository.dart b/lib/repositories/account_repository.dart index 16ef55a..0273263 100644 --- a/lib/repositories/account_repository.dart +++ b/lib/repositories/account_repository.dart @@ -198,4 +198,22 @@ class AccountRepository { return null; }); } + + Future addMemberToAccount(String accountId, GroupMember member) async { + try { + await _membersCollection(accountId).doc(member.userId).set(member.toMap()); + } catch (e) { + _errorService.logError('account_repository.dart', 'Erreur lors de l\'ajout du membre: $e'); + throw Exception('Erreur lors de l\'ajout du membre: $e'); + } + } + + Future removeMemberFromAccount(String accountId, String memberId) async { + try { + await _membersCollection(accountId).doc(memberId).delete(); + } catch (e) { + _errorService.logError('account_repository.dart', 'Erreur lors de la suppression du membre: $e'); + throw Exception('Erreur lors de la suppression du membre: $e'); + } + } } \ No newline at end of file