From fc403e5d26cc16fd49865fdca364490d2c171a0f Mon Sep 17 00:00:00 2001 From: Dayron Date: Tue, 14 Oct 2025 23:53:20 +0200 Subject: [PATCH] feat: Implement group management with BLoC pattern; add GroupBloc, GroupRepository, and related models NOT FUNCTIONNAL --- devtools_options.yaml | 3 + lib/blocs/group/group_bloc.dart | 148 ++++++----- lib/blocs/group/group_event.dart | 98 ++++--- lib/blocs/group/group_state.dart | 15 +- lib/components/group/group_content.dart | 257 +++++++++++++++---- lib/components/home/create_trip_content.dart | 68 +++-- lib/data/models/group.dart | 59 ++++- lib/data/models/group_member.dart | 51 ++++ lib/main.dart | 8 +- lib/repositories/group_repository.dart | 186 ++++++++++++++ 10 files changed, 708 insertions(+), 185 deletions(-) create mode 100644 devtools_options.yaml create mode 100644 lib/data/models/group_member.dart create mode 100644 lib/repositories/group_repository.dart diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 0000000..fa0b357 --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/lib/blocs/group/group_bloc.dart b/lib/blocs/group/group_bloc.dart index 44bcf82..68e80de 100644 --- a/lib/blocs/group/group_bloc.dart +++ b/lib/blocs/group/group_bloc.dart @@ -1,104 +1,140 @@ import 'dart:async'; import 'package:flutter_bloc/flutter_bloc.dart'; -import '../../services/group_service.dart'; import 'group_event.dart'; import 'group_state.dart'; -import '../../data/models/group.dart'; +import '../../repositories/group_repository.dart'; class GroupBloc extends Bloc { - final GroupService _groupService; + final GroupRepository _repository; StreamSubscription? _groupsSubscription; - GroupBloc({GroupService? groupService}) - : _groupService = groupService ?? GroupService(), - super(GroupInitial()) { - on(_onLoadRequested); - on<_GroupUpdated>(_onGroupUpdated); - on(_onCreateRequested); - on(_onUpdateRequested); - on(_onDeleteRequested); - on(_onMemberAddRequested); - on(_onMemberRemoveRequested); + GroupBloc(this._repository) : super(GroupInitial()) { + on(_onLoadGroupsByUserId); + on(_onLoadGroupsByTrip); + on(_onCreateGroup); + on(_onCreateGroupWithMembers); + on(_onAddMemberToGroup); + on(_onRemoveMemberFromGroup); + on(_onUpdateGroup); + on(_onDeleteGroup); } - Future _onLoadRequested( - GroupLoadRequested event, + // NOUVEAU : Charger les groupes par userId + Future _onLoadGroupsByUserId( + LoadGroupsByUserId event, Emitter emit, ) async { emit(GroupLoading()); await _groupsSubscription?.cancel(); - _groupsSubscription = _groupService.getGroupsStreamByUser(event.userId).listen( - (groups) => add(_GroupUpdated(groups: groups)), - onError: (error) => emit(GroupError(message: error.toString())), + _groupsSubscription = _repository.getGroupsByUserId(event.userId).listen( + (groups) => emit(GroupsLoaded(groups)), + onError: (error) => emit(GroupError(error.toString())), ); } - Future _onGroupUpdated( - _GroupUpdated event, - Emitter emit, - ) async { - emit(GroupLoaded(groups: event.groups)); - } - - Future _onCreateRequested( - GroupCreateRequested event, + // Charger les groupes d'un voyage (conservé) + Future _onLoadGroupsByTrip( + LoadGroupsByTrip event, Emitter emit, ) async { try { - await _groupService.createGroup(event.group); - emit(const GroupOperationSuccess(message: 'Groupe créé avec succès')); + emit(GroupLoading()); + final group = await _repository.getGroupByTripId(event.tripId); + if (group != null) { + emit(GroupsLoaded([group])); + } else { + emit(const GroupsLoaded([])); + } } catch (e) { - emit(GroupError(message: e.toString())); + emit(GroupError(e.toString())); } } - Future _onUpdateRequested( - GroupUpdateRequested event, + // Créer un groupe simple + Future _onCreateGroup( + CreateGroup event, Emitter emit, ) async { try { - await _groupService.updateGroup(event.group); - emit(const GroupOperationSuccess(message: 'Groupe mis à jour')); + emit(GroupLoading()); + await _repository.createGroupWithMembers( + group: event.group, + members: [], + ); + emit(const GroupOperationSuccess('Groupe créé avec succès')); } catch (e) { - emit(GroupError(message: e.toString())); + emit(GroupError('Erreur lors de la création: $e')); } } - Future _onDeleteRequested( - GroupDeleteRequested event, + // Créer un groupe avec ses membres + Future _onCreateGroupWithMembers( + CreateGroupWithMembers event, Emitter emit, ) async { try { - await _groupService.deleteGroup(event.groupId); - emit(const GroupOperationSuccess(message: 'Groupe supprimé')); + emit(GroupLoading()); + await _repository.createGroupWithMembers( + group: event.group, + members: event.members, + ); + emit(const GroupOperationSuccess('Groupe créé avec succès')); } catch (e) { - emit(GroupError(message: e.toString())); + emit(GroupError('Erreur lors de la création: $e')); } } - Future _onMemberAddRequested( - GroupMemberAddRequested event, + // Ajouter un membre + Future _onAddMemberToGroup( + AddMemberToGroup event, Emitter emit, ) async { try { - await _groupService.addMemberToGroup(event.groupId, event.memberId); - emit(const GroupOperationSuccess(message: 'Membre ajouté')); + await _repository.addMember(event.groupId, event.member); + emit(const GroupOperationSuccess('Membre ajouté')); } catch (e) { - emit(GroupError(message: e.toString())); + emit(GroupError('Erreur lors de l\'ajout: $e')); } } - Future _onMemberRemoveRequested( - GroupMemberRemoveRequested event, + // Supprimer un membre + Future _onRemoveMemberFromGroup( + RemoveMemberFromGroup event, Emitter emit, ) async { try { - await _groupService.removeMemberFromGroup(event.groupId, event.memberId); - emit(const GroupOperationSuccess(message: 'Membre retiré')); + await _repository.removeMember(event.groupId, event.userId); + emit(const GroupOperationSuccess('Membre supprimé')); } catch (e) { - emit(GroupError(message: e.toString())); + emit(GroupError('Erreur lors de la suppression: $e')); + } + } + + // Mettre à jour un groupe + Future _onUpdateGroup( + UpdateGroup event, + Emitter emit, + ) async { + try { + await _repository.updateGroup(event.groupId, event.group); + emit(const GroupOperationSuccess('Groupe mis à jour')); + } catch (e) { + emit(GroupError('Erreur lors de la mise à jour: $e')); + } + } + + // Supprimer un groupe + Future _onDeleteGroup( + DeleteGroup event, + Emitter emit, + ) async { + try { + await _repository.deleteGroup(event.groupId); + emit(const GroupOperationSuccess('Groupe supprimé')); + } catch (e) { + emit(GroupError('Erreur lors de la suppression: $e')); } } @@ -107,14 +143,4 @@ class GroupBloc extends Bloc { _groupsSubscription?.cancel(); return super.close(); } -} - -// Événement interne pour les mises à jour du stream -class _GroupUpdated extends GroupEvent { - final List groups; - - const _GroupUpdated({required this.groups}); - - @override - List get props => [groups]; -} +} \ No newline at end of file diff --git a/lib/blocs/group/group_event.dart b/lib/blocs/group/group_event.dart index 677f19e..cf6d594 100644 --- a/lib/blocs/group/group_event.dart +++ b/lib/blocs/group/group_event.dart @@ -1,5 +1,6 @@ import 'package:equatable/equatable.dart'; import '../../data/models/group.dart'; +import '../../data/models/group_member.dart'; abstract class GroupEvent extends Equatable { const GroupEvent(); @@ -8,64 +9,89 @@ abstract class GroupEvent extends Equatable { List get props => []; } -class GroupLoadRequested extends GroupEvent { +// NOUVEAU : Charger les groupes par userId +class LoadGroupsByUserId extends GroupEvent { final String userId; - const GroupLoadRequested({required this.userId}); + const LoadGroupsByUserId(this.userId); @override List get props => [userId]; } -class GroupCreateRequested extends GroupEvent { +// Charger les groupes d'un voyage (conservé pour compatibilité) +class LoadGroupsByTrip extends GroupEvent { + final String tripId; + + const LoadGroupsByTrip(this.tripId); + + @override + List get props => [tripId]; +} + +// Créer un groupe simple +class CreateGroup extends GroupEvent { final Group group; - const GroupCreateRequested({required this.group}); + const CreateGroup(this.group); @override List get props => [group]; } -class GroupUpdateRequested extends GroupEvent { +// Créer un groupe avec ses membres +class CreateGroupWithMembers extends GroupEvent { final Group group; + final List members; - const GroupUpdateRequested({required this.group}); + const CreateGroupWithMembers({ + required this.group, + required this.members, + }); @override - List get props => [group]; + List get props => [group, members]; } -class GroupDeleteRequested extends GroupEvent { +// Ajouter un membre +class AddMemberToGroup extends GroupEvent { + final String groupId; + final GroupMember member; + + const AddMemberToGroup(this.groupId, this.member); + + @override + List get props => [groupId, member]; +} + +// Supprimer un membre +class RemoveMemberFromGroup extends GroupEvent { + final String groupId; + final String userId; + + const RemoveMemberFromGroup(this.groupId, this.userId); + + @override + List get props => [groupId, userId]; +} + +// Mettre à jour un groupe +class UpdateGroup extends GroupEvent { + final String groupId; + final Group group; + + const UpdateGroup(this.groupId, this.group); + + @override + List get props => [groupId, group]; +} + +// Supprimer un groupe +class DeleteGroup extends GroupEvent { final String groupId; - const GroupDeleteRequested({required this.groupId}); + const DeleteGroup(this.groupId); @override List get props => [groupId]; -} - -class GroupMemberAddRequested extends GroupEvent { - final String groupId; - final String memberId; - - const GroupMemberAddRequested({ - required this.groupId, - required this.memberId, - }); - - @override - List get props => [groupId, memberId]; -} - -class GroupMemberRemoveRequested extends GroupEvent { - final String groupId; - final String memberId; - - const GroupMemberRemoveRequested({ - required this.groupId, - required this.memberId, - }); - - @override - List get props => [groupId, memberId]; -} +} \ No newline at end of file diff --git a/lib/blocs/group/group_state.dart b/lib/blocs/group/group_state.dart index 220aba3..bbe13d2 100644 --- a/lib/blocs/group/group_state.dart +++ b/lib/blocs/group/group_state.dart @@ -8,33 +8,38 @@ abstract class GroupState extends Equatable { List get props => []; } +// État initial class GroupInitial extends GroupState {} +// Chargement class GroupLoading extends GroupState {} -class GroupLoaded extends GroupState { +// Groupes chargés +class GroupsLoaded extends GroupState { final List groups; - const GroupLoaded({required this.groups}); + const GroupsLoaded(this.groups); @override List get props => [groups]; } +// Succès d'une opération class GroupOperationSuccess extends GroupState { final String message; - const GroupOperationSuccess({required this.message}); + const GroupOperationSuccess(this.message); @override List get props => [message]; } +// Erreur class GroupError extends GroupState { final String message; - const GroupError({required this.message}); + const GroupError(this.message); @override List get props => [message]; -} +} \ No newline at end of file diff --git a/lib/components/group/group_content.dart b/lib/components/group/group_content.dart index 1d7a1e8..4724307 100644 --- a/lib/components/group/group_content.dart +++ b/lib/components/group/group_content.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:travel_mate/data/models/group.dart'; import '../../blocs/user/user_bloc.dart'; import '../../blocs/user/user_state.dart' as user_state; import '../../blocs/group/group_bloc.dart'; import '../../blocs/group/group_state.dart'; import '../../blocs/group/group_event.dart'; +import '../../data/models/group.dart'; class GroupContent extends StatefulWidget { const GroupContent({super.key}); @@ -18,13 +18,14 @@ class _GroupContentState extends State { @override void initState() { super.initState(); - _loadGroupsIfUserLoaded(); + _loadInitialData(); } - void _loadGroupsIfUserLoaded() { + void _loadInitialData() { final userState = context.read().state; if (userState is user_state.UserLoaded) { - context.read().add(GroupLoadRequested(userId: userState.user.id)); + // Charger les groupes de l'utilisateur connecté + context.read().add(LoadGroupsByUserId(userState.user.id)); } } @@ -33,7 +34,7 @@ class _GroupContentState extends State { return BlocBuilder( builder: (context, userState) { if (userState is user_state.UserLoading) { - return Scaffold( + return const Scaffold( body: Center(child: CircularProgressIndicator()), ); } @@ -44,8 +45,8 @@ class _GroupContentState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.error, size: 64, color: Colors.red), - SizedBox(height: 16), + const Icon(Icons.error, size: 64, color: Colors.red), + const SizedBox(height: 16), Text('Erreur: ${userState.message}'), ], ), @@ -54,18 +55,13 @@ class _GroupContentState extends State { } if (userState is! user_state.UserLoaded) { - return Scaffold( + return const Scaffold( body: Center(child: Text('Utilisateur non connecté')), ); } final user = userState.user; - // Charger les groupes si ce n'est pas déjà fait - if (context.read().state is GroupInitial) { - context.read().add(GroupLoadRequested(userId: user.id)); - } - return BlocConsumer( listener: (context, groupState) { if (groupState is GroupOperationSuccess) { @@ -75,8 +71,8 @@ class _GroupContentState extends State { backgroundColor: Colors.green, ), ); - // Recharger les groupes - context.read().add(GroupLoadRequested(userId: user.id)); + // Recharger les groupes après une opération réussie + context.read().add(LoadGroupsByUserId(user.id)); } else if (groupState is GroupError) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -90,7 +86,7 @@ class _GroupContentState extends State { return Scaffold( body: RefreshIndicator( onRefresh: () async { - context.read().add(GroupLoadRequested(userId: user.id)); + context.read().add(LoadGroupsByUserId(user.id)); }, child: SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), @@ -98,29 +94,39 @@ class _GroupContentState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + // En-tête Text( - 'Vos Groupes', + 'Mes groupes', style: TextStyle( - fontSize: 24, + fontSize: 28, fontWeight: FontWeight.bold, - color: Theme.of(context).brightness == Brightness.dark - ? Colors.white - : Colors.black, + color: Theme.of(context).brightness == Brightness.dark + ? Colors.white + : Colors.black, ), ), - const SizedBox(height: 20), - + const SizedBox(height: 8), + Text( + 'Discutez avec les participants de vos voyages', + style: TextStyle( + fontSize: 14, + color: Colors.grey[600], + ), + ), + const SizedBox(height: 24), + + // Contenu principal if (groupState is GroupLoading) _buildLoadingState() else if (groupState is GroupError) _buildErrorState(groupState.message, user.id) - else if (groupState is GroupLoaded) + else if (groupState is GroupsLoaded) groupState.groups.isEmpty - ? _buildEmptyState() - : _buildGroupList(groupState.groups) + ? _buildEmptyState() + : _buildGroupGrid(groupState.groups) else _buildEmptyState(), - + const SizedBox(height: 80), ], ), @@ -136,7 +142,7 @@ class _GroupContentState extends State { Widget _buildLoadingState() { return const Center( child: Padding( - padding: EdgeInsets.all(16.0), + padding: EdgeInsets.all(32), child: CircularProgressIndicator(), ), ); @@ -145,25 +151,32 @@ class _GroupContentState extends State { Widget _buildErrorState(String error, String userId) { return Center( child: Padding( - padding: EdgeInsets.all(32), + padding: const EdgeInsets.all(32), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.error, size: 64, color: Colors.red), const SizedBox(height: 16), - const Text('Erreur lors du chargement des groupes.'), + const Text( + 'Erreur lors du chargement des groupes', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), const SizedBox(height: 8), Text( error, textAlign: TextAlign.center, - style: TextStyle(fontSize: 12, color: Colors.grey), + style: const TextStyle(fontSize: 12, color: Colors.grey), ), const SizedBox(height: 16), - ElevatedButton( + ElevatedButton.icon( onPressed: () { - context.read().add(GroupLoadRequested(userId: userId)); + context.read().add(LoadGroupsByUserId(userId)); }, - child: const Text('Réessayer'), + icon: const Icon(Icons.refresh), + label: const Text('Réessayer'), ), ], ), @@ -172,34 +185,174 @@ class _GroupContentState extends State { } Widget _buildEmptyState() { - return const Center( - child: Text( - 'Aucun groupe disponible. Créez ou rejoignez un voyage pour commencer à discuter!', - style: TextStyle(fontSize: 16, color: Colors.grey), - textAlign: TextAlign.center, + return Center( + child: Padding( + padding: const EdgeInsets.all(32), + child: Column( + children: [ + Icon( + Icons.forum_outlined, + size: 80, + color: Colors.grey[400], + ), + const SizedBox(height: 16), + const Text( + 'Aucun groupe', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Text( + 'Les groupes sont créés automatiquement lorsque vous créez ou rejoignez un voyage', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + color: Colors.grey[600], + ), + ), + ], + ), ), ); } - Widget _buildGroupList(List groups) { - return Column( - children: groups.map((group) => _buildGroupCard(group)).toList(), + Widget _buildGroupGrid(List groups) { + return GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + childAspectRatio: 0.85, + ), + itemCount: groups.length, + itemBuilder: (context, index) => _buildGroupCard(groups[index]), ); } Widget _buildGroupCard(Group group) { + final isDarkMode = Theme.of(context).brightness == Brightness.dark; + final colors = [ + Colors.blue, + Colors.purple, + Colors.green, + Colors.orange, + Colors.teal, + Colors.pink, + ]; + final color = colors[group.name.hashCode.abs() % colors.length]; + return Card( - margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), - child: ListTile( - leading: CircleAvatar( - child: Text(group.name.isNotEmpty ? group.name[0] : '?'), + elevation: 3, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: InkWell( + onTap: () => _openGroupChat(group), + borderRadius: BorderRadius.circular(16), + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + color.withOpacity(0.1), + color.withOpacity(0.05), + ], + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Avatar du groupe + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: color.withOpacity(0.2), + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + Icons.group, + color: color, + size: 32, + ), + ), + + const Spacer(), + + // Nom du groupe + Text( + group.name, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: isDarkMode ? Colors.white : Colors.black87, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + + const SizedBox(height: 8), + + // Nombre de membres + Row( + children: [ + Icon( + Icons.people, + size: 16, + color: Colors.grey[600], + ), + const SizedBox(width: 4), + Text( + '${group.members.length} membre${group.members.length > 1 ? 's' : ''}', + style: TextStyle( + fontSize: 13, + color: Colors.grey[600], + ), + ), + ], + ), + + const SizedBox(height: 4), + + // Afficher les premiers membres + if (group.members.isNotEmpty) + Text( + group.members.take(3).map((m) => m.pseudo).join(', ') + + (group.members.length > 3 ? '...' : ''), + style: TextStyle( + fontSize: 11, + color: Colors.grey[500], + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), ), - title: Text(group.name), - subtitle: Text('${group.members.length} membres'), - onTap: () { - // Logique pour ouvrir le chat de groupe - }, ), ); } -} + + void _openGroupChat(Group group) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Ouverture du chat: ${group.name}'), + duration: const Duration(seconds: 1), + ), + ); + + // TODO: Navigation vers la page de chat + // Navigator.push( + // context, + // MaterialPageRoute( + // builder: (context) => GroupChatPage(group: group), + // ), + // ); + } +} \ 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 a9baa07..d59b5b2 100644 --- a/lib/components/home/create_trip_content.dart +++ b/lib/components/home/create_trip_content.dart @@ -8,6 +8,7 @@ import '../../blocs/trip/trip_event.dart'; import '../../blocs/group/group_bloc.dart'; import '../../blocs/group/group_event.dart'; import '../../data/models/group.dart'; +import '../../data/models/group_member.dart'; import '../../services/user_service.dart'; class CreateTripContent extends StatefulWidget { @@ -426,10 +427,9 @@ class _CreateTripContentState extends State { }); try { - // Convertir les emails en IDs - List participantIds = await _changeUserEmailById(_participants); + final participantsData = await _getParticipantsData(_participants); + List participantIds = participantsData.map((p) => p['id'] as String).toList(); - // Ajouter le créateur if (!participantIds.contains(currentUser.id)) { participantIds.insert(0, currentUser.id); } @@ -449,20 +449,38 @@ class _CreateTripContentState extends State { updatedAt: DateTime.now(), ); - // Créer le groupe + context.read().add(TripCreateRequested(trip: trip)); + + // Attendre que le trip soit créé (simplifié) + await Future.delayed(Duration(milliseconds: 500)); + final group = Group( - id: '', + id: '', name: _titleController.text.trim(), - members: participantIds, + tripId: '', + createdBy: currentUser.id, ); - // Utiliser les BLoCs pour créer - context.read().add(TripCreateRequested(trip: trip)); - context.read().add(GroupCreateRequested(group: group)); + final groupMembers = [ + GroupMember( + userId: currentUser.id, + firstName: currentUser.prenom, + pseudo: currentUser.prenom, // Par défaut = prénom + role: 'admin', + ), + ...participantsData.map((p) => GroupMember( + userId: p['id'] as String, + firstName: p['firstName'] as String, + pseudo: p['firstName'] as String, // Par défaut = prénom + role: 'member', + )), + ]; - if (mounted) { - Navigator.pop(context, true); - } + + context.read().add(CreateGroupWithMembers( + group: group, + members: groupMembers, + )); } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( @@ -481,14 +499,24 @@ class _CreateTripContentState extends State { } } - Future> _changeUserEmailById(List participants) async { - List ids = []; + // ...existing code... - for (String email in participants) { + // Récupérer les IDs et prénoms des participants + Future>> _getParticipantsData(List emails) async { + List> participantsData = []; + + for (String email in emails) { try { - final id = await _userService.getUserIdByEmail(email); - if (id != null) { - ids.add(id); + final userId = await _userService.getUserIdByEmail(email); + if (userId != null) { + // Récupérer le prénom de l'utilisateur + final userDoc = await _userService.getUserById(userId); + final firstName = userDoc?.prenom ?? 'Utilisateur'; + + participantsData.add({ + 'id': userId, + 'firstName': firstName, + }); } else { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( @@ -500,10 +528,10 @@ class _CreateTripContentState extends State { } } } catch (e) { - print('Erreur: $e'); + print('Erreur lors de la récupération de l\'utilisateur: $e'); } } - return ids; + return participantsData; } } \ No newline at end of file diff --git a/lib/data/models/group.dart b/lib/data/models/group.dart index 9137d6d..94efec7 100644 --- a/lib/data/models/group.dart +++ b/lib/data/models/group.dart @@ -1,26 +1,65 @@ +import 'group_member.dart'; + class Group { - final String? id; + final String id; // ID obligatoire maintenant final String name; - final List members; + final String tripId; + final String createdBy; + final DateTime createdAt; + final DateTime updatedAt; + final List members; Group({ - this.id, + required this.id, // Obligatoire required this.name, - required this.members, - }); + required this.tripId, + required this.createdBy, + DateTime? createdAt, + DateTime? updatedAt, + List? members, + }) : createdAt = createdAt ?? DateTime.now(), + updatedAt = updatedAt ?? DateTime.now(), + members = members ?? []; - factory Group.fromMap(Map data, String documentId) { + factory Group.fromMap(Map map, String id) { return Group( - id: documentId, - name: data['name'] ?? '', - members: List.from(data['members'] ?? []), + id: id, + name: map['name'] ?? '', + tripId: map['tripId'] ?? '', + createdBy: map['createdBy'] ?? '', + createdAt: DateTime.fromMillisecondsSinceEpoch(map['createdAt'] ?? 0), + updatedAt: DateTime.fromMillisecondsSinceEpoch(map['updatedAt'] ?? 0), + members: [], ); } Map toMap() { return { 'name': name, - 'members': members, + 'tripId': tripId, + 'createdBy': createdBy, + 'createdAt': createdAt.millisecondsSinceEpoch, + 'updatedAt': updatedAt.millisecondsSinceEpoch, }; } + + Group copyWith({ + String? id, + String? name, + String? tripId, + String? createdBy, + DateTime? createdAt, + DateTime? updatedAt, + List? members, + }) { + return Group( + id: id ?? this.id, + name: name ?? this.name, + tripId: tripId ?? this.tripId, + createdBy: createdBy ?? this.createdBy, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + members: members ?? this.members, + ); + } } \ No newline at end of file diff --git a/lib/data/models/group_member.dart b/lib/data/models/group_member.dart new file mode 100644 index 0000000..c63b70d --- /dev/null +++ b/lib/data/models/group_member.dart @@ -0,0 +1,51 @@ +class GroupMember { + final String userId; + final String firstName; + final String pseudo; // Pseudo du membre (par défaut = prénom) + final String role; // 'admin' ou 'member' + final DateTime joinedAt; + + GroupMember({ + required this.userId, + required this.firstName, + String? pseudo, + this.role = 'member', + DateTime? joinedAt, + }) : pseudo = pseudo ?? firstName, // Par défaut, pseudo = prénom + joinedAt = joinedAt ?? DateTime.now(); + + factory GroupMember.fromMap(Map map, String userId) { + return GroupMember( + userId: userId, + firstName: map['firstName'] ?? '', + pseudo: map['pseudo'] ?? map['firstName'] ?? '', + role: map['role'] ?? 'member', + joinedAt: DateTime.fromMillisecondsSinceEpoch(map['joinedAt'] ?? 0), + ); + } + + Map toMap() { + return { + 'firstName': firstName, + 'pseudo': pseudo, + 'role': role, + 'joinedAt': joinedAt.millisecondsSinceEpoch, + }; + } + + GroupMember copyWith({ + String? userId, + String? firstName, + String? pseudo, + String? role, + DateTime? joinedAt, + }) { + return GroupMember( + userId: userId ?? this.userId, + firstName: firstName ?? this.firstName, + pseudo: pseudo ?? this.pseudo, + role: role ?? this.role, + joinedAt: joinedAt ?? this.joinedAt, + ); + } +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 851d143..bec3373 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -13,6 +13,7 @@ import 'blocs/trip/trip_bloc.dart'; import 'repositories/auth_repository.dart'; import 'repositories/trip_repository.dart'; import 'repositories/user_repository.dart'; +import 'repositories/group_repository.dart'; import 'pages/login.dart'; import 'pages/home.dart'; import 'pages/signup.dart'; @@ -41,6 +42,9 @@ class MyApp extends StatelessWidget { RepositoryProvider( create: (context) => TripRepository(), ), + RepositoryProvider( + create: (context) => GroupRepository(), + ), ], child: MultiBlocProvider( providers: [ @@ -52,7 +56,9 @@ class MyApp extends StatelessWidget { authRepository: context.read(), )..add(AuthCheckRequested()), ), - BlocProvider(create: (context) => GroupBloc()), + BlocProvider(create: (context) => GroupBloc( + context.read(), + )), BlocProvider(create: (context) => TripBloc( tripRepository: context.read(), ), diff --git a/lib/repositories/group_repository.dart b/lib/repositories/group_repository.dart new file mode 100644 index 0000000..297774c --- /dev/null +++ b/lib/repositories/group_repository.dart @@ -0,0 +1,186 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import '../data/models/group.dart'; +import '../data/models/group_member.dart'; + +class GroupRepository { + final FirebaseFirestore _firestore = FirebaseFirestore.instance; + + CollectionReference get _groupsCollection => _firestore.collection('groups'); + + CollectionReference _membersCollection(String groupId) { + return _groupsCollection.doc(groupId).collection('members'); + } + + // Créer un groupe avec ses membres (avec ID du trip) + Future createGroupWithMembers({ + required Group group, + required List members, + }) async { + try { + return await _firestore.runTransaction((transaction) async { + // Créer le document avec un ID généré + final groupRef = _groupsCollection.doc(); + + // Ajouter l'ID dans les données + final groupData = group.toMap(); + transaction.set(groupRef, groupData); + + // Ajouter tous les membres + for (var member in members) { + final memberRef = groupRef.collection('members').doc(member.userId); + transaction.set(memberRef, member.toMap()); + } + + return groupRef.id; + }); + } catch (e) { + throw Exception('Erreur lors de la création du groupe: $e'); + } + } + + // NOUVEAU : Récupérer les groupes où l'utilisateur est membre + Stream> getGroupsByUserId(String userId) { + return _groupsCollection.snapshots().asyncMap((snapshot) async { + List userGroups = []; + + for (var groupDoc in snapshot.docs) { + try { + // Vérifier si l'utilisateur est dans la sous-collection members + final memberDoc = await groupDoc.reference + .collection('members') + .doc(userId) + .get(); + + if (memberDoc.exists) { + // Charger le groupe avec tous ses membres + final group = Group.fromMap( + groupDoc.data() as Map, + groupDoc.id, + ); + final members = await getGroupMembers(groupDoc.id); + userGroups.add(group.copyWith(members: members)); + } + } catch (e) { + print('Erreur lors du traitement du groupe ${groupDoc.id}: $e'); + } + } + + return userGroups; + }); + } + + // Récupérer un groupe par son ID avec ses membres + Future getGroupById(String groupId) async { + try { + final doc = await _groupsCollection.doc(groupId).get(); + + if (!doc.exists) return null; + + final group = Group.fromMap(doc.data() as Map, doc.id); + final members = await getGroupMembers(groupId); + + return group.copyWith(members: members); + } catch (e) { + throw Exception('Erreur lors de la récupération du groupe: $e'); + } + } + + // Récupérer un groupe par tripId + Future getGroupByTripId(String tripId) async { + try { + final querySnapshot = await _groupsCollection + .where('tripId', isEqualTo: tripId) + .limit(1) + .get(); + + if (querySnapshot.docs.isEmpty) return null; + + final doc = querySnapshot.docs.first; + final group = Group.fromMap(doc.data() as Map, doc.id); + final members = await getGroupMembers(doc.id); + + return group.copyWith(members: members); + } catch (e) { + throw Exception('Erreur lors de la récupération du groupe: $e'); + } + } + + // Récupérer les membres d'un groupe + Future> getGroupMembers(String groupId) async { + try { + final snapshot = await _membersCollection(groupId).get(); + + return snapshot.docs + .map((doc) => GroupMember.fromMap( + doc.data() as Map, + doc.id, + )) + .toList(); + } catch (e) { + throw Exception('Erreur lors de la récupération des membres: $e'); + } + } + + // Ajouter un membre + Future addMember(String groupId, GroupMember member) async { + try { + await _membersCollection(groupId).doc(member.userId).set(member.toMap()); + + await _groupsCollection.doc(groupId).update({ + 'updatedAt': DateTime.now().millisecondsSinceEpoch, + }); + } catch (e) { + throw Exception('Erreur lors de l\'ajout du membre: $e'); + } + } + + // Supprimer un membre + Future removeMember(String groupId, String userId) async { + try { + await _membersCollection(groupId).doc(userId).delete(); + + await _groupsCollection.doc(groupId).update({ + 'updatedAt': DateTime.now().millisecondsSinceEpoch, + }); + } catch (e) { + throw Exception('Erreur lors de la suppression du membre: $e'); + } + } + + // Mettre à jour un groupe + Future updateGroup(String groupId, Group group) async { + try { + await _groupsCollection.doc(groupId).update( + group.toMap()..['updatedAt'] = DateTime.now().millisecondsSinceEpoch, + ); + } catch (e) { + throw Exception('Erreur lors de la mise à jour du groupe: $e'); + } + } + + // Supprimer un groupe + Future deleteGroup(String groupId) async { + try { + final membersSnapshot = await _membersCollection(groupId).get(); + for (var doc in membersSnapshot.docs) { + await doc.reference.delete(); + } + + await _groupsCollection.doc(groupId).delete(); + } catch (e) { + throw Exception('Erreur lors de la suppression du groupe: $e'); + } + } + + // Stream des membres en temps réel + Stream> watchGroupMembers(String groupId) { + return _membersCollection(groupId).snapshots().map( + (snapshot) => snapshot.docs + .map((doc) => GroupMember.fromMap( + doc.data() as Map, + doc.id, + )) + .toList(), + ); + } +} \ No newline at end of file