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 '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<GroupEvent, GroupState> {
final GroupService _groupService;
final GroupRepository _repository;
StreamSubscription? _groupsSubscription;
GroupBloc({GroupService? groupService})
: _groupService = groupService ?? GroupService(),
super(GroupInitial()) {
on<GroupLoadRequested>(_onLoadRequested);
on<_GroupUpdated>(_onGroupUpdated);
on<GroupCreateRequested>(_onCreateRequested);
on<GroupUpdateRequested>(_onUpdateRequested);
on<GroupDeleteRequested>(_onDeleteRequested);
on<GroupMemberAddRequested>(_onMemberAddRequested);
on<GroupMemberRemoveRequested>(_onMemberRemoveRequested);
GroupBloc(this._repository) : super(GroupInitial()) {
on<LoadGroupsByUserId>(_onLoadGroupsByUserId);
on<LoadGroupsByTrip>(_onLoadGroupsByTrip);
on<CreateGroup>(_onCreateGroup);
on<CreateGroupWithMembers>(_onCreateGroupWithMembers);
on<AddMemberToGroup>(_onAddMemberToGroup);
on<RemoveMemberFromGroup>(_onRemoveMemberFromGroup);
on<UpdateGroup>(_onUpdateGroup);
on<DeleteGroup>(_onDeleteGroup);
}
Future<void> _onLoadRequested(
GroupLoadRequested event,
// NOUVEAU : Charger les groupes par userId
Future<void> _onLoadGroupsByUserId(
LoadGroupsByUserId event,
Emitter<GroupState> 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<void> _onGroupUpdated(
_GroupUpdated event,
Emitter<GroupState> emit,
) async {
emit(GroupLoaded(groups: event.groups));
}
Future<void> _onCreateRequested(
GroupCreateRequested event,
// Charger les groupes d'un voyage (conservé)
Future<void> _onLoadGroupsByTrip(
LoadGroupsByTrip event,
Emitter<GroupState> 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<void> _onUpdateRequested(
GroupUpdateRequested event,
// Créer un groupe simple
Future<void> _onCreateGroup(
CreateGroup event,
Emitter<GroupState> 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<void> _onDeleteRequested(
GroupDeleteRequested event,
// Créer un groupe avec ses membres
Future<void> _onCreateGroupWithMembers(
CreateGroupWithMembers event,
Emitter<GroupState> 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<void> _onMemberAddRequested(
GroupMemberAddRequested event,
// Ajouter un membre
Future<void> _onAddMemberToGroup(
AddMemberToGroup event,
Emitter<GroupState> 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<void> _onMemberRemoveRequested(
GroupMemberRemoveRequested event,
// Supprimer un membre
Future<void> _onRemoveMemberFromGroup(
RemoveMemberFromGroup event,
Emitter<GroupState> 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<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();
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 '../../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<Object?> 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<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;
const GroupCreateRequested({required this.group});
const CreateGroup(this.group);
@override
List<Object?> get props => [group];
}
class GroupUpdateRequested extends GroupEvent {
// Créer un groupe avec ses membres
class CreateGroupWithMembers extends GroupEvent {
final Group group;
final List<GroupMember> members;
const GroupUpdateRequested({required this.group});
const CreateGroupWithMembers({
required this.group,
required this.members,
});
@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;
const GroupDeleteRequested({required this.groupId});
const DeleteGroup(this.groupId);
@override
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 => [];
}
// État initial
class GroupInitial extends GroupState {}
// Chargement
class GroupLoading extends GroupState {}
class GroupLoaded extends GroupState {
// Groupes chargés
class GroupsLoaded extends GroupState {
final List<Group> groups;
const GroupLoaded({required this.groups});
const GroupsLoaded(this.groups);
@override
List<Object?> 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<Object?> get props => [message];
}
// Erreur
class GroupError extends GroupState {
final String message;
const GroupError({required this.message});
const GroupError(this.message);
@override
List<Object?> get props => [message];
}
}

View File

@@ -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<GroupContent> {
@override
void initState() {
super.initState();
_loadGroupsIfUserLoaded();
_loadInitialData();
}
void _loadGroupsIfUserLoaded() {
void _loadInitialData() {
final userState = context.read<UserBloc>().state;
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>(
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<GroupContent> {
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<GroupContent> {
}
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<GroupBloc>().state is GroupInitial) {
context.read<GroupBloc>().add(GroupLoadRequested(userId: user.id));
}
return BlocConsumer<GroupBloc, GroupState>(
listener: (context, groupState) {
if (groupState is GroupOperationSuccess) {
@@ -75,8 +71,8 @@ class _GroupContentState extends State<GroupContent> {
backgroundColor: Colors.green,
),
);
// Recharger les groupes
context.read<GroupBloc>().add(GroupLoadRequested(userId: user.id));
// Recharger les groupes après une opération réussie
context.read<GroupBloc>().add(LoadGroupsByUserId(user.id));
} else if (groupState is GroupError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
@@ -90,7 +86,7 @@ class _GroupContentState extends State<GroupContent> {
return Scaffold(
body: RefreshIndicator(
onRefresh: () async {
context.read<GroupBloc>().add(GroupLoadRequested(userId: user.id));
context.read<GroupBloc>().add(LoadGroupsByUserId(user.id));
},
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
@@ -98,29 +94,39 @@ class _GroupContentState extends State<GroupContent> {
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<GroupContent> {
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<GroupContent> {
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<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() {
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<Group> groups) {
return Column(
children: groups.map((group) => _buildGroupCard(group)).toList(),
Widget _buildGroupGrid(List<Group> 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),
// ),
// );
}
}

View File

@@ -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<CreateTripContent> {
});
try {
// Convertir les emails en IDs
List<String> participantIds = await _changeUserEmailById(_participants);
final participantsData = await _getParticipantsData(_participants);
List<String> 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<CreateTripContent> {
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(
id: '',
id: '',
name: _titleController.text.trim(),
members: participantIds,
tripId: '',
createdBy: currentUser.id,
);
// Utiliser les BLoCs pour créer
context.read<TripBloc>().add(TripCreateRequested(trip: trip));
context.read<GroupBloc>().add(GroupCreateRequested(group: group));
final groupMembers = <GroupMember>[
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<GroupBloc>().add(CreateGroupWithMembers(
group: group,
members: groupMembers,
));
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
@@ -481,14 +499,24 @@ class _CreateTripContentState extends State<CreateTripContent> {
}
}
Future<List<String>> _changeUserEmailById(List<String> participants) async {
List<String> ids = [];
// ...existing code...
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 {
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<CreateTripContent> {
}
}
} 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 {
final String? id;
final String id; // ID obligatoire maintenant
final String name;
final List<String> members;
final String tripId;
final String createdBy;
final DateTime createdAt;
final DateTime updatedAt;
final List<GroupMember> 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<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(
id: documentId,
name: data['name'] ?? '',
members: List<String>.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<String, dynamic> 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<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/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<TripRepository>(
create: (context) => TripRepository(),
),
RepositoryProvider<GroupRepository>(
create: (context) => GroupRepository(),
),
],
child: MultiBlocProvider(
providers: [
@@ -52,7 +56,9 @@ class MyApp extends StatelessWidget {
authRepository: context.read<AuthRepository>(),
)..add(AuthCheckRequested()),
),
BlocProvider(create: (context) => GroupBloc()),
BlocProvider(create: (context) => GroupBloc(
context.read<GroupRepository>(),
)),
BlocProvider(create: (context) => TripBloc(
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(),
);
}
}