feat: Implement group management with BLoC pattern; add GroupBloc, GroupRepository, and related models

NOT FUNCTIONNAL
This commit is contained in:
Dayron
2025-10-14 23:53:20 +02:00
parent 2eedb26778
commit fc403e5d26
10 changed files with 708 additions and 185 deletions

3
devtools_options.yaml Normal file
View File

@@ -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:

View File

@@ -1,104 +1,140 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import '../../services/group_service.dart';
import 'group_event.dart'; import 'group_event.dart';
import 'group_state.dart'; import 'group_state.dart';
import '../../data/models/group.dart'; import '../../repositories/group_repository.dart';
class GroupBloc extends Bloc<GroupEvent, GroupState> { class GroupBloc extends Bloc<GroupEvent, GroupState> {
final GroupService _groupService; final GroupRepository _repository;
StreamSubscription? _groupsSubscription; StreamSubscription? _groupsSubscription;
GroupBloc({GroupService? groupService}) GroupBloc(this._repository) : super(GroupInitial()) {
: _groupService = groupService ?? GroupService(), on<LoadGroupsByUserId>(_onLoadGroupsByUserId);
super(GroupInitial()) { on<LoadGroupsByTrip>(_onLoadGroupsByTrip);
on<GroupLoadRequested>(_onLoadRequested); on<CreateGroup>(_onCreateGroup);
on<_GroupUpdated>(_onGroupUpdated); on<CreateGroupWithMembers>(_onCreateGroupWithMembers);
on<GroupCreateRequested>(_onCreateRequested); on<AddMemberToGroup>(_onAddMemberToGroup);
on<GroupUpdateRequested>(_onUpdateRequested); on<RemoveMemberFromGroup>(_onRemoveMemberFromGroup);
on<GroupDeleteRequested>(_onDeleteRequested); on<UpdateGroup>(_onUpdateGroup);
on<GroupMemberAddRequested>(_onMemberAddRequested); on<DeleteGroup>(_onDeleteGroup);
on<GroupMemberRemoveRequested>(_onMemberRemoveRequested);
} }
Future<void> _onLoadRequested( // NOUVEAU : Charger les groupes par userId
GroupLoadRequested event, Future<void> _onLoadGroupsByUserId(
LoadGroupsByUserId event,
Emitter<GroupState> emit, Emitter<GroupState> emit,
) async { ) async {
emit(GroupLoading()); emit(GroupLoading());
await _groupsSubscription?.cancel(); await _groupsSubscription?.cancel();
_groupsSubscription = _groupService.getGroupsStreamByUser(event.userId).listen( _groupsSubscription = _repository.getGroupsByUserId(event.userId).listen(
(groups) => add(_GroupUpdated(groups: groups)), (groups) => emit(GroupsLoaded(groups)),
onError: (error) => emit(GroupError(message: error.toString())), onError: (error) => emit(GroupError(error.toString())),
); );
} }
Future<void> _onGroupUpdated( // Charger les groupes d'un voyage (conservé)
_GroupUpdated event, Future<void> _onLoadGroupsByTrip(
Emitter<GroupState> emit, LoadGroupsByTrip event,
) async {
emit(GroupLoaded(groups: event.groups));
}
Future<void> _onCreateRequested(
GroupCreateRequested event,
Emitter<GroupState> emit, Emitter<GroupState> emit,
) async { ) async {
try { try {
await _groupService.createGroup(event.group); emit(GroupLoading());
emit(const GroupOperationSuccess(message: 'Groupe créé avec succès')); final group = await _repository.getGroupByTripId(event.tripId);
if (group != null) {
emit(GroupsLoaded([group]));
} else {
emit(const GroupsLoaded([]));
}
} catch (e) { } catch (e) {
emit(GroupError(message: e.toString())); emit(GroupError(e.toString()));
} }
} }
Future<void> _onUpdateRequested( // Créer un groupe simple
GroupUpdateRequested event, Future<void> _onCreateGroup(
CreateGroup event,
Emitter<GroupState> emit, Emitter<GroupState> emit,
) async { ) async {
try { try {
await _groupService.updateGroup(event.group); emit(GroupLoading());
emit(const GroupOperationSuccess(message: 'Groupe mis à jour')); await _repository.createGroupWithMembers(
group: event.group,
members: [],
);
emit(const GroupOperationSuccess('Groupe créé avec succès'));
} catch (e) { } catch (e) {
emit(GroupError(message: e.toString())); emit(GroupError('Erreur lors de la création: $e'));
} }
} }
Future<void> _onDeleteRequested( // Créer un groupe avec ses membres
GroupDeleteRequested event, Future<void> _onCreateGroupWithMembers(
CreateGroupWithMembers event,
Emitter<GroupState> emit, Emitter<GroupState> emit,
) async { ) async {
try { try {
await _groupService.deleteGroup(event.groupId); emit(GroupLoading());
emit(const GroupOperationSuccess(message: 'Groupe supprimé')); await _repository.createGroupWithMembers(
group: event.group,
members: event.members,
);
emit(const GroupOperationSuccess('Groupe créé avec succès'));
} catch (e) { } catch (e) {
emit(GroupError(message: e.toString())); emit(GroupError('Erreur lors de la création: $e'));
} }
} }
Future<void> _onMemberAddRequested( // Ajouter un membre
GroupMemberAddRequested event, Future<void> _onAddMemberToGroup(
AddMemberToGroup event,
Emitter<GroupState> emit, Emitter<GroupState> emit,
) async { ) async {
try { try {
await _groupService.addMemberToGroup(event.groupId, event.memberId); await _repository.addMember(event.groupId, event.member);
emit(const GroupOperationSuccess(message: 'Membre ajouté')); emit(const GroupOperationSuccess('Membre ajouté'));
} catch (e) { } catch (e) {
emit(GroupError(message: e.toString())); emit(GroupError('Erreur lors de l\'ajout: $e'));
} }
} }
Future<void> _onMemberRemoveRequested( // Supprimer un membre
GroupMemberRemoveRequested event, Future<void> _onRemoveMemberFromGroup(
RemoveMemberFromGroup event,
Emitter<GroupState> emit, Emitter<GroupState> emit,
) async { ) async {
try { try {
await _groupService.removeMemberFromGroup(event.groupId, event.memberId); await _repository.removeMember(event.groupId, event.userId);
emit(const GroupOperationSuccess(message: 'Membre retiré')); emit(const GroupOperationSuccess('Membre supprimé'));
} catch (e) { } catch (e) {
emit(GroupError(message: e.toString())); emit(GroupError('Erreur lors de la suppression: $e'));
}
}
// Mettre à jour un groupe
Future<void> _onUpdateGroup(
UpdateGroup event,
Emitter<GroupState> 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<void> _onDeleteGroup(
DeleteGroup event,
Emitter<GroupState> 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<GroupEvent, GroupState> {
_groupsSubscription?.cancel(); _groupsSubscription?.cancel();
return super.close(); return super.close();
} }
} }
// Événement interne pour les mises à jour du stream
class _GroupUpdated extends GroupEvent {
final List<Group> groups;
const _GroupUpdated({required this.groups});
@override
List<Object?> get props => [groups];
}

View File

@@ -1,5 +1,6 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import '../../data/models/group.dart'; import '../../data/models/group.dart';
import '../../data/models/group_member.dart';
abstract class GroupEvent extends Equatable { abstract class GroupEvent extends Equatable {
const GroupEvent(); const GroupEvent();
@@ -8,64 +9,89 @@ abstract class GroupEvent extends Equatable {
List<Object?> get props => []; List<Object?> get props => [];
} }
class GroupLoadRequested extends GroupEvent { // NOUVEAU : Charger les groupes par userId
class LoadGroupsByUserId extends GroupEvent {
final String userId; final String userId;
const GroupLoadRequested({required this.userId}); const LoadGroupsByUserId(this.userId);
@override @override
List<Object?> get props => [userId]; List<Object?> 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<Object?> get props => [tripId];
}
// Créer un groupe simple
class CreateGroup extends GroupEvent {
final Group group; final Group group;
const GroupCreateRequested({required this.group}); const CreateGroup(this.group);
@override @override
List<Object?> get props => [group]; List<Object?> get props => [group];
} }
class GroupUpdateRequested extends GroupEvent { // Créer un groupe avec ses membres
class CreateGroupWithMembers extends GroupEvent {
final Group group; final Group group;
final List<GroupMember> members;
const GroupUpdateRequested({required this.group}); const CreateGroupWithMembers({
required this.group,
required this.members,
});
@override @override
List<Object?> get props => [group]; List<Object?> 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<Object?> 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<Object?> 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<Object?> get props => [groupId, group];
}
// Supprimer un groupe
class DeleteGroup extends GroupEvent {
final String groupId; final String groupId;
const GroupDeleteRequested({required this.groupId}); const DeleteGroup(this.groupId);
@override @override
List<Object?> get props => [groupId]; List<Object?> get props => [groupId];
} }
class GroupMemberAddRequested extends GroupEvent {
final String groupId;
final String memberId;
const GroupMemberAddRequested({
required this.groupId,
required this.memberId,
});
@override
List<Object?> get props => [groupId, memberId];
}
class GroupMemberRemoveRequested extends GroupEvent {
final String groupId;
final String memberId;
const GroupMemberRemoveRequested({
required this.groupId,
required this.memberId,
});
@override
List<Object?> get props => [groupId, memberId];
}

View File

@@ -8,33 +8,38 @@ abstract class GroupState extends Equatable {
List<Object?> get props => []; List<Object?> get props => [];
} }
// État initial
class GroupInitial extends GroupState {} class GroupInitial extends GroupState {}
// Chargement
class GroupLoading extends GroupState {} class GroupLoading extends GroupState {}
class GroupLoaded extends GroupState { // Groupes chargés
class GroupsLoaded extends GroupState {
final List<Group> groups; final List<Group> groups;
const GroupLoaded({required this.groups}); const GroupsLoaded(this.groups);
@override @override
List<Object?> get props => [groups]; List<Object?> get props => [groups];
} }
// Succès d'une opération
class GroupOperationSuccess extends GroupState { class GroupOperationSuccess extends GroupState {
final String message; final String message;
const GroupOperationSuccess({required this.message}); const GroupOperationSuccess(this.message);
@override @override
List<Object?> get props => [message]; List<Object?> get props => [message];
} }
// Erreur
class GroupError extends GroupState { class GroupError extends GroupState {
final String message; final String message;
const GroupError({required this.message}); const GroupError(this.message);
@override @override
List<Object?> get props => [message]; List<Object?> get props => [message];
} }

View File

@@ -1,11 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.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_bloc.dart';
import '../../blocs/user/user_state.dart' as user_state; import '../../blocs/user/user_state.dart' as user_state;
import '../../blocs/group/group_bloc.dart'; import '../../blocs/group/group_bloc.dart';
import '../../blocs/group/group_state.dart'; import '../../blocs/group/group_state.dart';
import '../../blocs/group/group_event.dart'; import '../../blocs/group/group_event.dart';
import '../../data/models/group.dart';
class GroupContent extends StatefulWidget { class GroupContent extends StatefulWidget {
const GroupContent({super.key}); const GroupContent({super.key});
@@ -18,13 +18,14 @@ class _GroupContentState extends State<GroupContent> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_loadGroupsIfUserLoaded(); _loadInitialData();
} }
void _loadGroupsIfUserLoaded() { void _loadInitialData() {
final userState = context.read<UserBloc>().state; final userState = context.read<UserBloc>().state;
if (userState is user_state.UserLoaded) { if (userState is user_state.UserLoaded) {
context.read<GroupBloc>().add(GroupLoadRequested(userId: userState.user.id)); // Charger les groupes de l'utilisateur connecté
context.read<GroupBloc>().add(LoadGroupsByUserId(userState.user.id));
} }
} }
@@ -33,7 +34,7 @@ class _GroupContentState extends State<GroupContent> {
return BlocBuilder<UserBloc, user_state.UserState>( return BlocBuilder<UserBloc, user_state.UserState>(
builder: (context, userState) { builder: (context, userState) {
if (userState is user_state.UserLoading) { if (userState is user_state.UserLoading) {
return Scaffold( return const Scaffold(
body: Center(child: CircularProgressIndicator()), body: Center(child: CircularProgressIndicator()),
); );
} }
@@ -44,8 +45,8 @@ class _GroupContentState extends State<GroupContent> {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Icon(Icons.error, size: 64, color: Colors.red), const Icon(Icons.error, size: 64, color: Colors.red),
SizedBox(height: 16), const SizedBox(height: 16),
Text('Erreur: ${userState.message}'), Text('Erreur: ${userState.message}'),
], ],
), ),
@@ -54,18 +55,13 @@ class _GroupContentState extends State<GroupContent> {
} }
if (userState is! user_state.UserLoaded) { if (userState is! user_state.UserLoaded) {
return Scaffold( return const Scaffold(
body: Center(child: Text('Utilisateur non connecté')), body: Center(child: Text('Utilisateur non connecté')),
); );
} }
final user = userState.user; final user = userState.user;
// Charger les groupes si ce n'est pas déjà fait
if (context.read<GroupBloc>().state is GroupInitial) {
context.read<GroupBloc>().add(GroupLoadRequested(userId: user.id));
}
return BlocConsumer<GroupBloc, GroupState>( return BlocConsumer<GroupBloc, GroupState>(
listener: (context, groupState) { listener: (context, groupState) {
if (groupState is GroupOperationSuccess) { if (groupState is GroupOperationSuccess) {
@@ -75,8 +71,8 @@ class _GroupContentState extends State<GroupContent> {
backgroundColor: Colors.green, backgroundColor: Colors.green,
), ),
); );
// Recharger les groupes // Recharger les groupes après une opération réussie
context.read<GroupBloc>().add(GroupLoadRequested(userId: user.id)); context.read<GroupBloc>().add(LoadGroupsByUserId(user.id));
} else if (groupState is GroupError) { } else if (groupState is GroupError) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
@@ -90,7 +86,7 @@ class _GroupContentState extends State<GroupContent> {
return Scaffold( return Scaffold(
body: RefreshIndicator( body: RefreshIndicator(
onRefresh: () async { onRefresh: () async {
context.read<GroupBloc>().add(GroupLoadRequested(userId: user.id)); context.read<GroupBloc>().add(LoadGroupsByUserId(user.id));
}, },
child: SingleChildScrollView( child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
@@ -98,29 +94,39 @@ class _GroupContentState extends State<GroupContent> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// En-tête
Text( Text(
'Vos Groupes', 'Mes groupes',
style: TextStyle( style: TextStyle(
fontSize: 24, fontSize: 28,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Theme.of(context).brightness == Brightness.dark color: Theme.of(context).brightness == Brightness.dark
? Colors.white ? Colors.white
: Colors.black, : 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) if (groupState is GroupLoading)
_buildLoadingState() _buildLoadingState()
else if (groupState is GroupError) else if (groupState is GroupError)
_buildErrorState(groupState.message, user.id) _buildErrorState(groupState.message, user.id)
else if (groupState is GroupLoaded) else if (groupState is GroupsLoaded)
groupState.groups.isEmpty groupState.groups.isEmpty
? _buildEmptyState() ? _buildEmptyState()
: _buildGroupList(groupState.groups) : _buildGroupGrid(groupState.groups)
else else
_buildEmptyState(), _buildEmptyState(),
const SizedBox(height: 80), const SizedBox(height: 80),
], ],
), ),
@@ -136,7 +142,7 @@ class _GroupContentState extends State<GroupContent> {
Widget _buildLoadingState() { Widget _buildLoadingState() {
return const Center( return const Center(
child: Padding( child: Padding(
padding: EdgeInsets.all(16.0), padding: EdgeInsets.all(32),
child: CircularProgressIndicator(), child: CircularProgressIndicator(),
), ),
); );
@@ -145,25 +151,32 @@ class _GroupContentState extends State<GroupContent> {
Widget _buildErrorState(String error, String userId) { Widget _buildErrorState(String error, String userId) {
return Center( return Center(
child: Padding( child: Padding(
padding: EdgeInsets.all(32), padding: const EdgeInsets.all(32),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
const Icon(Icons.error, size: 64, color: Colors.red), const Icon(Icons.error, size: 64, color: Colors.red),
const SizedBox(height: 16), 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), const SizedBox(height: 8),
Text( Text(
error, error,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle(fontSize: 12, color: Colors.grey), style: const TextStyle(fontSize: 12, color: Colors.grey),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
ElevatedButton( ElevatedButton.icon(
onPressed: () { onPressed: () {
context.read<GroupBloc>().add(GroupLoadRequested(userId: userId)); context.read<GroupBloc>().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<GroupContent> {
} }
Widget _buildEmptyState() { Widget _buildEmptyState() {
return const Center( return Center(
child: Text( child: Padding(
'Aucun groupe disponible. Créez ou rejoignez un voyage pour commencer à discuter!', padding: const EdgeInsets.all(32),
style: TextStyle(fontSize: 16, color: Colors.grey), child: Column(
textAlign: TextAlign.center, 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<Group> groups) { Widget _buildGroupGrid(List<Group> groups) {
return Column( return GridView.builder(
children: groups.map((group) => _buildGroupCard(group)).toList(), 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) { 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( return Card(
margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), elevation: 3,
child: ListTile( shape: RoundedRectangleBorder(
leading: CircleAvatar( borderRadius: BorderRadius.circular(16),
child: Text(group.name.isNotEmpty ? group.name[0] : '?'), ),
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),
// ),
// );
}
}

View File

@@ -8,6 +8,7 @@ import '../../blocs/trip/trip_event.dart';
import '../../blocs/group/group_bloc.dart'; import '../../blocs/group/group_bloc.dart';
import '../../blocs/group/group_event.dart'; import '../../blocs/group/group_event.dart';
import '../../data/models/group.dart'; import '../../data/models/group.dart';
import '../../data/models/group_member.dart';
import '../../services/user_service.dart'; import '../../services/user_service.dart';
class CreateTripContent extends StatefulWidget { class CreateTripContent extends StatefulWidget {
@@ -426,10 +427,9 @@ class _CreateTripContentState extends State<CreateTripContent> {
}); });
try { try {
// Convertir les emails en IDs final participantsData = await _getParticipantsData(_participants);
List<String> participantIds = await _changeUserEmailById(_participants); List<String> participantIds = participantsData.map((p) => p['id'] as String).toList();
// Ajouter le créateur
if (!participantIds.contains(currentUser.id)) { if (!participantIds.contains(currentUser.id)) {
participantIds.insert(0, currentUser.id); participantIds.insert(0, currentUser.id);
} }
@@ -449,20 +449,38 @@ class _CreateTripContentState extends State<CreateTripContent> {
updatedAt: DateTime.now(), updatedAt: DateTime.now(),
); );
// Créer le groupe context.read<TripBloc>().add(TripCreateRequested(trip: trip));
// Attendre que le trip soit créé (simplifié)
await Future.delayed(Duration(milliseconds: 500));
final group = Group( final group = Group(
id: '', id: '',
name: _titleController.text.trim(), name: _titleController.text.trim(),
members: participantIds, tripId: '',
createdBy: currentUser.id,
); );
// Utiliser les BLoCs pour créer final groupMembers = <GroupMember>[
context.read<TripBloc>().add(TripCreateRequested(trip: trip)); GroupMember(
context.read<GroupBloc>().add(GroupCreateRequested(group: group)); 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<GroupBloc>().add(CreateGroupWithMembers(
} group: group,
members: groupMembers,
));
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
@@ -481,14 +499,24 @@ class _CreateTripContentState extends State<CreateTripContent> {
} }
} }
Future<List<String>> _changeUserEmailById(List<String> participants) async { // ...existing code...
List<String> ids = [];
for (String email in participants) { // Récupérer les IDs et prénoms des participants
Future<List<Map<String, String>>> _getParticipantsData(List<String> emails) async {
List<Map<String, String>> participantsData = [];
for (String email in emails) {
try { try {
final id = await _userService.getUserIdByEmail(email); final userId = await _userService.getUserIdByEmail(email);
if (id != null) { if (userId != null) {
ids.add(id); // 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 { } else {
if (mounted) { if (mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
@@ -500,10 +528,10 @@ class _CreateTripContentState extends State<CreateTripContent> {
} }
} }
} catch (e) { } catch (e) {
print('Erreur: $e'); print('Erreur lors de la récupération de l\'utilisateur: $e');
} }
} }
return ids; return participantsData;
} }
} }

View File

@@ -1,26 +1,65 @@
import 'group_member.dart';
class Group { class Group {
final String? id; final String id; // ID obligatoire maintenant
final String name; final String name;
final List<String> members; final String tripId;
final String createdBy;
final DateTime createdAt;
final DateTime updatedAt;
final List<GroupMember> members;
Group({ Group({
this.id, required this.id, // Obligatoire
required this.name, required this.name,
required this.members, required this.tripId,
}); required this.createdBy,
DateTime? createdAt,
DateTime? updatedAt,
List<GroupMember>? members,
}) : createdAt = createdAt ?? DateTime.now(),
updatedAt = updatedAt ?? DateTime.now(),
members = members ?? [];
factory Group.fromMap(Map<String, dynamic> data, String documentId) { factory Group.fromMap(Map<String, dynamic> map, String id) {
return Group( return Group(
id: documentId, id: id,
name: data['name'] ?? '', name: map['name'] ?? '',
members: List<String>.from(data['members'] ?? []), tripId: map['tripId'] ?? '',
createdBy: map['createdBy'] ?? '',
createdAt: DateTime.fromMillisecondsSinceEpoch(map['createdAt'] ?? 0),
updatedAt: DateTime.fromMillisecondsSinceEpoch(map['updatedAt'] ?? 0),
members: [],
); );
} }
Map<String, dynamic> toMap() { Map<String, dynamic> toMap() {
return { return {
'name': name, '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<GroupMember>? 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,
);
}
} }

View File

@@ -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<String, dynamic> 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<String, dynamic> 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,
);
}
}

View File

@@ -13,6 +13,7 @@ import 'blocs/trip/trip_bloc.dart';
import 'repositories/auth_repository.dart'; import 'repositories/auth_repository.dart';
import 'repositories/trip_repository.dart'; import 'repositories/trip_repository.dart';
import 'repositories/user_repository.dart'; import 'repositories/user_repository.dart';
import 'repositories/group_repository.dart';
import 'pages/login.dart'; import 'pages/login.dart';
import 'pages/home.dart'; import 'pages/home.dart';
import 'pages/signup.dart'; import 'pages/signup.dart';
@@ -41,6 +42,9 @@ class MyApp extends StatelessWidget {
RepositoryProvider<TripRepository>( RepositoryProvider<TripRepository>(
create: (context) => TripRepository(), create: (context) => TripRepository(),
), ),
RepositoryProvider<GroupRepository>(
create: (context) => GroupRepository(),
),
], ],
child: MultiBlocProvider( child: MultiBlocProvider(
providers: [ providers: [
@@ -52,7 +56,9 @@ class MyApp extends StatelessWidget {
authRepository: context.read<AuthRepository>(), authRepository: context.read<AuthRepository>(),
)..add(AuthCheckRequested()), )..add(AuthCheckRequested()),
), ),
BlocProvider(create: (context) => GroupBloc()), BlocProvider(create: (context) => GroupBloc(
context.read<GroupRepository>(),
)),
BlocProvider(create: (context) => TripBloc( BlocProvider(create: (context) => TripBloc(
tripRepository: context.read<TripRepository>(), tripRepository: context.read<TripRepository>(),
), ),

View File

@@ -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<String> createGroupWithMembers({
required Group group,
required List<GroupMember> members,
}) async {
try {
return await _firestore.runTransaction<String>((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<List<Group>> getGroupsByUserId(String userId) {
return _groupsCollection.snapshots().asyncMap((snapshot) async {
List<Group> 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<String, dynamic>,
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<Group?> 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<String, dynamic>, 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<Group?> 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<String, dynamic>, 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<List<GroupMember>> getGroupMembers(String groupId) async {
try {
final snapshot = await _membersCollection(groupId).get();
return snapshot.docs
.map((doc) => GroupMember.fromMap(
doc.data() as Map<String, dynamic>,
doc.id,
))
.toList();
} catch (e) {
throw Exception('Erreur lors de la récupération des membres: $e');
}
}
// Ajouter un membre
Future<void> 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<void> 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<void> 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<void> 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<List<GroupMember>> watchGroupMembers(String groupId) {
return _membersCollection(groupId).snapshots().map(
(snapshot) => snapshot.docs
.map((doc) => GroupMember.fromMap(
doc.data() as Map<String, dynamic>,
doc.id,
))
.toList(),
);
}
}