diff --git a/lib/blocs/group/group_bloc.dart b/lib/blocs/group/group_bloc.dart index 550ba46..a1fafcf 100644 --- a/lib/blocs/group/group_bloc.dart +++ b/lib/blocs/group/group_bloc.dart @@ -13,7 +13,7 @@ class GroupBloc extends Bloc { GroupBloc(this._repository) : super(GroupInitial()) { on(_onLoadGroupsByUserId); - on<_GroupsUpdated>(_onGroupsUpdated); // NOUVEAU événement interne + on<_GroupsUpdated>(_onGroupsUpdated); on(_onLoadGroupsByTrip); on(_onCreateGroup); on(_onCreateGroupWithMembers); @@ -44,7 +44,6 @@ class GroupBloc extends Bloc { } } - // NOUVEAU: Handler pour les mises à jour du stream Future _onGroupsUpdated( _GroupsUpdated event, Emitter emit, @@ -111,6 +110,7 @@ class GroupBloc extends Bloc { Emitter emit, ) async { try { + // CORRECTION : Utiliser addMemberToGroup au lieu de addMember await _repository.addMember(event.groupId, event.member); emit(const GroupOperationSuccess('Membre ajouté')); } catch (e) { @@ -123,6 +123,7 @@ class GroupBloc extends Bloc { Emitter emit, ) async { try { + // CORRECTION : Utiliser removeMemberFromGroup au lieu de removeMember await _repository.removeMember(event.groupId, event.userId); emit(const GroupOperationSuccess('Membre supprimé')); } catch (e) { @@ -161,7 +162,6 @@ class GroupBloc extends Bloc { } } -// NOUVEAU: Événement interne pour les mises à jour du stream class _GroupsUpdated extends GroupEvent { final List groups; final String? error; diff --git a/lib/blocs/trip/trip_bloc.dart b/lib/blocs/trip/trip_bloc.dart index a955329..a081d4c 100644 --- a/lib/blocs/trip/trip_bloc.dart +++ b/lib/blocs/trip/trip_bloc.dart @@ -1,124 +1,133 @@ import 'dart:async'; import 'package:flutter_bloc/flutter_bloc.dart'; -import '../../repositories/trip_repository.dart'; +import 'package:travel_mate/data/models/trip.dart'; import 'trip_event.dart'; import 'trip_state.dart'; -import '../../data/models/trip.dart'; +import '../../repositories/trip_repository.dart'; + class TripBloc extends Bloc { - final TripRepository _tripRepository; + final TripRepository _repository; StreamSubscription? _tripsSubscription; + String? _currentUserId; - TripBloc({required TripRepository tripRepository}) - : _tripRepository = tripRepository, - super(TripInitial()) { - on(_onLoadRequested); - on<_TripUpdated>(_onTripUpdated); - on(_onCreateRequested); - on(_onUpdateRequested); - on(_onDeleteRequested); - on(_onParticipantAddRequested); - on(_onParticipantRemoveRequested); - on(_onResetTrips); + TripBloc(this._repository) : super(TripInitial()) { + on(_onLoadTripsByUserId); + on(_onTripCreateRequested); + on(_onTripUpdateRequested); + on(_onTripDeleteRequested); + on<_TripsUpdated>(_onTripsUpdated); } - Future _onLoadRequested( - TripLoadRequested event, + Future _onLoadTripsByUserId( + LoadTripsByUserId event, Emitter emit, ) async { - emit(TripLoading()); - - await _tripsSubscription?.cancel(); + print('🔍 Chargement des trips pour userId: ${event.userId}'); - _tripsSubscription = _tripRepository.getUserTrips(event.userId).listen( - (trips) => add(_TripUpdated(trips: trips)), - onError: (error) => emit(TripError(message: error.toString())), + // MODIFIÉ : Toujours émettre Loading pour forcer le rechargement + emit(TripLoading()); + + _currentUserId = event.userId; + await _tripsSubscription?.cancel(); + + _tripsSubscription = _repository.getTripsByUserId(event.userId).listen( + (trips) { + print('📦 Stream reçu: ${trips.length} trips'); + add(_TripsUpdated(trips)); + }, + onError: (error) { + print('❌ Erreur stream: $error'); + emit(TripError(error.toString())); + }, ); } - Future _onTripUpdated( - _TripUpdated event, + void _onTripsUpdated( + _TripsUpdated event, Emitter emit, - ) async { - emit(TripLoaded(trips: event.trips)); + ) { + print('✅ Émission de TripLoaded avec ${event.trips.length} trips'); + emit(TripLoaded(event.trips)); } - Future _onCreateRequested( + Future _onTripCreateRequested( TripCreateRequested event, Emitter emit, ) async { try { - await _tripRepository.createTrip(event.trip); - emit(const TripOperationSuccess(message: 'Voyage créé avec succès')); + print('📝 Création du voyage: ${event.trip.title}'); + emit(TripLoading()); + + final tripId = await _repository.createTrip(event.trip); + print('✅ Voyage créé avec ID: $tripId'); + + // Émettre TripCreated pour que create_trip_content puisse créer le groupe + emit(TripCreated(tripId: tripId)); + + // AJOUTÉ : Attendre un peu puis recharger manuellement + await Future.delayed(const Duration(milliseconds: 800)); + if (_currentUserId != null) { + print('🔄 Rechargement forcé après création'); + add(LoadTripsByUserId(userId: _currentUserId!)); + } + } catch (e) { - emit(TripError(message: e.toString())); + print('❌ Erreur création: $e'); + emit(TripError('Erreur lors de la création: $e')); } } - Future _onUpdateRequested( + Future _onTripUpdateRequested( TripUpdateRequested event, Emitter emit, ) async { try { - await _tripRepository.updateTrip(event.trip); - emit(const TripOperationSuccess(message: 'Voyage mis à jour')); + print('📝 Mise à jour du voyage: ${event.trip.title}'); + + await _repository.updateTrip(event.trip.id!, event.trip); + print('✅ Voyage mis à jour'); + + emit(const TripOperationSuccess('Voyage mis à jour avec succès')); + + // AJOUTÉ : Recharger après mise à jour + await Future.delayed(const Duration(milliseconds: 500)); + if (_currentUserId != null) { + print('🔄 Rechargement forcé après mise à jour'); + add(LoadTripsByUserId(userId: _currentUserId!)); + } + } catch (e) { - emit(TripError(message: e.toString())); + print('❌ Erreur mise à jour: $e'); + emit(TripError('Erreur lors de la mise à jour: $e')); } } - Future _onDeleteRequested( + Future _onTripDeleteRequested( TripDeleteRequested event, Emitter emit, ) async { try { - await _tripRepository.deleteTrip(event.tripId); - emit(const TripOperationSuccess(message: 'Voyage supprimé')); + print('🗑️ Suppression du voyage: ${event.tripId}'); + + await _repository.deleteTrip(event.tripId); + print('✅ Voyage supprimé'); + + emit(const TripOperationSuccess('Voyage supprimé avec succès')); + + // AJOUTÉ : Recharger après suppression + await Future.delayed(const Duration(milliseconds: 500)); + if (_currentUserId != null) { + print('🔄 Rechargement forcé après suppression'); + add(LoadTripsByUserId(userId: _currentUserId!)); + } + } catch (e) { - emit(TripError(message: e.toString())); + print('❌ Erreur suppression: $e'); + emit(TripError('Erreur lors de la suppression: $e')); } } - Future _onParticipantAddRequested( - TripParticipantAddRequested event, - Emitter emit, - ) async { - try { - await _tripRepository.addParticipant( - event.tripId, - event.participantEmail, - ); - emit(const TripOperationSuccess(message: 'Participant ajouté')); - } catch (e) { - emit(TripError(message: e.toString())); - } - } - - Future _onParticipantRemoveRequested( - TripParticipantRemoveRequested event, - Emitter emit, - ) async { - try { - await _tripRepository.removeParticipant( - event.tripId, - event.participantEmail, - ); - emit(const TripOperationSuccess(message: 'Participant retiré')); - } catch (e) { - emit(TripError(message: e.toString())); - } - } - - - Future _onResetTrips( - ResetTrips event, - Emitter emit, - ) async { - await _tripsSubscription?.cancel(); - _tripsSubscription = null; - emit(TripInitial()); - } - @override Future close() { _tripsSubscription?.cancel(); @@ -126,10 +135,10 @@ class TripBloc extends Bloc { } } -class _TripUpdated extends TripEvent { +class _TripsUpdated extends TripEvent { final List trips; - const _TripUpdated({required this.trips}); + const _TripsUpdated(this.trips); @override List get props => [trips]; diff --git a/lib/blocs/trip/trip_event.dart b/lib/blocs/trip/trip_event.dart index 4aa2ce4..0378e72 100644 --- a/lib/blocs/trip/trip_event.dart +++ b/lib/blocs/trip/trip_event.dart @@ -8,10 +8,10 @@ abstract class TripEvent extends Equatable { List get props => []; } -class TripLoadRequested extends TripEvent { +class LoadTripsByUserId extends TripEvent { final String userId; - const TripLoadRequested({required this.userId}); + const LoadTripsByUserId({required this.userId}); @override List get props => [userId]; @@ -42,38 +42,4 @@ class TripDeleteRequested extends TripEvent { @override List get props => [tripId]; -} - -class TripParticipantAddRequested extends TripEvent { - final String tripId; - final String participantEmail; - - const TripParticipantAddRequested({ - required this.tripId, - required this.participantEmail, - }); - - @override - List get props => [tripId, participantEmail]; -} - -class TripParticipantRemoveRequested extends TripEvent { - final String tripId; - final String participantEmail; - - const TripParticipantRemoveRequested({ - required this.tripId, - required this.participantEmail, - }); - - @override - List get props => [tripId, participantEmail]; -} - -// NOUVEAU : Événement pour réinitialiser les trips -class ResetTrips extends TripEvent { - const ResetTrips(); - - @override - List get props => []; } \ No newline at end of file diff --git a/lib/blocs/trip/trip_state.dart b/lib/blocs/trip/trip_state.dart index 1652e2d..203707b 100644 --- a/lib/blocs/trip/trip_state.dart +++ b/lib/blocs/trip/trip_state.dart @@ -15,16 +15,30 @@ class TripLoading extends TripState {} class TripLoaded extends TripState { final List trips; - const TripLoaded({required this.trips}); + const TripLoaded(this.trips); @override List get props => [trips]; } +// NOUVEAU : État pour indiquer qu'un voyage a été créé avec succès +class TripCreated extends TripState { + final String tripId; + final String message; + + const TripCreated({ + required this.tripId, + this.message = 'Voyage créé avec succès', + }); + + @override + List get props => [tripId, message]; +} + class TripOperationSuccess extends TripState { final String message; - const TripOperationSuccess({required this.message}); + const TripOperationSuccess(this.message); @override List get props => [message]; @@ -33,7 +47,7 @@ class TripOperationSuccess extends TripState { class TripError extends TripState { final String message; - const TripError({required this.message}); + const TripError(this.message); @override List get props => [message]; diff --git a/lib/components/home/create_trip_content.dart b/lib/components/home/create_trip_content.dart index b277036..15db114 100644 --- a/lib/components/home/create_trip_content.dart +++ b/lib/components/home/create_trip_content.dart @@ -6,14 +6,20 @@ import '../../blocs/user/user_bloc.dart'; import '../../blocs/user/user_state.dart' as user_state; import '../../blocs/trip/trip_bloc.dart'; import '../../blocs/trip/trip_event.dart'; +import '../../blocs/trip/trip_state.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'; +import '../../repositories/group_repository.dart'; class CreateTripContent extends StatefulWidget { - const CreateTripContent({super.key}); + final Trip? tripToEdit; + const CreateTripContent({ + super.key, + this.tripToEdit, + }); @override State createState() => _CreateTripContentState(); @@ -27,6 +33,7 @@ class _CreateTripContentState extends State { final _locationController = TextEditingController(); final _budgetController = TextEditingController(); final _userService = UserService(); + final _groupRepository = GroupRepository(); DateTime? _startDate; DateTime? _endDate; @@ -35,6 +42,58 @@ class _CreateTripContentState extends State { final List _participants = []; final _participantController = TextEditingController(); + bool get isEditing => widget.tripToEdit != null; + + @override + void initState() { + super.initState(); + _initializeFormWithTrip(); + } + + Future _initializeFormWithTrip() async { + if (widget.tripToEdit != null) { + final trip = widget.tripToEdit!; + + setState(() { + _titleController.text = trip.title; + _descriptionController.text = trip.description; + _locationController.text = trip.location; + _budgetController.text = trip.budget?.toString() ?? ''; + _startDate = trip.startDate; + _endDate = trip.endDate; + }); + + await _loadParticipantEmails(trip.participants); + } + } + + Future _loadParticipantEmails(List participantIds) async { + final userState = context.read().state; + String? currentUserId; + + if (userState is user_state.UserLoaded) { + currentUserId = userState.user.id; + } + + for (String userId in participantIds) { + if (userId == currentUserId) continue; + + try { + final userDoc = await _userService.getUserById(userId); + if (userDoc != null && userDoc.email.isNotEmpty) { + setState(() { + _participants.add(userDoc.email); + }); + } + } catch (e) { + _errorService.logError( + 'create_trip_content.dart', + 'Erreur chargement participant $userId: $e', + ); + } + } + } + @override void dispose() { _titleController.dispose(); @@ -47,235 +106,271 @@ class _CreateTripContentState extends State { @override Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, userState) { - if (userState is! user_state.UserLoaded) { - return Scaffold( - appBar: AppBar(title: Text('Créer un voyage')), - body: Center(child: Text('Veuillez vous connecter')), - ); + return BlocListener( + listener: (context, tripState) { + // Écouter l'état TripCreated pour récupérer l'ID du voyage + if (tripState is TripCreated) { + _createGroupForTrip(tripState.tripId); + } else if (tripState is TripOperationSuccess) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(tripState.message), + backgroundColor: Colors.green, + ), + ); + Navigator.pop(context); + if (isEditing) { + Navigator.pop(context); // Retour supplémentaire en mode édition + } + } + } else if (tripState is TripError) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(tripState.message), + backgroundColor: Colors.red, + ), + ); + setState(() { + _isLoading = false; + }); + } } + }, + child: BlocBuilder( + builder: (context, userState) { + if (userState is! user_state.UserLoaded) { + return Scaffold( + appBar: AppBar( + title: Text(isEditing ? 'Modifier le voyage' : 'Créer un voyage'), + ), + body: Center(child: Text('Veuillez vous connecter')), + ); + } - return Scaffold( - appBar: AppBar( - title: Text('Créer un voyage'), - backgroundColor: Theme.of(context).colorScheme.primary, - foregroundColor: Colors.white, - ), - body: SingleChildScrollView( - padding: EdgeInsets.all(16), - child: Form( - key: _formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildSectionTitle('Informations générales'), - SizedBox(height: 16), + return Scaffold( + appBar: AppBar( + title: Text(isEditing ? 'Modifier le voyage' : 'Créer un voyage'), + backgroundColor: Theme.of(context).colorScheme.primary, + foregroundColor: Colors.white, + ), + body: SingleChildScrollView( + padding: EdgeInsets.all(16), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildSectionTitle('Informations générales'), + SizedBox(height: 16), - TextFormField( - controller: _titleController, - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'Titre requis'; - } - return null; - }, - decoration: InputDecoration( - labelText: 'Titre du voyage *', - hintText: 'ex: Voyage à Paris', - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - ), - prefixIcon: Icon(Icons.travel_explore), - ), - ), - - SizedBox(height: 16), - - TextFormField( - controller: _descriptionController, - maxLines: 3, - decoration: InputDecoration( - labelText: 'Description', - hintText: 'Décrivez votre voyage...', - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - ), - prefixIcon: Icon(Icons.description), - ), - ), - - SizedBox(height: 16), - - TextFormField( - controller: _locationController, - validator: (value) { - if (value == null || value.trim().isEmpty) { - return 'Destination requise'; - } - return null; - }, - decoration: InputDecoration( - labelText: 'Destination *', - hintText: 'ex: Paris, France', - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - ), - prefixIcon: Icon(Icons.location_on), - ), - ), - - SizedBox(height: 24), - - _buildSectionTitle('Dates du voyage'), - SizedBox(height: 16), - - Row( - children: [ - Expanded( - child: _buildDateField( - label: 'Date de début *', - date: _startDate, - onTap: () => _selectStartDate(context), + TextFormField( + controller: _titleController, + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'Titre requis'; + } + return null; + }, + decoration: InputDecoration( + labelText: 'Titre du voyage *', + hintText: 'ex: Voyage à Paris', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), ), + prefixIcon: Icon(Icons.travel_explore), ), - SizedBox(width: 16), - Expanded( - child: _buildDateField( - label: 'Date de fin *', - date: _endDate, - onTap: () => _selectEndDate(context), + ), + + SizedBox(height: 16), + + TextFormField( + controller: _descriptionController, + maxLines: 3, + decoration: InputDecoration( + labelText: 'Description', + hintText: 'Décrivez votre voyage...', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + ), + prefixIcon: Icon(Icons.description), + ), + ), + + SizedBox(height: 16), + + TextFormField( + controller: _locationController, + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'Destination requise'; + } + return null; + }, + decoration: InputDecoration( + labelText: 'Destination *', + hintText: 'ex: Paris, France', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + ), + prefixIcon: Icon(Icons.location_on), + ), + ), + + SizedBox(height: 24), + + _buildSectionTitle('Dates du voyage'), + SizedBox(height: 16), + + Row( + children: [ + Expanded( + child: _buildDateField( + label: 'Date de début *', + date: _startDate, + onTap: () => _selectStartDate(context), + ), + ), + SizedBox(width: 16), + Expanded( + child: _buildDateField( + label: 'Date de fin *', + date: _endDate, + onTap: () => _selectEndDate(context), + ), + ), + ], + ), + + SizedBox(height: 24), + + _buildSectionTitle('Budget'), + SizedBox(height: 16), + + TextFormField( + controller: _budgetController, + keyboardType: TextInputType.numberWithOptions(decimal: true), + decoration: InputDecoration( + labelText: 'Budget estimé', + hintText: 'ex: 1200.50', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + ), + prefixIcon: Icon(Icons.euro), + suffixText: '€', + ), + ), + + SizedBox(height: 24), + + _buildSectionTitle('Participants'), + SizedBox(height: 8), + Text( + 'Ajoutez les emails des personnes que vous souhaitez inviter', + style: TextStyle(color: Colors.grey[600], fontSize: 14), + ), + SizedBox(height: 16), + + Row( + children: [ + Expanded( + child: TextFormField( + controller: _participantController, + keyboardType: TextInputType.emailAddress, + decoration: InputDecoration( + labelText: 'Email du participant', + hintText: 'ex: ami@email.com', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + ), + prefixIcon: Icon(Icons.person_add), + ), + ), + ), + SizedBox(width: 8), + ElevatedButton( + onPressed: _addParticipant, + style: ElevatedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + padding: EdgeInsets.all(16), + ), + child: Icon(Icons.add), + ), + ], + ), + + SizedBox(height: 16), + + if (_participants.isNotEmpty) ...[ + Text( + 'Participants ajoutés (${_participants.length})', + style: TextStyle(fontWeight: FontWeight.w500), + ), + SizedBox(height: 8), + Container( + width: double.infinity, + padding: EdgeInsets.all(12), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey[300]!), + borderRadius: BorderRadius.circular(12), + ), + child: Wrap( + spacing: 8, + runSpacing: 8, + children: _participants + .map( + (email) => Chip( + label: Text(email, style: TextStyle(fontSize: 12)), + deleteIcon: Icon(Icons.close, size: 18), + onDeleted: () => _removeParticipant(email), + backgroundColor: Theme.of(context) + .colorScheme + .primary + .withValues(alpha: 0.1), + ), + ) + .toList(), ), ), ], - ), - SizedBox(height: 24), + SizedBox(height: 32), - _buildSectionTitle('Budget'), - SizedBox(height: 16), - - TextFormField( - controller: _budgetController, - keyboardType: TextInputType.numberWithOptions(decimal: true), - decoration: InputDecoration( - labelText: 'Budget estimé', - hintText: 'ex: 1200.50', - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - ), - prefixIcon: Icon(Icons.euro), - suffixText: '€', - ), - ), - - SizedBox(height: 24), - - _buildSectionTitle('Participants'), - SizedBox(height: 8), - Text( - 'Ajoutez les emails des personnes que vous souhaitez inviter', - style: TextStyle(color: Colors.grey[600], fontSize: 14), - ), - SizedBox(height: 16), - - Row( - children: [ - Expanded( - child: TextFormField( - controller: _participantController, - keyboardType: TextInputType.emailAddress, - decoration: InputDecoration( - labelText: 'Email du participant', - hintText: 'ex: ami@email.com', - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - ), - prefixIcon: Icon(Icons.person_add), - ), - ), - ), - SizedBox(width: 8), - ElevatedButton( - onPressed: _addParticipant, + SizedBox( + width: double.infinity, + height: 50, + child: ElevatedButton( + onPressed: _isLoading ? null : () => _saveTrip(userState.user), style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.primary, + foregroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), - padding: EdgeInsets.all(16), ), - child: Icon(Icons.add), - ), - ], - ), - - SizedBox(height: 16), - - if (_participants.isNotEmpty) ...[ - Text( - 'Participants ajoutés (${_participants.length})', - style: TextStyle(fontWeight: FontWeight.w500), - ), - SizedBox(height: 8), - Container( - width: double.infinity, - padding: EdgeInsets.all(12), - decoration: BoxDecoration( - border: Border.all(color: Colors.grey[300]!), - borderRadius: BorderRadius.circular(12), - ), - child: Wrap( - spacing: 8, - runSpacing: 8, - children: _participants - .map( - (email) => Chip( - label: Text(email, style: TextStyle(fontSize: 12)), - deleteIcon: Icon(Icons.close, size: 18), - onDeleted: () => _removeParticipant(email), - backgroundColor: Theme.of( - context, - ).colorScheme.primary.withAlpha(25), + child: _isLoading + ? CircularProgressIndicator(color: Colors.white) + : Text( + isEditing ? 'Mettre à jour le voyage' : 'Créer le voyage', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), ), - ) - .toList(), ), ), + + SizedBox(height: 20), ], - - SizedBox(height: 32), - - SizedBox( - width: double.infinity, - height: 50, - child: ElevatedButton( - onPressed: _isLoading ? null : () => _saveTrip(userState.user), - style: ElevatedButton.styleFrom( - backgroundColor: Theme.of(context).colorScheme.primary, - foregroundColor: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - child: _isLoading - ? CircularProgressIndicator(color: Colors.white) - : Text( - 'Créer le voyage', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - ), - ), - - SizedBox(height: 20), - ], + ), ), ), - ), - ); - }, + ); + }, + ), ); } @@ -300,7 +395,7 @@ class _CreateTripContentState extends State { final labelColor = isDarkMode ? Colors.white70 : Colors.grey[600]; final iconColor = isDarkMode ? Colors.white70 : Colors.grey[600]; final placeholderColor = isDarkMode ? Colors.white38 : Colors.grey[500]; - + return InkWell( onTap: onTap, child: Container( @@ -319,9 +414,7 @@ class _CreateTripContentState extends State { Icon(Icons.calendar_today, size: 16, color: iconColor), SizedBox(width: 8), Text( - date != null - ? '${date.day}/${date.month}/${date.year}' - : 'Sélectionner', + date != null ? '${date.day}/${date.month}/${date.year}' : 'Sélectionner', style: TextStyle( fontSize: 16, color: date != null ? textColor : placeholderColor, @@ -382,18 +475,15 @@ class _CreateTripContentState extends State { final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$'); if (!emailRegex.hasMatch(email)) { if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Email invalide')) - ); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Email invalide'))); } return; } if (_participants.contains(email)) { if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Ce participant est déjà ajouté')) - ); + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text('Ce participant est déjà ajouté'))); } return; } @@ -410,6 +500,128 @@ class _CreateTripContentState extends State { }); } + // Mettre à jour le groupe avec les nouveaux membres + Future _updateGroupMembers( + String tripId, + user_state.UserModel currentUser, + List> participantsData, + ) async { + try { + final group = await _groupRepository.getGroupByTripId(tripId); + + if (group == null) { + _errorService.logError( + 'create_trip_content.dart', + 'Groupe non trouvé pour le voyage $tripId', + ); + return; + } + + final newMembers = [ + GroupMember( + userId: currentUser.id, + firstName: currentUser.prenom, + pseudo: currentUser.prenom, + role: 'admin', + ), + ...participantsData.map((p) => GroupMember( + userId: p['id'] as String, + firstName: p['firstName'] as String, + pseudo: p['firstName'] as String, + role: 'member', + )), + ]; + + final currentMembers = await _groupRepository.getGroupMembers(group.id); + final currentMemberIds = currentMembers.map((m) => m.userId).toSet(); + final newMemberIds = newMembers.map((m) => m.userId).toSet(); + + final membersToAdd = newMembers.where((m) => !currentMemberIds.contains(m.userId)).toList(); + + final membersToRemove = currentMembers + .where((m) => !newMemberIds.contains(m.userId) && m.role != 'admin') + .toList(); + + for (final member in membersToAdd) { + context.read().add(AddMemberToGroup(group.id, member)); + } + + for (final member in membersToRemove) { + context.read().add(RemoveMemberFromGroup(group.id, member.userId)); + } + } catch (e) { + _errorService.logError( + 'create_trip_content.dart', + 'Erreur lors de la mise à jour du groupe: $e', + ); + } + } + + // NOUVELLE MÉTHODE : Créer le groupe après la création du voyage + Future _createGroupForTrip(String tripId) async { + try { + final userState = context.read().state; + if (userState is! user_state.UserLoaded) return; + + final currentUser = userState.user; + final participantsData = await _getParticipantsData(_participants); + + // Créer le groupe avec le tripId récupéré + final group = Group( + id: '', // Sera généré par Firestore + name: _titleController.text.trim(), + tripId: tripId, // ✅ ID du voyage récupéré + createdBy: currentUser.id, + ); + + final groupMembers = [ + GroupMember( + userId: currentUser.id, + firstName: currentUser.prenom, + pseudo: currentUser.prenom, + role: 'admin', + ), + ...participantsData.map((p) => GroupMember( + userId: p['id'] as String, + firstName: p['firstName'] as String, + pseudo: p['firstName'] as String, + role: 'member', + )), + ]; + + context.read().add(CreateGroupWithMembers( + group: group, + members: groupMembers, + )); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Voyage et groupe créés avec succès !'), + backgroundColor: Colors.green, + ), + ); + + setState(() { + _isLoading = false; + }); + + Navigator.pop(context); + } + } catch (e) { + _errorService.logError( + 'create_trip_content.dart', + 'Erreur lors de la création du groupe: $e', + ); + + if (mounted) { + setState(() { + _isLoading = false; + }); + } + } + } + Future _saveTrip(user_state.UserModel currentUser) async { if (!_formKey.currentState!.validate()) { return; @@ -431,14 +643,13 @@ class _CreateTripContentState extends State { try { final participantsData = await _getParticipantsData(_participants); List participantIds = participantsData.map((p) => p['id'] as String).toList(); - + if (!participantIds.contains(currentUser.id)) { participantIds.insert(0, currentUser.id); } - // Créer le voyage final trip = Trip( - id: '', + id: isEditing ? widget.tripToEdit!.id : '', title: _titleController.text.trim(), description: _descriptionController.text.trim(), location: _locationController.text.trim(), @@ -447,42 +658,23 @@ class _CreateTripContentState extends State { budget: double.tryParse(_budgetController.text) ?? 0.0, createdBy: currentUser.id, participants: participantIds, - createdAt: DateTime.now(), + createdAt: isEditing ? widget.tripToEdit!.createdAt : DateTime.now(), updatedAt: DateTime.now(), ); - context.read().add(TripCreateRequested(trip: trip)); + if (isEditing) { + // Mode mise à jour + context.read().add(TripUpdateRequested(trip: trip)); - // Attendre que le trip soit créé (simplifié) - await Future.delayed(Duration(milliseconds: 500)); - - final group = Group( - id: '', - name: _titleController.text.trim(), - tripId: '', - createdBy: currentUser.id, - ); - - final groupMembers = [ - GroupMember( - userId: currentUser.id, - firstName: currentUser.prenom, - pseudo: currentUser.prenom, // Par défaut = prénom - role: 'admin', - ), - ...participantsData.map((p) => GroupMember( - userId: p['id'] as String, - firstName: p['firstName'] as String, - pseudo: p['firstName'] as String, // Par défaut = prénom - role: 'member', - )), - ]; - - - context.read().add(CreateGroupWithMembers( - group: group, - members: groupMembers, - )); + await _updateGroupMembers( + widget.tripToEdit!.id!, + currentUser, + participantsData, + ); + } else { + // Mode création - Le groupe sera créé dans le listener TripCreated + context.read().add(TripCreateRequested(trip: trip)); + } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( @@ -491,9 +683,7 @@ class _CreateTripContentState extends State { backgroundColor: Colors.red, ), ); - } - } finally { - if (mounted) { + setState(() { _isLoading = false; }); @@ -501,9 +691,6 @@ class _CreateTripContentState extends State { } } - // ...existing code... - - // Récupérer les IDs et prénoms des participants Future>> _getParticipantsData(List emails) async { List> participantsData = []; @@ -511,10 +698,9 @@ class _CreateTripContentState extends State { try { 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, @@ -530,7 +716,10 @@ class _CreateTripContentState extends State { } } } catch (e) { - _errorService.logError('Erreur lors de la récupération de l\'utilisateur $email: $e', StackTrace.current); + _errorService.logError( + 'create_trip_content.dart', + 'Erreur lors de la récupération de l\'utilisateur $email: $e', + ); } } diff --git a/lib/components/home/home_content.dart b/lib/components/home/home_content.dart index 38a391b..7e279ab 100644 --- a/lib/components/home/home_content.dart +++ b/lib/components/home/home_content.dart @@ -16,23 +16,31 @@ class HomeContent extends StatefulWidget { State createState() => _HomeContentState(); } -class _HomeContentState extends State { +class _HomeContentState extends State with AutomaticKeepAliveClientMixin { + @override + bool get wantKeepAlive => true; + @override void initState() { super.initState(); - // Charger les trips quand le widget est initialisé - _loadTripsIfUserLoaded(); + // MODIFIÉ : Attendre un frame avant de charger + WidgetsBinding.instance.addPostFrameCallback((_) { + _loadTripsIfUserLoaded(); + }); } void _loadTripsIfUserLoaded() { final userState = context.read().state; if (userState is UserLoaded) { - context.read().add(TripLoadRequested(userId: userState.user.id)); + print('🚀 Chargement initial des trips pour ${userState.user.id}'); + context.read().add(LoadTripsByUserId(userId: userState.user.id)); } } @override Widget build(BuildContext context) { + super.build(context); // Important pour AutomaticKeepAliveClientMixin + return BlocBuilder( builder: (context, userState) { if (userState is UserLoading) { @@ -42,7 +50,7 @@ class _HomeContentState extends State { ), ); } - + if (userState is UserError) { return Scaffold( body: Center( @@ -57,7 +65,7 @@ class _HomeContentState extends State { ), ); } - + if (userState is! UserLoaded) { return Scaffold( body: Center( @@ -65,14 +73,9 @@ class _HomeContentState extends State { ), ); } - + final user = userState.user; - - // Charger les trips si ce n'est pas déjà fait - if (context.read().state is TripInitial) { - context.read().add(TripLoadRequested(userId: user.id)); - } - + return BlocConsumer( listener: (context, tripState) { if (tripState is TripOperationSuccess) { @@ -82,8 +85,6 @@ class _HomeContentState extends State { backgroundColor: Colors.green, ), ); - // Recharger les trips après une opération réussie - context.read().add(TripLoadRequested(userId: user.id)); } else if (tripState is TripError) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -91,62 +92,70 @@ class _HomeContentState extends State { backgroundColor: Colors.red, ), ); + } else if (tripState is TripCreated) { + // Afficher un message de succès temporaire + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Voyage en cours de création...'), + backgroundColor: Colors.blue, + duration: Duration(seconds: 1), + ), + ); } }, builder: (context, tripState) { return Scaffold( body: RefreshIndicator( onRefresh: () async { - context.read().add(TripLoadRequested(userId: user.id)); + print('🔄 Pull to refresh'); + context.read().add(LoadTripsByUserId(userId: user.id)); + // Attendre que le chargement soit terminé + await Future.delayed(Duration(milliseconds: 500)); }, child: SingleChildScrollView( - physics: AlwaysScrollableScrollPhysics(), + physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Header de bienvenue Text( 'Bonjour ${user.prenom} !', - style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold), + style: const TextStyle(fontSize: 28, fontWeight: FontWeight.bold), ), - SizedBox(height: 8), + const SizedBox(height: 8), Text( 'Vos voyages', style: TextStyle(fontSize: 16, color: Colors.grey[600]), ), - SizedBox(height: 20), + const SizedBox(height: 20), - // Contenu principal basé sur l'état du TripBloc - if (tripState is TripLoading) + if (tripState is TripLoading || tripState is TripCreated) _buildLoadingState() else if (tripState is TripError) _buildErrorState(tripState.message, user.id) else if (tripState is TripLoaded) - tripState.trips.isEmpty - ? _buildEmptyState() - : _buildTripsList(tripState.trips) + tripState.trips.isEmpty + ? _buildEmptyState() + : _buildTripsList(tripState.trips) else _buildEmptyState(), - // Espacement en bas pour éviter que le FAB cache le contenu const SizedBox(height: 80), ], ), ), ), - - // FloatingActionButton floatingActionButton: FloatingActionButton( onPressed: () async { final result = await Navigator.push( context, - MaterialPageRoute(builder: (context) => CreateTripContent()), + MaterialPageRoute(builder: (context) => const CreateTripContent()), ); - - if (result == true) { - // Recharger les trips - context.read().add(TripLoadRequested(userId: user.id)); + + // AJOUTÉ : Recharger manuellement après retour + if (result == true && mounted) { + print('🔄 Retour de création, rechargement...'); + context.read().add(LoadTripsByUserId(userId: user.id)); } }, backgroundColor: Theme.of(context).colorScheme.primary, @@ -189,7 +198,7 @@ class _HomeContentState extends State { SizedBox(height: 16), ElevatedButton( onPressed: () { - context.read().add(TripLoadRequested(userId: userId)); + context.read().add(LoadTripsByUserId(userId: userId)); }, child: Text('Réessayer'), ), @@ -205,28 +214,17 @@ class _HomeContentState extends State { padding: EdgeInsets.all(32), child: Column( children: [ - Icon( - Icons.travel_explore, - size: 64, - color: Colors.grey[400], - ), + Icon(Icons.travel_explore, size: 80, color: Colors.grey[400]), SizedBox(height: 16), Text( - 'Aucun voyage pour le moment', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w500, - color: Colors.grey[600], - ), + 'Aucun voyage', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), ), SizedBox(height: 8), Text( 'Créez votre premier voyage en appuyant sur le bouton +', textAlign: TextAlign.center, - style: TextStyle( - fontSize: 14, - color: Colors.grey[500], - ), + style: TextStyle(fontSize: 14, color: Colors.grey[600]), ), ], ), @@ -236,205 +234,127 @@ class _HomeContentState extends State { Widget _buildTripsList(List trips) { return Column( - children: trips.map((trip) => _buildTravelCard(trip)).toList(), + children: trips.map((trip) => _buildTripCard(trip)).toList(), ); } - Widget _buildTravelCard(Trip trip) { - final colors = [Colors.blue, Colors.orange, Colors.green, Colors.purple, Colors.red]; - final color = colors[trip.title.hashCode.abs() % colors.length]; - + Widget _buildTripCard(Trip trip) { final isDarkMode = Theme.of(context).brightness == Brightness.dark; - final secondaryTextColor = isDarkMode ? Colors.white70 : Colors.grey[700]; - final iconColor = isDarkMode ? Colors.white70 : Colors.grey[600]; + final textColor = isDarkMode ? Colors.white : Colors.black; + final subtextColor = isDarkMode ? Colors.white70 : Colors.grey[600]; return Card( - elevation: 4, - margin: const EdgeInsets.only(bottom: 16), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + margin: EdgeInsets.only(bottom: 12), + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), child: InkWell( - onTap: () { - Navigator.push( + onTap: () async { + // AJOUTÉ : Recharger après retour des détails + final result = await Navigator.push( context, - MaterialPageRoute(builder: (context) => ShowTripDetailsContent(trip: trip)), + MaterialPageRoute( + builder: (context) => ShowTripDetailsContent(trip: trip), + ), ); + + if (result == true && mounted) { + final userState = context.read().state; + if (userState is UserLoaded) { + print('🔄 Retour des détails, rechargement...'); + context.read().add(LoadTripsByUserId(userId: userState.user.id)); + } + } }, borderRadius: BorderRadius.circular(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Image d'en-tête avec titre overlay - Container( - height: 150, - decoration: BoxDecoration( - borderRadius: const BorderRadius.vertical(top: Radius.circular(12)), - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - color.withValues(alpha: 0.7), - color.withValues(alpha: 0.9), - ], - ), - ), - child: Stack( + child: Padding( + padding: EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( children: [ - Container( - width: double.infinity, - decoration: BoxDecoration( - borderRadius: const BorderRadius.vertical( - top: Radius.circular(12), - ), - color: color.withValues(alpha: 0.3), - ), - ), - Positioned( - bottom: 16, - left: 16, - right: 16, + Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( trip.title, - style: const TextStyle( - fontSize: 20, + style: TextStyle( + fontSize: 18, fontWeight: FontWeight.bold, - color: Colors.white, + color: textColor, ), ), - const SizedBox(height: 4), + SizedBox(height: 4), Row( children: [ - const Icon( - Icons.location_on, - color: Colors.white, - size: 16, - ), - const SizedBox(width: 4), - Expanded( - child: Text( - trip.location, - style: const TextStyle( - fontSize: 14, - color: Colors.white, - ), - overflow: TextOverflow.ellipsis, - ), + Icon(Icons.location_on, size: 16, color: subtextColor), + SizedBox(width: 4), + Text( + trip.location, + style: TextStyle(color: subtextColor), ), ], ), ], ), ), - ], - ), - ), - - // Contenu de la carte - Padding( - padding: EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Description - if (trip.description.isNotEmpty) ...[ - Text( - trip.description, - style: TextStyle( - fontSize: 14, - color: secondaryTextColor, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, + Container( + padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: _getStatusColor(trip).withOpacity(0.2), + borderRadius: BorderRadius.circular(12), ), - SizedBox(height: 12), - ], - - // Dates - Row( - children: [ - Icon( - Icons.calendar_today, - size: 16, - color: iconColor, + child: Text( + _getStatusText(trip), + style: TextStyle( + color: _getStatusColor(trip), + fontWeight: FontWeight.bold, + fontSize: 12, ), - SizedBox(width: 8), - Text( - '${trip.startDate.day}/${trip.startDate.month}/${trip.startDate.year} - ${trip.endDate.day}/${trip.endDate.month}/${trip.endDate.year}', - style: TextStyle( - fontSize: 14, - color: iconColor, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - - SizedBox(height: 12), - - // Participants - Row( - children: [ - Icon(Icons.group, size: 16, color: iconColor), - SizedBox(width: 8), - Text( - '${trip.participants.length} participant${trip.participants.length > 1 ? 's' : ''}', - style: TextStyle( - fontSize: 14, - color: iconColor, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - - SizedBox(height: 12), - - // Budget et statut - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - if (trip.budget! > 0) - Row( - children: [ - Icon(Icons.euro, size: 16, color: iconColor), - SizedBox(width: 8), - Text( - 'Budget: ${trip.budget!.toStringAsFixed(2)}€', - style: TextStyle( - fontSize: 14, - color: iconColor, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - - Container( - padding: EdgeInsets.symmetric( - horizontal: 8, - vertical: 4, - ), - decoration: BoxDecoration( - color: _getStatusColor(trip).withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(12), - ), - child: Text( - _getStatusText(trip), - style: TextStyle( - fontSize: 12, - color: _getStatusColor(trip), - fontWeight: FontWeight.w500, - ), - ), - ), - ], + ), ), ], ), - ), - ], + SizedBox(height: 12), + Row( + children: [ + Icon(Icons.calendar_today, size: 16, color: subtextColor), + SizedBox(width: 4), + Text( + '${_formatDate(trip.startDate)} - ${_formatDate(trip.endDate)}', + style: TextStyle(fontSize: 14, color: subtextColor), + ), + ], + ), + if (trip.budget != null) ...[ + SizedBox(height: 8), + Row( + children: [ + Icon(Icons.euro, size: 16, color: subtextColor), + SizedBox(width: 4), + Text( + '${trip.budget!.toStringAsFixed(2)} €', + style: TextStyle(fontSize: 14, color: subtextColor), + ), + ], + ), + ], + SizedBox(height: 8), + Row( + children: [ + Icon(Icons.people, size: 16, color: subtextColor), + SizedBox(width: 4), + Text( + '${trip.participants.length} participant${trip.participants.length > 1 ? 's' : ''}', + style: TextStyle(fontSize: 14, color: subtextColor), + ), + ], + ), + ], + ), ), ), ); @@ -442,23 +362,27 @@ class _HomeContentState extends State { Color _getStatusColor(Trip trip) { final now = DateTime.now(); - if (trip.endDate.isBefore(now)) { - return Colors.grey; - } else if (trip.startDate.isBefore(now) && trip.endDate.isAfter(now)) { - return Colors.green; - } else { + if (now.isBefore(trip.startDate)) { return Colors.blue; + } else if (now.isAfter(trip.endDate)) { + return Colors.grey; + } else { + return Colors.green; } } String _getStatusText(Trip trip) { final now = DateTime.now(); - if (trip.endDate.isBefore(now)) { - return 'Terminé'; - } else if (trip.startDate.isBefore(now) && trip.endDate.isAfter(now)) { - return 'En cours'; - } else { + if (now.isBefore(trip.startDate)) { return 'À venir'; + } else if (now.isAfter(trip.endDate)) { + return 'Terminé'; + } else { + return 'En cours'; } } -} + + String _formatDate(DateTime date) { + return '${date.day}/${date.month}/${date.year}'; + } +} \ No newline at end of file diff --git a/lib/components/home/show_trip_details_content.dart b/lib/components/home/show_trip_details_content.dart index 03fd1d8..02368cc 100644 --- a/lib/components/home/show_trip_details_content.dart +++ b/lib/components/home/show_trip_details_content.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:travel_mate/components/home/create_trip_content.dart'; import 'package:travel_mate/data/models/trip.dart'; class ShowTripDetailsContent extends StatefulWidget { @@ -12,7 +13,7 @@ class ShowTripDetailsContent extends StatefulWidget { class _ShowTripDetailsContentState extends State { @override Widget build(BuildContext context) { - // Détecter le thème actuel + final isDarkMode = Theme.of(context).brightness == Brightness.dark; final textColor = isDarkMode ? Colors.white : Colors.black; final secondaryTextColor = isDarkMode ? Colors.white70 : Colors.grey[600]; @@ -85,8 +86,20 @@ class _ShowTripDetailsContentState extends State { width: double.infinity, height: 50, child: ElevatedButton( - onPressed: () { - // Handle button press + onPressed: () async { + final result = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => CreateTripContent( + tripToEdit: widget.trip, +), +), + ); + + + if (result == true && mounted) { + Navigator.pop(context, true); // Retour avec flag + } }, style: ElevatedButton.styleFrom( backgroundColor: Color.fromARGB(255, 0, 123, 255), diff --git a/lib/data/models/trip.dart b/lib/data/models/trip.dart index 3d054c7..117a11a 100644 --- a/lib/data/models/trip.dart +++ b/lib/data/models/trip.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'package:cloud_firestore/cloud_firestore.dart'; class Trip { final String? id; @@ -9,10 +10,10 @@ class Trip { final DateTime endDate; final double? budget; final List participants; - final String createdBy; // ID de l'utilisateur créateur + final String createdBy; final DateTime createdAt; final DateTime updatedAt; - final String status; // 'draft', 'active', 'completed', 'cancelled' + final String status; Trip({ this.id, @@ -29,74 +30,71 @@ class Trip { this.status = 'draft', }); - // Constructeur pour créer un Trip depuis un Map (utile pour Firebase) - factory Trip.fromMap(Map map) { - return Trip( - id: map['id'], - title: map['title'] ?? '', - description: map['description'] ?? '', - location: map['location'] ?? '', - startDate: _parseDateTime(map['startDate']), - endDate: _parseDateTime(map['endDate']), - budget: map['budget']?.toDouble(), - participants: List.from(map['participants'] ?? []), - createdBy: map['createdBy'] ?? '', - createdAt: _parseDateTime(map['createdAt']), - updatedAt: _parseDateTime(map['updatedAt']), - status: map['status'] ?? 'draft', - ); - } - - // Méthode helper pour parser les dates depuis différents formats - static DateTime _parseDateTime(dynamic dateValue) { - if (dateValue == null) { - return DateTime.now(); + // NOUVELLE MÉTHODE HELPER pour convertir n'importe quel format de date + static DateTime _parseDateTime(dynamic value) { + if (value == null) return DateTime.now(); + + // Si c'est déjà un Timestamp Firebase + if (value is Timestamp) { + return value.toDate(); } - - if (dateValue is DateTime) { - return dateValue; + + // Si c'est un int (millisecondes depuis epoch) + if (value is int) { + return DateTime.fromMillisecondsSinceEpoch(value); } - - if (dateValue is String) { - try { - // Essayer de parser comme ISO 8601 - return DateTime.parse(dateValue); - } catch (e) { - return DateTime.now(); - } + + // Si c'est un String (ISO 8601) + if (value is String) { + return DateTime.parse(value); } - - if (dateValue is int) { - try { - // Traiter comme millisecondes - return DateTime.fromMillisecondsSinceEpoch(dateValue); - } catch (e) { - return DateTime.now(); - } + + // Si c'est déjà un DateTime + if (value is DateTime) { + return value; } + + // Par défaut return DateTime.now(); } - // Constructeur pour créer un Trip depuis JSON - factory Trip.fromJson(String jsonStr) { - Map map = json.decode(jsonStr); - return Trip.fromMap(map); + // Constructeur pour créer un Trip depuis un Map (utile pour Firebase) + factory Trip.fromMap(Map map, String id) { + try { + return Trip( + id: id, + title: map['title'] as String? ?? '', + description: map['description'] as String? ?? '', + location: map['location'] as String? ?? '', + startDate: _parseDateTime(map['startDate']), + endDate: _parseDateTime(map['endDate']), + budget: (map['budget'] as num?)?.toDouble(), + createdBy: map['createdBy'] as String? ?? '', + participants: List.from(map['participants'] as List? ?? []), + createdAt: _parseDateTime(map['createdAt']), + updatedAt: _parseDateTime(map['updatedAt']), + status: map['status'] as String? ?? 'draft', + ); + } catch (e) { + print('❌ Erreur parsing Trip: $e'); + print('Map reçue: $map'); + rethrow; + } } - // Méthode pour convertir un Trip en Map (utile pour Firebase) + // MODIFIÉ : Convertir en Map avec Timestamp pour Firestore Map toMap() { return { - 'id': id, 'title': title, 'description': description, 'location': location, - 'startDate': startDate.millisecondsSinceEpoch, - 'endDate': endDate.millisecondsSinceEpoch, + 'startDate': Timestamp.fromDate(startDate), + 'endDate': Timestamp.fromDate(endDate), 'budget': budget, 'participants': participants, 'createdBy': createdBy, - 'createdAt': createdAt.millisecondsSinceEpoch, - 'updatedAt': updatedAt.millisecondsSinceEpoch, + 'createdAt': Timestamp.fromDate(createdAt), + 'updatedAt': Timestamp.fromDate(updatedAt), 'status': status, }; } @@ -166,12 +164,12 @@ class Trip { // Méthode pour obtenir le budget par participant double? get budgetPerParticipant { if (budget == null || participants.isEmpty) return null; - return budget! / (participants.length + 1); // +1 pour le créateur + return budget! / (participants.length + 1); } // Méthode pour obtenir le nombre total de participants (incluant le créateur) int get totalParticipants { - return participants.length + 1; // +1 pour le créateur + return participants.length + 1; } // Méthode pour formater les dates @@ -197,7 +195,7 @@ class Trip { @override String toString() { - return 'Trip(id: $id, title: $title, location: $location, dates: $formattedDates, participants: ${participants.length})'; + return 'Trip(id: $id, title: $title, location: $location, status: $status)'; } @override @@ -208,4 +206,4 @@ class Trip { @override int get hashCode => id.hashCode; -} +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index c83ae1b..87c0783 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -63,7 +63,7 @@ class MyApp extends StatelessWidget { ), BlocProvider( create: (context) => - TripBloc(tripRepository: context.read()), + TripBloc(context.read()), ), BlocProvider(create: (context) => UserBloc()), ], diff --git a/lib/pages/home.dart b/lib/pages/home.dart index 34a091f..eddb84c 100644 --- a/lib/pages/home.dart +++ b/lib/pages/home.dart @@ -108,7 +108,6 @@ class _HomePageState extends State { if (shouldLogout != true || !mounted) return; try { - context.read().add(ResetTrips()); context.read().add(UserLoggedOut()); _pageCache.clear(); context.read().add(AuthSignOutRequested()); diff --git a/lib/repositories/trip_repository.dart b/lib/repositories/trip_repository.dart index f72279e..b208dcf 100644 --- a/lib/repositories/trip_repository.dart +++ b/lib/repositories/trip_repository.dart @@ -2,112 +2,107 @@ import 'package:cloud_firestore/cloud_firestore.dart'; import '../data/models/trip.dart'; class TripRepository { - final FirebaseFirestore _firestore; + final FirebaseFirestore _firestore = FirebaseFirestore.instance; - TripRepository({FirebaseFirestore? firestore}) - : _firestore = firestore ?? FirebaseFirestore.instance; + CollectionReference get _tripsCollection => _firestore.collection('trips'); - // Créer un voyage - Future createTrip(Trip trip) async { + // Récupérer tous les voyages d'un utilisateur + Stream> getTripsByUserId(String userId) { + print('🔍 Chargement des trips pour userId: $userId'); + try { - final docRef = await _firestore.collection('trips').add(trip.toMap()); - final createdTrip = trip.copyWith(id: docRef.id); - - // Mettre à jour avec l'ID généré - await docRef.update({'id': docRef.id}); - - return createdTrip; + return _tripsCollection + .where('participants', arrayContains: userId) + .snapshots() + .map((snapshot) { + print('📦 Snapshot reçu: ${snapshot.docs.length} documents'); + + final trips = snapshot.docs + .map((doc) { + try { + final data = doc.data() as Map; + print('📄 Document ${doc.id}: ${data.keys.toList()}'); + return Trip.fromMap(data, doc.id); + } catch (e) { + print('❌ Erreur parsing trip ${doc.id}: $e'); + return null; + } + }) + .whereType() + .toList(); + + print('✅ ${trips.length} trips parsés avec succès'); + return trips; + }); } catch (e) { + print('❌ Erreur getTripsByUserId: $e'); + throw Exception('Erreur lors de la récupération des voyages: $e'); + } + } + + // Créer un voyage et retourner son ID + Future createTrip(Trip trip) async { + try { + print('📝 Création du voyage: ${trip.title}'); + + final tripData = trip.toMap(); + // Ne pas modifier les timestamps ici, ils sont déjà au bon format + final docRef = await _tripsCollection.add(tripData); + + print('✅ Voyage créé avec ID: ${docRef.id}'); + return docRef.id; + } catch (e) { + print('❌ Erreur création voyage: $e'); throw Exception('Erreur lors de la création du voyage: $e'); } } - // Récupérer les voyages d'un utilisateur - Stream> getUserTrips(String userId) { - return _firestore - .collection('trips') - .where('createdBy', isEqualTo: userId) - .snapshots() - .map((snapshot) { - return snapshot.docs.map((doc) { - final data = doc.data(); - return Trip.fromMap({...data, 'id': doc.id}); - }).toList(); - }); - } - - // Récupérer les voyages où l'utilisateur est participant - Stream> getSharedTrips(String userId) { - return _firestore - .collection('trips') - .where('participants', arrayContains: userId) - .snapshots() - .map((snapshot) { - return snapshot.docs.map((doc) { - final data = doc.data(); - return Trip.fromMap({...data, 'id': doc.id}); - }).toList(); - }); - } - - // Récupérer un voyage par ID + // Récupérer un voyage par son ID Future getTripById(String tripId) async { try { - final doc = await _firestore.collection('trips').doc(tripId).get(); - if (doc.exists) { - final data = doc.data() as Map; - return Trip.fromMap({...data, 'id': doc.id}); + final doc = await _tripsCollection.doc(tripId).get(); + + if (!doc.exists) { + print('⚠️ Voyage $tripId non trouvé'); + return null; } - return null; + + return Trip.fromMap(doc.data() as Map, doc.id); } catch (e) { + print('❌ Erreur getTripById: $e'); throw Exception('Erreur lors de la récupération du voyage: $e'); } } // Mettre à jour un voyage - Future updateTrip(Trip trip) async { + Future updateTrip(String tripId, Trip trip) async { try { - await _firestore - .collection('trips') - .doc(trip.id) - .update(trip.toMap()); - return true; + print('📝 Mise à jour du voyage: $tripId'); + + final tripData = trip.toMap(); + // Mettre à jour le timestamp de modification + tripData['updatedAt'] = Timestamp.now(); + + await _tripsCollection.doc(tripId).update(tripData); + + print('✅ Voyage $tripId mis à jour'); } catch (e) { + print('❌ Erreur mise à jour voyage: $e'); throw Exception('Erreur lors de la mise à jour du voyage: $e'); } } // Supprimer un voyage - Future deleteTrip(String tripId) async { + Future deleteTrip(String tripId) async { try { - await _firestore.collection('trips').doc(tripId).delete(); - return true; + print('🗑️ Suppression du voyage: $tripId'); + + await _tripsCollection.doc(tripId).delete(); + + print('✅ Voyage $tripId supprimé'); } catch (e) { + print('❌ Erreur suppression voyage: $e'); throw Exception('Erreur lors de la suppression du voyage: $e'); } } - - // Ajouter un participant - Future addParticipant(String tripId, String participantEmail) async { - try { - await _firestore.collection('trips').doc(tripId).update({ - 'participants': FieldValue.arrayUnion([participantEmail]) - }); - return true; - } catch (e) { - throw Exception('Erreur lors de l\'ajout du participant: $e'); - } - } - - // Retirer un participant - Future removeParticipant(String tripId, String participantEmail) async { - try { - await _firestore.collection('trips').doc(tripId).update({ - 'participants': FieldValue.arrayRemove([participantEmail]) - }); - return true; - } catch (e) { - throw Exception('Erreur lors du retrait du participant: $e'); - } - } } \ No newline at end of file diff --git a/lib/services/trip_service.dart b/lib/services/trip_service.dart index 7b69d56..2d90461 100644 --- a/lib/services/trip_service.dart +++ b/lib/services/trip_service.dart @@ -17,7 +17,7 @@ class TripService { return querySnapshot.docs.map((doc) { final data = doc.data() as Map; - return Trip.fromMap({...data, 'id': doc.id}); + return Trip.fromMap({...data, 'id': doc.id}, doc.id); }).toList(); } catch (e) { _errorService.logError('Erreur lors du chargement des voyages: $e', StackTrace.current); @@ -176,7 +176,7 @@ class TripService { processedData['description'] = processedData['description'] ?? ''; processedData['status'] = processedData['status'] ?? 'draft'; - final trip = Trip.fromMap(processedData); + final trip = Trip.fromMap(processedData, docId); return trip; } catch (e, stackTrace) { @@ -224,7 +224,7 @@ class TripService { if (doc.exists) { final data = doc.data() as Map; - return Trip.fromMap({...data, 'id': doc.id}); + return Trip.fromMap({...data, 'id': doc.id}, doc.id); } return null; } catch (e) {