Add functionality to manage account members: implement add and remove member events, update account repository methods, and integrate with trip details for participant management.

This commit is contained in:
Van Leemput Dayron
2025-11-14 00:03:38 +01:00
parent 9101a94691
commit c322bc079a
5 changed files with 415 additions and 115 deletions

View File

@@ -40,6 +40,8 @@ class AccountBloc extends Bloc<AccountEvent, AccountState> {
on<_AccountsUpdated>(_onAccountsUpdated); on<_AccountsUpdated>(_onAccountsUpdated);
on<CreateAccount>(_onCreateAccount); on<CreateAccount>(_onCreateAccount);
on<CreateAccountWithMembers>(_onCreateAccountWithMembers); on<CreateAccountWithMembers>(_onCreateAccountWithMembers);
on<AddMemberToAccount>(_onAddMemberToAccount);
on<RemoveMemberFromAccount>(_onRemoveMemberFromAccount);
} }
Future<void> _onLoadAccountsByUserId( Future<void> _onLoadAccountsByUserId(
@@ -109,6 +111,34 @@ class AccountBloc extends Bloc<AccountEvent, AccountState> {
} }
} }
Future<void> _onAddMemberToAccount(
AddMemberToAccount event,
Emitter<AccountState> 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<void> _onRemoveMemberFromAccount(
RemoveMemberFromAccount event,
Emitter<AccountState> 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 @override
Future<void> close() { Future<void> close() {
_accountsSubscription?.cancel(); _accountsSubscription?.cancel();

View File

@@ -86,3 +86,31 @@ class CreateAccountWithMembers extends AccountEvent {
@override @override
List<Object?> get props => [account, members]; List<Object?> 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<Object?> 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<Object?> get props => [accountId, memberId];
}

View File

@@ -17,6 +17,7 @@ import '../../models/group.dart';
import '../../models/group_member.dart'; import '../../models/group_member.dart';
import '../../services/user_service.dart'; import '../../services/user_service.dart';
import '../../repositories/group_repository.dart'; import '../../repositories/group_repository.dart';
import '../../repositories/account_repository.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart';
import '../../services/place_image_service.dart'; import '../../services/place_image_service.dart';
@@ -71,6 +72,7 @@ class _CreateTripContentState extends State<CreateTripContent> {
/// Services for user and group operations /// Services for user and group operations
final _userService = UserService(); final _userService = UserService();
final _groupRepository = GroupRepository(); final _groupRepository = GroupRepository();
final _accountRepository = AccountRepository();
final _placeImageService = PlaceImageService(); final _placeImageService = PlaceImageService();
final _tripGeocodingService = TripGeocodingService(); final _tripGeocodingService = TripGeocodingService();
@@ -611,10 +613,9 @@ class _CreateTripContentState extends State<CreateTripContent> {
const SizedBox(height: 20), const SizedBox(height: 20),
// Dates // Dates
Row( Column(
children: [ children: [
Expanded( Column(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
@@ -631,10 +632,8 @@ class _CreateTripContentState extends State<CreateTripContent> {
), ),
], ],
), ),
), const SizedBox(height: 20),
const SizedBox(width: 16), Column(
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
@@ -651,7 +650,6 @@ class _CreateTripContentState extends State<CreateTripContent> {
), ),
], ],
), ),
),
], ],
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
@@ -673,7 +671,8 @@ class _CreateTripContentState extends State<CreateTripContent> {
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
// Inviter des amis // Inviter des amis - seulement en mode création
if (!isEditing) ...[
Text( Text(
'Invite tes amis', 'Invite tes amis',
style: theme.textTheme.titleMedium?.copyWith( style: theme.textTheme.titleMedium?.copyWith(
@@ -750,6 +749,7 @@ class _CreateTripContentState extends State<CreateTripContent> {
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
], ],
],
const SizedBox(height: 32), const SizedBox(height: 32),
@@ -871,13 +871,15 @@ class _CreateTripContentState extends State<CreateTripContent> {
}); });
} }
// Mettre à jour le groupe avec les nouveaux membres // Mettre à jour le groupe ET le compte avec les nouveaux membres
Future<void> _updateGroupMembers( Future<void> _updateGroupAndAccountMembers(
String tripId, String tripId,
user_state.UserModel currentUser, user_state.UserModel currentUser,
List<Map<String, dynamic>> participantsData, List<Map<String, dynamic>> participantsData,
) async { ) async {
final groupBloc = context.read<GroupBloc>(); final groupBloc = context.read<GroupBloc>();
final accountBloc = context.read<AccountBloc>();
try { try {
final group = await _groupRepository.getGroupByTripId(tripId); final group = await _groupRepository.getGroupByTripId(tripId);
@@ -889,6 +891,9 @@ class _CreateTripContentState extends State<CreateTripContent> {
return; return;
} }
// Récupérer le compte associé au voyage
final account = await _accountRepository.getAccountByTripId(tripId);
final newMembers = await _createMembers(); final newMembers = await _createMembers();
final currentMembers = await _groupRepository.getGroupMembers(group.id); final currentMembers = await _groupRepository.getGroupMembers(group.id);
@@ -901,21 +906,41 @@ class _CreateTripContentState extends State<CreateTripContent> {
.where((m) => !newMemberIds.contains(m.userId) && m.role != 'admin') .where((m) => !newMemberIds.contains(m.userId) && m.role != 'admin')
.toList(); .toList();
// Ajouter les nouveaux membres au groupe ET au compte
for (final member in membersToAdd) { for (final member in membersToAdd) {
if (mounted) { if (mounted) {
groupBloc.add(AddMemberToGroup(group.id, member)); 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) { for (final member in membersToRemove) {
if (mounted) { if (mounted) {
groupBloc.add(RemoveMemberFromGroup(group.id, member.userId)); 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) { } catch (e) {
_errorService.logError( _errorService.logError(
'create_trip_content.dart', '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<CreateTripContent> {
// Géolocaliser le voyage avant de le sauvegarder // Géolocaliser le voyage avant de le sauvegarder
Trip tripWithCoordinates; Trip tripWithCoordinates;
try { try {
print('🌍 [CreateTrip] Géolocalisation en cours pour: ${trip.location}');
tripWithCoordinates = await _tripGeocodingService.geocodeTrip(trip); tripWithCoordinates = await _tripGeocodingService.geocodeTrip(trip);
print('✅ [CreateTrip] Géolocalisation réussie: ${tripWithCoordinates.latitude}, ${tripWithCoordinates.longitude}');
} catch (e) { } catch (e) {
print('⚠️ [CreateTrip] Erreur de géolocalisation: $e');
// Continuer sans coordonnées en cas d'erreur // Continuer sans coordonnées en cas d'erreur
tripWithCoordinates = trip; tripWithCoordinates = trip;
if (mounted) { if (mounted) {
@@ -1089,9 +1111,11 @@ class _CreateTripContentState extends State<CreateTripContent> {
// Mode mise à jour // Mode mise à jour
tripBloc.add(TripUpdateRequested(trip: tripWithCoordinates)); 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) { 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!, widget.tripToEdit!.id!,
currentUser, currentUser,
participantsData, participantsData,

View File

@@ -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/error_service.dart';
import 'package:travel_mate/services/activity_cache_service.dart'; import 'package:travel_mate/services/activity_cache_service.dart';
import 'package:travel_mate/repositories/group_repository.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:travel_mate/components/activities/activities_page.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
@@ -26,6 +29,8 @@ class _ShowTripDetailsContentState extends State<ShowTripDetailsContent> {
final ErrorService _errorService = ErrorService(); final ErrorService _errorService = ErrorService();
final ActivityCacheService _cacheService = ActivityCacheService(); final ActivityCacheService _cacheService = ActivityCacheService();
final GroupRepository _groupRepository = GroupRepository(); final GroupRepository _groupRepository = GroupRepository();
final UserRepository _userRepository = UserRepository();
final AccountRepository _accountRepository = AccountRepository();
@override @override
void initState() { void initState() {
@@ -683,6 +688,11 @@ class _ShowTripDetailsContentState extends State<ShowTripDetailsContent> {
); );
}, },
), ),
// Bouton "+" pour ajouter un participant
Padding(
padding: const EdgeInsets.only(right: 12),
child: _buildAddParticipantButton(),
),
], ],
), ),
); );
@@ -738,6 +748,196 @@ class _ShowTripDetailsContentState extends State<ShowTripDetailsContent> {
); );
} }
/// 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<void> _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<TripBloc>().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() { void _navigateToActivities() {
Navigator.push( Navigator.push(
context, context,

View File

@@ -198,4 +198,22 @@ class AccountRepository {
return null; return null;
}); });
} }
Future<void> 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<void> 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');
}
}
} }