Add functionality to manage account members: implement add and remove member events, update account repository methods, and integrate with trip details for participant management.
This commit is contained in:
@@ -40,6 +40,8 @@ class AccountBloc extends Bloc<AccountEvent, AccountState> {
|
|||||||
on<_AccountsUpdated>(_onAccountsUpdated);
|
on<_AccountsUpdated>(_onAccountsUpdated);
|
||||||
on<CreateAccount>(_onCreateAccount);
|
on<CreateAccount>(_onCreateAccount);
|
||||||
on<CreateAccountWithMembers>(_onCreateAccountWithMembers);
|
on<CreateAccountWithMembers>(_onCreateAccountWithMembers);
|
||||||
|
on<AddMemberToAccount>(_onAddMemberToAccount);
|
||||||
|
on<RemoveMemberFromAccount>(_onRemoveMemberFromAccount);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onLoadAccountsByUserId(
|
Future<void> _onLoadAccountsByUserId(
|
||||||
@@ -109,6 +111,34 @@ class AccountBloc extends Bloc<AccountEvent, AccountState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _onAddMemberToAccount(
|
||||||
|
AddMemberToAccount event,
|
||||||
|
Emitter<AccountState> emit,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
emit(AccountLoading());
|
||||||
|
await _repository.addMemberToAccount(event.accountId, event.member);
|
||||||
|
emit(AccountOperationSuccess('Membre ajouté avec succès'));
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
_errorService.logError(e.toString(), stackTrace);
|
||||||
|
emit(AccountError('Erreur lors de l\'ajout du membre: ${e.toString()}'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onRemoveMemberFromAccount(
|
||||||
|
RemoveMemberFromAccount event,
|
||||||
|
Emitter<AccountState> emit,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
emit(AccountLoading());
|
||||||
|
await _repository.removeMemberFromAccount(event.accountId, event.memberId);
|
||||||
|
emit(AccountOperationSuccess('Membre supprimé avec succès'));
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
_errorService.logError(e.toString(), stackTrace);
|
||||||
|
emit(AccountError('Erreur lors de la suppression du membre: ${e.toString()}'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> close() {
|
Future<void> close() {
|
||||||
_accountsSubscription?.cancel();
|
_accountsSubscription?.cancel();
|
||||||
|
|||||||
@@ -85,4 +85,32 @@ class CreateAccountWithMembers extends AccountEvent {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [account, members];
|
List<Object?> get props => [account, members];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Event to add a member to an existing account.
|
||||||
|
///
|
||||||
|
/// This event is dispatched when a new member needs to be added to
|
||||||
|
/// an account, typically when editing a trip and adding new participants.
|
||||||
|
class AddMemberToAccount extends AccountEvent {
|
||||||
|
final String accountId;
|
||||||
|
final GroupMember member;
|
||||||
|
|
||||||
|
const AddMemberToAccount(this.accountId, this.member);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [accountId, member];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Event to remove a member from an existing account.
|
||||||
|
///
|
||||||
|
/// This event is dispatched when a member needs to be removed from
|
||||||
|
/// an account, typically when editing a trip and removing participants.
|
||||||
|
class RemoveMemberFromAccount extends AccountEvent {
|
||||||
|
final String accountId;
|
||||||
|
final String memberId;
|
||||||
|
|
||||||
|
const RemoveMemberFromAccount(this.accountId, this.memberId);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [accountId, memberId];
|
||||||
}
|
}
|
||||||
@@ -17,6 +17,7 @@ import '../../models/group.dart';
|
|||||||
import '../../models/group_member.dart';
|
import '../../models/group_member.dart';
|
||||||
import '../../services/user_service.dart';
|
import '../../services/user_service.dart';
|
||||||
import '../../repositories/group_repository.dart';
|
import '../../repositories/group_repository.dart';
|
||||||
|
import '../../repositories/account_repository.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||||
import '../../services/place_image_service.dart';
|
import '../../services/place_image_service.dart';
|
||||||
@@ -71,6 +72,7 @@ class _CreateTripContentState extends State<CreateTripContent> {
|
|||||||
/// Services for user and group operations
|
/// Services for user and group operations
|
||||||
final _userService = UserService();
|
final _userService = UserService();
|
||||||
final _groupRepository = GroupRepository();
|
final _groupRepository = GroupRepository();
|
||||||
|
final _accountRepository = AccountRepository();
|
||||||
final _placeImageService = PlaceImageService();
|
final _placeImageService = PlaceImageService();
|
||||||
final _tripGeocodingService = TripGeocodingService();
|
final _tripGeocodingService = TripGeocodingService();
|
||||||
|
|
||||||
@@ -611,46 +613,42 @@ class _CreateTripContentState extends State<CreateTripContent> {
|
|||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
// Dates
|
// Dates
|
||||||
Row(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Column(
|
||||||
child: Column(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
children: [
|
||||||
children: [
|
Text(
|
||||||
Text(
|
'Début du voyage',
|
||||||
'Début du voyage',
|
style: theme.textTheme.titleMedium?.copyWith(
|
||||||
style: theme.textTheme.titleMedium?.copyWith(
|
fontWeight: FontWeight.w600,
|
||||||
fontWeight: FontWeight.w600,
|
color: theme.colorScheme.onSurface,
|
||||||
color: theme.colorScheme.onSurface,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
),
|
||||||
_buildDateField(
|
const SizedBox(height: 12),
|
||||||
date: _startDate,
|
_buildDateField(
|
||||||
onTap: () => _selectStartDate(context),
|
date: _startDate,
|
||||||
),
|
onTap: () => _selectStartDate(context),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(height: 20),
|
||||||
Expanded(
|
Column(
|
||||||
child: Column(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
children: [
|
||||||
children: [
|
Text(
|
||||||
Text(
|
'Fin du voyage',
|
||||||
'Fin du voyage',
|
style: theme.textTheme.titleMedium?.copyWith(
|
||||||
style: theme.textTheme.titleMedium?.copyWith(
|
fontWeight: FontWeight.w600,
|
||||||
fontWeight: FontWeight.w600,
|
color: theme.colorScheme.onSurface,
|
||||||
color: theme.colorScheme.onSurface,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
),
|
||||||
_buildDateField(
|
const SizedBox(height: 12),
|
||||||
date: _endDate,
|
_buildDateField(
|
||||||
onTap: () => _selectEndDate(context),
|
date: _endDate,
|
||||||
),
|
onTap: () => _selectEndDate(context),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -673,82 +671,84 @@ class _CreateTripContentState extends State<CreateTripContent> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
// Inviter des amis
|
// Inviter des amis - seulement en mode création
|
||||||
Text(
|
if (!isEditing) ...[
|
||||||
'Invite tes amis',
|
Text(
|
||||||
style: theme.textTheme.titleMedium?.copyWith(
|
'Invite tes amis',
|
||||||
fontWeight: FontWeight.w600,
|
style: theme.textTheme.titleMedium?.copyWith(
|
||||||
color: theme.colorScheme.onSurface,
|
fontWeight: FontWeight.w600,
|
||||||
|
color: theme.colorScheme.onSurface,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 12),
|
||||||
const SizedBox(height: 12),
|
Row(
|
||||||
Row(
|
children: [
|
||||||
children: [
|
Expanded(
|
||||||
Expanded(
|
child: _buildModernTextField(
|
||||||
child: _buildModernTextField(
|
controller: _participantController,
|
||||||
controller: _participantController,
|
label: 'adresse@email.com',
|
||||||
label: 'adresse@email.com',
|
icon: Icons.alternate_email,
|
||||||
icon: Icons.alternate_email,
|
keyboardType: TextInputType.emailAddress,
|
||||||
keyboardType: TextInputType.emailAddress,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Container(
|
|
||||||
height: 56,
|
|
||||||
width: 56,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.teal,
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: IconButton(
|
|
||||||
onPressed: _addParticipant,
|
|
||||||
icon: const Icon(Icons.add, color: Colors.white),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
|
|
||||||
// Participants ajoutés
|
|
||||||
if (_participants.isNotEmpty) ...[
|
|
||||||
Wrap(
|
|
||||||
spacing: 8,
|
|
||||||
runSpacing: 8,
|
|
||||||
children: _participants.map((email) {
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 12,
|
|
||||||
vertical: 8,
|
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Container(
|
||||||
|
height: 56,
|
||||||
|
width: 56,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.teal.withOpacity(0.1),
|
color: Colors.teal,
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: IconButton(
|
||||||
mainAxisSize: MainAxisSize.min,
|
onPressed: _addParticipant,
|
||||||
children: [
|
icon: const Icon(Icons.add, color: Colors.white),
|
||||||
Text(
|
|
||||||
email,
|
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
|
||||||
color: Colors.teal,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
GestureDetector(
|
|
||||||
onTap: () => _removeParticipant(email),
|
|
||||||
child: const Icon(
|
|
||||||
Icons.close,
|
|
||||||
size: 16,
|
|
||||||
color: Colors.teal,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
}).toList(),
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Participants ajoutés
|
||||||
|
if (_participants.isNotEmpty) ...[
|
||||||
|
Wrap(
|
||||||
|
spacing: 8,
|
||||||
|
runSpacing: 8,
|
||||||
|
children: _participants.map((email) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12,
|
||||||
|
vertical: 8,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.teal.withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
email,
|
||||||
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
|
color: Colors.teal,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () => _removeParticipant(email),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.close,
|
||||||
|
size: 16,
|
||||||
|
color: Colors.teal,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
@@ -871,13 +871,15 @@ class _CreateTripContentState extends State<CreateTripContent> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mettre à jour le groupe avec les nouveaux membres
|
// Mettre à jour le groupe ET le compte avec les nouveaux membres
|
||||||
Future<void> _updateGroupMembers(
|
Future<void> _updateGroupAndAccountMembers(
|
||||||
String tripId,
|
String tripId,
|
||||||
user_state.UserModel currentUser,
|
user_state.UserModel currentUser,
|
||||||
List<Map<String, dynamic>> participantsData,
|
List<Map<String, dynamic>> participantsData,
|
||||||
) async {
|
) async {
|
||||||
final groupBloc = context.read<GroupBloc>();
|
final groupBloc = context.read<GroupBloc>();
|
||||||
|
final accountBloc = context.read<AccountBloc>();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final group = await _groupRepository.getGroupByTripId(tripId);
|
final group = await _groupRepository.getGroupByTripId(tripId);
|
||||||
|
|
||||||
@@ -889,6 +891,9 @@ class _CreateTripContentState extends State<CreateTripContent> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Récupérer le compte associé au voyage
|
||||||
|
final account = await _accountRepository.getAccountByTripId(tripId);
|
||||||
|
|
||||||
final newMembers = await _createMembers();
|
final newMembers = await _createMembers();
|
||||||
|
|
||||||
final currentMembers = await _groupRepository.getGroupMembers(group.id);
|
final currentMembers = await _groupRepository.getGroupMembers(group.id);
|
||||||
@@ -901,21 +906,41 @@ class _CreateTripContentState extends State<CreateTripContent> {
|
|||||||
.where((m) => !newMemberIds.contains(m.userId) && m.role != 'admin')
|
.where((m) => !newMemberIds.contains(m.userId) && m.role != 'admin')
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
|
// Ajouter les nouveaux membres au groupe ET au compte
|
||||||
for (final member in membersToAdd) {
|
for (final member in membersToAdd) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
groupBloc.add(AddMemberToGroup(group.id, member));
|
groupBloc.add(AddMemberToGroup(group.id, member));
|
||||||
|
if (account != null) {
|
||||||
|
accountBloc.add(AddMemberToAccount(account.id, member));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Supprimer les membres supprimés du groupe ET du compte
|
||||||
for (final member in membersToRemove) {
|
for (final member in membersToRemove) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
groupBloc.add(RemoveMemberFromGroup(group.id, member.userId));
|
groupBloc.add(RemoveMemberFromGroup(group.id, member.userId));
|
||||||
|
if (account != null) {
|
||||||
|
accountBloc.add(RemoveMemberFromAccount(account.id, member.userId));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Groupe et compte mis à jour avec succès !'),
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
setState(() {
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_errorService.logError(
|
_errorService.logError(
|
||||||
'create_trip_content.dart',
|
'create_trip_content.dart',
|
||||||
'Erreur lors de la mise à jour du groupe: $e',
|
'Erreur lors de la mise à jour du groupe et du compte: $e',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1067,11 +1092,8 @@ class _CreateTripContentState extends State<CreateTripContent> {
|
|||||||
// Géolocaliser le voyage avant de le sauvegarder
|
// Géolocaliser le voyage avant de le sauvegarder
|
||||||
Trip tripWithCoordinates;
|
Trip tripWithCoordinates;
|
||||||
try {
|
try {
|
||||||
print('🌍 [CreateTrip] Géolocalisation en cours pour: ${trip.location}');
|
|
||||||
tripWithCoordinates = await _tripGeocodingService.geocodeTrip(trip);
|
tripWithCoordinates = await _tripGeocodingService.geocodeTrip(trip);
|
||||||
print('✅ [CreateTrip] Géolocalisation réussie: ${tripWithCoordinates.latitude}, ${tripWithCoordinates.longitude}');
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('⚠️ [CreateTrip] Erreur de géolocalisation: $e');
|
|
||||||
// Continuer sans coordonnées en cas d'erreur
|
// Continuer sans coordonnées en cas d'erreur
|
||||||
tripWithCoordinates = trip;
|
tripWithCoordinates = trip;
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
@@ -1089,14 +1111,16 @@ class _CreateTripContentState extends State<CreateTripContent> {
|
|||||||
// Mode mise à jour
|
// Mode mise à jour
|
||||||
tripBloc.add(TripUpdateRequested(trip: tripWithCoordinates));
|
tripBloc.add(TripUpdateRequested(trip: tripWithCoordinates));
|
||||||
|
|
||||||
// Vérifier que l'ID du voyage existe avant de mettre à jour le groupe
|
// Mettre à jour le groupe ET les comptes avec les nouveaux participants
|
||||||
if (widget.tripToEdit != null && widget.tripToEdit!.id != null && widget.tripToEdit!.id!.isNotEmpty) {
|
if (widget.tripToEdit != null && widget.tripToEdit!.id != null && widget.tripToEdit!.id!.isNotEmpty) {
|
||||||
await _updateGroupMembers(
|
print('🔄 [CreateTrip] Mise à jour du groupe et du compte pour le voyage ID: ${widget.tripToEdit!.id}');
|
||||||
|
print('👥 Participants: ${participantsData.map((p) => p['id']).toList()}');
|
||||||
|
await _updateGroupAndAccountMembers(
|
||||||
widget.tripToEdit!.id!,
|
widget.tripToEdit!.id!,
|
||||||
currentUser,
|
currentUser,
|
||||||
participantsData,
|
participantsData,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Mode création - Le groupe sera créé dans le listener TripCreated
|
// Mode création - Le groupe sera créé dans le listener TripCreated
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ import 'package:travel_mate/components/map/map_content.dart';
|
|||||||
import 'package:travel_mate/services/error_service.dart';
|
import 'package:travel_mate/services/error_service.dart';
|
||||||
import 'package:travel_mate/services/activity_cache_service.dart';
|
import 'package:travel_mate/services/activity_cache_service.dart';
|
||||||
import 'package:travel_mate/repositories/group_repository.dart';
|
import 'package:travel_mate/repositories/group_repository.dart';
|
||||||
|
import 'package:travel_mate/repositories/user_repository.dart';
|
||||||
|
import 'package:travel_mate/repositories/account_repository.dart';
|
||||||
|
import 'package:travel_mate/models/group_member.dart';
|
||||||
import 'package:travel_mate/components/activities/activities_page.dart';
|
import 'package:travel_mate/components/activities/activities_page.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
@@ -26,6 +29,8 @@ class _ShowTripDetailsContentState extends State<ShowTripDetailsContent> {
|
|||||||
final ErrorService _errorService = ErrorService();
|
final ErrorService _errorService = ErrorService();
|
||||||
final ActivityCacheService _cacheService = ActivityCacheService();
|
final ActivityCacheService _cacheService = ActivityCacheService();
|
||||||
final GroupRepository _groupRepository = GroupRepository();
|
final GroupRepository _groupRepository = GroupRepository();
|
||||||
|
final UserRepository _userRepository = UserRepository();
|
||||||
|
final AccountRepository _accountRepository = AccountRepository();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -683,6 +688,11 @@ class _ShowTripDetailsContentState extends State<ShowTripDetailsContent> {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
// Bouton "+" pour ajouter un participant
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 12),
|
||||||
|
child: _buildAddParticipantButton(),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -738,6 +748,196 @@ class _ShowTripDetailsContentState extends State<ShowTripDetailsContent> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Construire le bouton pour ajouter un participant
|
||||||
|
Widget _buildAddParticipantButton() {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
return Tooltip(
|
||||||
|
message: 'Ajouter un participant',
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(
|
||||||
|
color: theme.colorScheme.primary.withValues(alpha: 0.3),
|
||||||
|
width: 2,
|
||||||
|
),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withValues(alpha: 0.1),
|
||||||
|
blurRadius: 4,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: _showAddParticipantDialog,
|
||||||
|
child: CircleAvatar(
|
||||||
|
radius: 28,
|
||||||
|
backgroundColor: theme.colorScheme.primary.withValues(alpha: 0.1),
|
||||||
|
child: Icon(
|
||||||
|
Icons.add,
|
||||||
|
color: theme.colorScheme.primary,
|
||||||
|
size: 28,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Afficher le dialogue pour ajouter un participant
|
||||||
|
void _showAddParticipantDialog() {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final TextEditingController emailController = TextEditingController();
|
||||||
|
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
backgroundColor: theme.dialogBackgroundColor,
|
||||||
|
title: Text(
|
||||||
|
'Ajouter un participant',
|
||||||
|
style: theme.textTheme.titleLarge?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Entrez l\'email du participant à ajouter :',
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: theme.colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextField(
|
||||||
|
controller: emailController,
|
||||||
|
keyboardType: TextInputType.emailAddress,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: 'participant@example.com',
|
||||||
|
hintStyle: TextStyle(
|
||||||
|
color: theme.colorScheme.onSurface.withValues(alpha: 0.5),
|
||||||
|
),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
style: TextStyle(color: theme.colorScheme.onSurface),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: Text(
|
||||||
|
'Annuler',
|
||||||
|
style: TextStyle(color: theme.colorScheme.primary),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
if (emailController.text.isNotEmpty) {
|
||||||
|
_addParticipantByEmail(emailController.text);
|
||||||
|
Navigator.pop(context);
|
||||||
|
} else {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Veuillez entrer un email valide'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
'Ajouter',
|
||||||
|
style: TextStyle(color: theme.colorScheme.primary),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ajouter un participant par email
|
||||||
|
Future<void> _addParticipantByEmail(String email) async {
|
||||||
|
try {
|
||||||
|
// Chercher l'utilisateur par email
|
||||||
|
final user = await _userRepository.getUserByEmail(email);
|
||||||
|
|
||||||
|
if (user == null) {
|
||||||
|
_errorService.showError(
|
||||||
|
message: 'Utilisateur non trouvé avec cet email',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.id == null) {
|
||||||
|
_errorService.showError(
|
||||||
|
message: 'ID utilisateur invalide',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ajouter l'utilisateur au groupe
|
||||||
|
if (widget.trip.id != null) {
|
||||||
|
final group = await _groupRepository.getGroupByTripId(widget.trip.id!);
|
||||||
|
if (group != null) {
|
||||||
|
// Créer un GroupMember à partir du User
|
||||||
|
final newMember = GroupMember(
|
||||||
|
userId: user.id!,
|
||||||
|
firstName: user.prenom,
|
||||||
|
pseudo: user.prenom,
|
||||||
|
profilePictureUrl: user.profilePictureUrl,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Ajouter le membre au groupe
|
||||||
|
await _groupRepository.addMember(group.id, newMember);
|
||||||
|
|
||||||
|
// Ajouter le membre au compte
|
||||||
|
final account = await _accountRepository.getAccountByTripId(widget.trip.id!);
|
||||||
|
if (account != null) {
|
||||||
|
await _accountRepository.addMemberToAccount(account.id, newMember);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mettre à jour la liste des participants du voyage
|
||||||
|
final newParticipants = [
|
||||||
|
...widget.trip.participants,
|
||||||
|
user.id!,
|
||||||
|
];
|
||||||
|
final updatedTrip = widget.trip.copyWith(
|
||||||
|
participants: newParticipants,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
|
context.read<TripBloc>().add(
|
||||||
|
TripUpdateRequested(trip: updatedTrip),
|
||||||
|
);
|
||||||
|
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('${user.prenom} a été ajouté au voyage'),
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Rafraîchir la page
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
_errorService.showError(
|
||||||
|
message: 'Erreur lors de l\'ajout du participant: $e',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _navigateToActivities() {
|
void _navigateToActivities() {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
|
|||||||
@@ -198,4 +198,22 @@ class AccountRepository {
|
|||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> addMemberToAccount(String accountId, GroupMember member) async {
|
||||||
|
try {
|
||||||
|
await _membersCollection(accountId).doc(member.userId).set(member.toMap());
|
||||||
|
} catch (e) {
|
||||||
|
_errorService.logError('account_repository.dart', 'Erreur lors de l\'ajout du membre: $e');
|
||||||
|
throw Exception('Erreur lors de l\'ajout du membre: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> removeMemberFromAccount(String accountId, String memberId) async {
|
||||||
|
try {
|
||||||
|
await _membersCollection(accountId).doc(memberId).delete();
|
||||||
|
} catch (e) {
|
||||||
|
_errorService.logError('account_repository.dart', 'Erreur lors de la suppression du membre: $e');
|
||||||
|
throw Exception('Erreur lors de la suppression du membre: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user