From 4e403f3308ca5c9d775f17901ca34b957a43a5df Mon Sep 17 00:00:00 2001 From: Dayron Date: Tue, 7 Oct 2025 11:38:20 +0200 Subject: [PATCH] feat: Enhance trip creation and management features with user validation, improved error handling, and Firestore integration --- lib/components/home/create_trip_content.dart | 75 +++- lib/components/home/home_content.dart | 395 ++++++++++++------- lib/models/trip.dart | 42 +- lib/providers/user_provider.dart | 23 +- lib/services/trip_service.dart | 315 ++++++++++++--- 5 files changed, 625 insertions(+), 225 deletions(-) diff --git a/lib/components/home/create_trip_content.dart b/lib/components/home/create_trip_content.dart index 7854c3f..205f936 100644 --- a/lib/components/home/create_trip_content.dart +++ b/lib/components/home/create_trip_content.dart @@ -1,4 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:travel_mate/models/trip.dart'; +import 'package:travel_mate/providers/user_provider.dart'; +import 'package:travel_mate/services/trip_service.dart'; class CreateTripContent extends StatefulWidget { @@ -428,10 +432,70 @@ class _CreateTripContentState extends State { }); try { - // TODO: Implémenter la sauvegarde du voyage - await Future.delayed(Duration(seconds: 2)); // Simulation + final userProvider = Provider.of(context, listen: false); + final currentUser = userProvider.currentUser; - if (mounted) { + if (currentUser == null) { + throw Exception('Utilisateur non connecté'); + } + + print('Création du voyage par: ${currentUser.id} (${currentUser.email})'); + + // Convertir les emails en IDs utilisateur + List participantIds = []; + + // Ajouter automatiquement le créateur + if (currentUser.id != null) { + participantIds.add(currentUser.id!); + } + + // Convertir chaque email en ID utilisateur + for (String email in _participants) { + try { + final userId = await userProvider.getUserIdByEmail(email); + if (userId != null && !participantIds.contains(userId)) { + participantIds.add(userId); + print('Email $email converti en ID: $userId'); + } else if (userId == null) { + print('Utilisateur non trouvé pour l\'email: $email'); + // Optionnel: afficher un warning à l'utilisateur + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Utilisateur non trouvé pour l\'email: $email'), + backgroundColor: Colors.orange, + duration: Duration(seconds: 2), + ), + ); + } + } catch (e) { + print('Erreur lors de la recherche de l\'utilisateur $email: $e'); + } + } + + // Créer l'objet Trip avec les IDs des participants + final trip = Trip( + id: '', // Sera généré par Firebase + title: _titleController.text.trim(), + description: _descriptionController.text.trim(), + location: _locationController.text.trim(), + startDate: _startDate!, + endDate: _endDate!, + budget: double.tryParse(_budgetController.text) ?? 0.0, + createdBy: currentUser.id ?? '', + participants: participantIds, // Contient uniquement les IDs + createdAt: DateTime.now(), + updatedAt: DateTime.now(), + ); + + print('Participants IDs: $participantIds'); + print('Données du voyage: ${trip.toMap()}'); + + // Sauvegarder le voyage + final tripService = TripService(); + final success = await tripService.addTrip(trip); + + if (success && mounted) { + print('Voyage créé avec succès !'); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Voyage créé avec succès !'), @@ -439,9 +503,12 @@ class _CreateTripContentState extends State { ), ); - Navigator.pop(context); + Navigator.pop(context, true); // Retourner true pour indiquer le succès + } else { + throw Exception('Erreur lors de la sauvegarde'); } } catch (e) { + print('Erreur lors de la création: $e'); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( diff --git a/lib/components/home/home_content.dart b/lib/components/home/home_content.dart index 920b3bb..5567d65 100644 --- a/lib/components/home/home_content.dart +++ b/lib/components/home/home_content.dart @@ -2,10 +2,19 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:travel_mate/components/home/create_trip_content.dart'; import '../../providers/user_provider.dart'; +import '../../services/trip_service.dart'; +import '../../models/trip.dart'; -class HomeContent extends StatelessWidget { +class HomeContent extends StatefulWidget { const HomeContent({super.key}); + @override + State createState() => _HomeContentState(); +} + +class _HomeContentState extends State { + final TripService _tripService = TripService(); + @override Widget build(BuildContext context) { return Scaffold( @@ -13,72 +22,102 @@ class HomeContent extends StatelessWidget { builder: (context, userProvider, child) { final user = userProvider.currentUser; - return SingleChildScrollView( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Header de bienvenue - Text( - 'Bonjour ${user?.prenom ?? 'Voyageur'} !', - style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold), - ), - SizedBox(height: 8), - Text( - 'Vos voyages en cours', - style: TextStyle(fontSize: 16, color: Colors.grey[600]), - ), - SizedBox(height: 20), + if (user == null || user.id == null) { + return Center( + child: Text('Utilisateur non connecté'), + ); + } - // Liste des groupes/voyages - _buildTravelCard( - context, - title: 'Voyage à Paris', - location: 'Paris, France', - dates: '15-20 Juin 2024', - participants: ['Alice', 'Bob', 'Charlie'], - budget: 1200.50, - imageUrl: - 'https://images.unsplash.com/photo-1502602898536-47ad22581b52', - color: Colors.blue, - ), + return StreamBuilder>( + stream: _tripService.getTripsStreamByUser(user.id!, user.email), + builder: (context, snapshot) { + print('StreamBuilder - ConnectionState: ${snapshot.connectionState}'); + print('StreamBuilder - HasError: ${snapshot.hasError}'); + print('StreamBuilder - Data: ${snapshot.data?.length ?? 0} trips'); + + if (snapshot.connectionState == ConnectionState.waiting) { + return _buildLoadingState(); + } - _buildTravelCard( - context, - title: 'Road Trip Espagne', - location: 'Madrid, Espagne', - dates: '10-18 Août 2024', - participants: ['Marie', 'Paul', 'Sophie', 'Thomas'], - budget: 850.75, - imageUrl: - 'https://images.unsplash.com/photo-1539037116277-4db20889f2d4', - color: Colors.orange, - ), + if (snapshot.hasError) { + print('Erreur du stream: ${snapshot.error}'); + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.error, size: 64, color: Colors.red), + SizedBox(height: 16), + Text('Erreur lors du chargement des voyages'), + SizedBox(height: 8), + Text('${snapshot.error}'), + SizedBox(height: 8), + ElevatedButton( + onPressed: () { + setState(() {}); // Forcer le rebuild + }, + child: Text('Réessayer'), + ), + ], + ), + ); + } - _buildTravelCard( - context, - title: 'Weekend à Amsterdam', - location: 'Amsterdam, Pays-Bas', - dates: '3-5 Septembre 2024', - participants: ['Emma', 'Lucas'], - budget: 450.25, - imageUrl: - 'https://images.unsplash.com/photo-1534351450181-ea9f78427fe8', - color: Colors.green, - ), + final trips = snapshot.data ?? []; + print('Trips reçus du stream: ${trips.length}'); - // Espacement en bas pour éviter que le FAB cache le contenu - const SizedBox(height: 80), - ], - ), + return RefreshIndicator( + onRefresh: () async { + setState(() {}); // Forcer le rebuild du stream + }, + child: SingleChildScrollView( + physics: AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header de bienvenue + Text( + 'Bonjour ${user.prenom ?? 'Voyageur'} !', + style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold), + ), + SizedBox(height: 8), + Text( + 'Vos voyages', + style: TextStyle(fontSize: 16, color: Colors.grey[600]), + ), + SizedBox(height: 20), + + // Contenu principal + trips.isEmpty ? _buildEmptyState() : _buildTripsList(trips), + + // Espacement en bas pour éviter que le FAB cache le contenu + const SizedBox(height: 80), + ], + ), + ), + ); + }, ); }, ), - // FloatingActionButton intégré directement dans HomeContent + // FloatingActionButton floatingActionButton: FloatingActionButton( - onPressed: () { - _showCreateTravelDialog(context); + onPressed: () async { + final result = await Navigator.push( + context, + MaterialPageRoute(builder: (context) => CreateTripContent()), + ); + + // Le stream se mettra à jour automatiquement + if (result == true) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Voyage créé ! Il apparaîtra dans quelques secondes.'), + backgroundColor: Colors.green, + ), + ); + } }, backgroundColor: Theme.of(context).colorScheme.primary, foregroundColor: Colors.white, @@ -88,24 +127,67 @@ class HomeContent extends StatelessWidget { ); } - Widget _buildTravelCard( - BuildContext context, { - required String title, - required String location, - required String dates, - required List participants, - required double budget, - required String imageUrl, - required Color color, - }) { + Widget _buildLoadingState() { + return Center( + child: Padding( + padding: EdgeInsets.all(32), + child: CircularProgressIndicator(), + ), + ); + } + + Widget _buildEmptyState() { + return Center( + child: Padding( + padding: EdgeInsets.all(32), + child: Column( + children: [ + Icon( + Icons.travel_explore, + size: 64, + color: Colors.grey[400], + ), + SizedBox(height: 16), + Text( + 'Aucun voyage pour le moment', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w500, + color: Colors.grey[600], + ), + ), + 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], + ), + ), + ], + ), + ), + ); + } + + Widget _buildTripsList(List trips) { + return Column( + children: trips.map((trip) => _buildTravelCard(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]; + return Card( elevation: 4, margin: const EdgeInsets.only(bottom: 16), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), child: InkWell( onTap: () { - // Navigation vers les détails du voyage - _showTravelDetails(context, title); + _showTravelDetails(context, trip); }, borderRadius: BorderRadius.circular(12), child: Column( @@ -127,7 +209,6 @@ class HomeContent extends StatelessWidget { ), child: Stack( children: [ - // Image placeholder (vous pouvez utiliser Network.image avec imageUrl) Container( width: double.infinity, decoration: BoxDecoration( @@ -137,8 +218,6 @@ class HomeContent extends StatelessWidget { color: color.withValues(alpha: 0.3), ), ), - - // Titre et localisation Positioned( bottom: 16, left: 16, @@ -147,7 +226,7 @@ class HomeContent extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - title, + trip.title, style: const TextStyle( fontSize: 20, fontWeight: FontWeight.bold, @@ -163,11 +242,14 @@ class HomeContent extends StatelessWidget { size: 16, ), const SizedBox(width: 4), - Text( - location, - style: const TextStyle( - fontSize: 14, - color: Colors.white, + Expanded( + child: Text( + trip.location, + style: const TextStyle( + fontSize: 14, + color: Colors.white, + ), + overflow: TextOverflow.ellipsis, ), ), ], @@ -185,6 +267,20 @@ class HomeContent extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + // Description + if (trip.description.isNotEmpty) ...[ + Text( + trip.description, + style: TextStyle( + fontSize: 14, + color: Colors.grey[700], + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + SizedBox(height: 12), + ], + // Dates Row( children: [ @@ -195,7 +291,7 @@ class HomeContent extends StatelessWidget { ), SizedBox(width: 8), Text( - dates, + '${trip.startDate.day}/${trip.startDate.month}/${trip.startDate.year} - ${trip.endDate.day}/${trip.endDate.month}/${trip.endDate.year}', style: TextStyle( fontSize: 14, color: Colors.grey[600], @@ -209,53 +305,15 @@ class HomeContent extends StatelessWidget { // Participants Row( - crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon(Icons.group, size: 16, color: Colors.grey[600]), SizedBox(width: 8), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '${participants.length} participant${participants.length > 1 ? 's' : ''}', - style: TextStyle( - fontSize: 14, - color: Colors.grey[600], - fontWeight: FontWeight.w500, - ), - ), - SizedBox(height: 4), - Wrap( - spacing: 4, - children: participants - .take(3) - .map( - (name) => Chip( - label: Text( - name, - style: TextStyle(fontSize: 12), - ), - backgroundColor: color.withValues( - alpha: 0.1, - ), - materialTapTargetSize: - MaterialTapTargetSize.shrinkWrap, - visualDensity: VisualDensity.compact, - ), - ) - .toList(), - ), - if (participants.length > 3) - Text( - '+${participants.length - 3} autres', - style: TextStyle( - fontSize: 12, - color: Colors.grey[500], - fontStyle: FontStyle.italic, - ), - ), - ], + Text( + '${trip.participants.length} participant${trip.participants.length > 0 ? 's' : ''}', + style: TextStyle( + fontSize: 14, + color: Colors.grey[600], + fontWeight: FontWeight.w500, ), ), ], @@ -263,40 +321,40 @@ class HomeContent extends StatelessWidget { SizedBox(height: 12), - // Budget + // Budget et statut Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row( - children: [ - Icon(Icons.euro, size: 16, color: Colors.grey[600]), - SizedBox(width: 8), - Text( - 'Budget: ${budget.toStringAsFixed(2)}€', - style: TextStyle( - fontSize: 14, - color: Colors.grey[600], - fontWeight: FontWeight.w500, + if (trip.budget! > 0) + Row( + children: [ + Icon(Icons.euro, size: 16, color: Colors.grey[600]), + SizedBox(width: 8), + Text( + 'Budget: ${trip.budget?.toStringAsFixed(2)}€', + style: TextStyle( + fontSize: 14, + color: Colors.grey[600], + fontWeight: FontWeight.w500, + ), ), - ), - ], - ), + ], + ), - // Statut Container( padding: EdgeInsets.symmetric( horizontal: 8, vertical: 4, ), decoration: BoxDecoration( - color: Colors.green.withValues(alpha: 0.1), + color: _getStatusColor(trip).withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), ), child: Text( - 'En cours', + _getStatusText(trip), style: TextStyle( fontSize: 12, - color: Colors.green[700], + color: _getStatusColor(trip), fontWeight: FontWeight.w500, ), ), @@ -312,24 +370,55 @@ class HomeContent extends StatelessWidget { ); } - void _showTravelDetails(BuildContext context, String title) { + 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 { + return Colors.blue; + } + } + + 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 { + return 'À venir'; + } + } + + void _showTravelDetails(BuildContext context, Trip trip) { Navigator.push( context, MaterialPageRoute( builder: (context) => Scaffold( - appBar: AppBar(title: Text(title)), - body: Center( - child: Text('Détails du voyage: $title\n(À implémenter)'), + appBar: AppBar(title: Text(trip.title)), + body: Padding( + padding: EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Destination: ${trip.location}'), + SizedBox(height: 8), + Text('Description: ${trip.description}'), + SizedBox(height: 8), + Text('Dates: ${trip.startDate.day}/${trip.startDate.month}/${trip.startDate.year} - ${trip.endDate.day}/${trip.endDate.month}/${trip.endDate.year}'), + SizedBox(height: 8), + Text('Budget: ${trip.budget}€'), + SizedBox(height: 8), + Text('Participants: ${trip.participants.length}'), + SizedBox(height: 16), + Text('(Détails complets à implémenter)'), + ], + ), ), ), ), ); } - - void _showCreateTravelDialog(BuildContext context) { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => CreateTripContent()), - ); - } } diff --git a/lib/models/trip.dart b/lib/models/trip.dart index dc40d73..de2cf16 100644 --- a/lib/models/trip.dart +++ b/lib/models/trip.dart @@ -36,17 +36,51 @@ class Trip { title: map['title'] ?? '', description: map['description'] ?? '', location: map['location'] ?? '', - startDate: DateTime.fromMillisecondsSinceEpoch(map['startDate']), - endDate: DateTime.fromMillisecondsSinceEpoch(map['endDate']), + startDate: _parseDateTime(map['startDate']), + endDate: _parseDateTime(map['endDate']), budget: map['budget']?.toDouble(), participants: List.from(map['participants'] ?? []), createdBy: map['createdBy'] ?? '', - createdAt: DateTime.fromMillisecondsSinceEpoch(map['createdAt']), - updatedAt: DateTime.fromMillisecondsSinceEpoch(map['updatedAt']), + 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(); + } + + if (dateValue is DateTime) { + return dateValue; + } + + if (dateValue is String) { + try { + // Essayer de parser comme ISO 8601 + return DateTime.parse(dateValue); + } catch (e) { + print('Erreur parsing date string: $dateValue - $e'); + return DateTime.now(); + } + } + + if (dateValue is int) { + try { + // Traiter comme millisecondes + return DateTime.fromMillisecondsSinceEpoch(dateValue); + } catch (e) { + print('Erreur parsing date int: $dateValue - $e'); + return DateTime.now(); + } + } + + print('Type de date non supporté: ${dateValue.runtimeType} - $dateValue'); + return DateTime.now(); + } + // Constructeur pour créer un Trip depuis JSON factory Trip.fromJson(String jsonStr) { Map map = json.decode(jsonStr); diff --git a/lib/providers/user_provider.dart b/lib/providers/user_provider.dart index a7250fa..e30e0f9 100644 --- a/lib/providers/user_provider.dart +++ b/lib/providers/user_provider.dart @@ -71,7 +71,6 @@ class UserProvider extends ChangeNotifier { try { // Structurer les données pour que tous les utilisateurs aient le même format final userDoc = { - 'id': userData['uid'], 'email': userData['email'] ?? '', 'nom': '', // Nom vide pour tous les utilisateurs 'prenom': userData['name'] ?? userData['nom'] ?? 'Utilisateur', // Nom complet dans prenom @@ -79,7 +78,7 @@ class UserProvider extends ChangeNotifier { await _firestore.collection('users').doc(userData['uid']).set(userDoc); - // Retourner l'objet User créé + // Retourner l'objet User créé avec l'ID correct return User.fromMap({...userDoc, 'id': userData['uid']}); } catch (e) { print('Erreur lors de la création de l\'utilisateur: $e'); @@ -87,6 +86,25 @@ class UserProvider extends ChangeNotifier { } } + // Méthode pour obtenir l'ID d'un utilisateur par son email + Future getUserIdByEmail(String email) async { + try { + final QuerySnapshot querySnapshot = await _firestore + .collection('users') + .where('email', isEqualTo: email.trim()) + .limit(1) + .get(); + + if (querySnapshot.docs.isNotEmpty) { + return querySnapshot.docs.first.id; + } + return null; + } catch (e) { + print('Erreur lors de la recherche de l\'utilisateur par email: $e'); + return null; + } + } + // Initialiser l'utilisateur connecté Future initializeUser() async { firebase_auth.User? firebaseUser = _authService.currentUser; @@ -99,3 +117,4 @@ class UserProvider extends ChangeNotifier { } } } + diff --git a/lib/services/trip_service.dart b/lib/services/trip_service.dart index c810dd9..5a31096 100644 --- a/lib/services/trip_service.dart +++ b/lib/services/trip_service.dart @@ -1,89 +1,239 @@ -import 'dart:convert'; -import 'package:shared_preferences/shared_preferences.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; import '../models/trip.dart'; class TripService { - static const String _tripsKey = 'trips_data'; + final FirebaseFirestore _firestore = FirebaseFirestore.instance; + static const String _tripsCollection = 'trips'; // Charger tous les voyages Future> loadTrips() async { try { - final prefs = await SharedPreferences.getInstance(); - final tripsJson = prefs.getString(_tripsKey); + final QuerySnapshot querySnapshot = await _firestore + .collection(_tripsCollection) + .orderBy('createdAt', descending: true) + .get(); - if (tripsJson == null || tripsJson.isEmpty) return []; - - final List jsonList = json.decode(tripsJson); - return jsonList.map((json) => Trip.fromMap(json)).toList(); + return querySnapshot.docs.map((doc) { + final data = doc.data() as Map; + return Trip.fromMap({...data, 'id': doc.id}); + }).toList(); } catch (e) { - //print('Erreur lors du chargement des voyages: $e'); + print('Erreur lors du chargement des voyages: $e'); return []; } } - // Sauvegarder tous les voyages - Future saveTrips(List trips) async { - try { - final prefs = await SharedPreferences.getInstance(); - final jsonList = trips.map((trip) => trip.toMap()).toList(); - await prefs.setString(_tripsKey, json.encode(jsonList)); - } catch (e) { - //print('Erreur lors de la sauvegarde des voyages: $e'); - throw Exception('Erreur de sauvegarde'); - } - } - // Ajouter un nouveau voyage Future addTrip(Trip trip) async { try { - final trips = await loadTrips(); - - // Générer un ID unique - final newTrip = trip.copyWith( - id: DateTime.now().millisecondsSinceEpoch.toString(), - createdAt: DateTime.now(), - updatedAt: DateTime.now(), - ); - - trips.add(newTrip); - await saveTrips(trips); + final tripData = trip.toMap(); + // Retirer l'ID vide du map + tripData.remove('id'); + + // Les participants contiennent déjà uniquement des IDs + // Pas besoin d'ajouter le créateur car il est déjà inclus + + // Convertir les dates en Timestamp pour Firestore + tripData['startDate'] = Timestamp.fromDate(trip.startDate); + tripData['endDate'] = Timestamp.fromDate(trip.endDate); + tripData['createdAt'] = FieldValue.serverTimestamp(); + tripData['updatedAt'] = FieldValue.serverTimestamp(); + + await _firestore.collection(_tripsCollection).add(tripData); return true; } catch (e) { - //print('Erreur lors de l\'ajout du voyage: $e'); + print('Erreur lors de l\'ajout du voyage: $e'); return false; } } - // Obtenir les voyages d'un utilisateur + // Stream pour écouter les voyages d'un utilisateur en temps réel + Stream> getTripsStreamByUser(String userId, String userEmail) { + print('=== STREAM CRÉÉ ==='); + print('UserId: $userId'); + + return _firestore + .collection(_tripsCollection) + .snapshots() + .map((snapshot) { + print('=== NOUVEAU SNAPSHOT ==='); + print('Nombre de documents: ${snapshot.docs.length}'); + + final List trips = []; + + for (int i = 0; i < snapshot.docs.length; i++) { + var doc = snapshot.docs[i]; + print('\n--- Document $i (${doc.id}) ---'); + + try { + final data = doc.data() as Map; + + // Vérifier si l'utilisateur est impliqué dans ce voyage + final String createdBy = data['createdBy']?.toString() ?? ''; + final List participants = data['participants'] ?? []; + + print('CreatedBy: "$createdBy"'); + print('UserId: "$userId"'); + print('Participants: $participants'); + + bool userIsInvolved = false; + String reason = ''; + + // L'utilisateur est le créateur + if (createdBy == userId) { + userIsInvolved = true; + reason = 'Créateur du voyage'; + } + + // L'utilisateur est dans la liste des participants (par ID uniquement) + if (participants.contains(userId)) { + userIsInvolved = true; + reason = reason.isEmpty ? 'Participant par ID' : '$reason + Participant par ID'; + } + + print('Utilisateur impliqué: $userIsInvolved'); + print('Raison: $reason'); + + if (userIsInvolved) { + print('Tentative de conversion du trip...'); + final trip = _convertDocumentToTrip(doc.id, data); + if (trip != null) { + trips.add(trip); + print('Trip ajouté: ${trip.title}'); + } else { + print('Échec de la conversion du trip'); + } + } else { + print('Utilisateur non impliqué dans ce voyage'); + } + } catch (e, stackTrace) { + print('Erreur lors du traitement du document ${doc.id}: $e'); + print('StackTrace: $stackTrace'); + } + } + + print('\n=== RÉSUMÉ ==='); + print('Trips trouvés: ${trips.length}'); + if (trips.isNotEmpty) { + for (int i = 0; i < trips.length; i++) { + print(' ${i+1}. ${trips[i].title} (${trips[i].id})'); + } + } + + // Trier par date de création (les plus récents en premier) + trips.sort((a, b) { + try { + return b.createdAt.compareTo(a.createdAt); + } catch (e) { + print('Erreur lors du tri: $e'); + return 0; + } + }); + + return trips; + }).handleError((error, stackTrace) { + print('Erreur dans le stream: $error'); + print('StackTrace: $stackTrace'); + return []; + }); + } + + // Obtenir les voyages d'un utilisateur (version simplifiée) Future> getTripsByUser(String userId) async { try { - final trips = await loadTrips(); - return trips - .where( - (trip) => - trip.createdBy == userId || trip.participants.contains(userId), - ) - .toList(); + print('Récupération des voyages pour userId: $userId'); + + // Récupérer d'abord les voyages créés par l'utilisateur + final QuerySnapshot createdTrips = await _firestore + .collection(_tripsCollection) + .where('createdBy', isEqualTo: userId) + .get(); + + final List trips = []; + + for (var doc in createdTrips.docs) { + try { + final data = doc.data() as Map; + final trip = _convertDocumentToTrip(doc.id, data); + if (trip != null) { + trips.add(trip); + } + } catch (e) { + print('Erreur lors de la conversion du voyage créé ${doc.id}: $e'); + } + } + + // Trier par date de création + trips.sort((a, b) { + try { + return b.createdAt.compareTo(a.createdAt); + } catch (e) { + return 0; + } + }); + return trips; } catch (e) { - //print('Erreur lors de la récupération des voyages: $e'); + print('Erreur lors de la récupération des voyages: $e'); return []; } } + // Méthode helper pour convertir un document Firestore en Trip + Trip? _convertDocumentToTrip(String docId, Map data) { + print('\n=== CONVERSION TRIP $docId ==='); + + try { + // Créer une copie des données pour ne pas modifier l'original + Map processedData = Map.from(data); + + // Convertir les Timestamps Firestore en String ISO + if (processedData['createdAt'] is Timestamp) { + processedData['createdAt'] = (processedData['createdAt'] as Timestamp).toDate().toIso8601String(); + } + + if (processedData['updatedAt'] is Timestamp) { + processedData['updatedAt'] = (processedData['updatedAt'] as Timestamp).toDate().toIso8601String(); + } + + if (processedData['startDate'] is Timestamp) { + processedData['startDate'] = (processedData['startDate'] as Timestamp).toDate().toIso8601String(); + } + + if (processedData['endDate'] is Timestamp) { + processedData['endDate'] = (processedData['endDate'] as Timestamp).toDate().toIso8601String(); + } + + // Assurer que tous les champs requis sont présents + processedData['id'] = docId; + processedData['participants'] = processedData['participants'] ?? []; + processedData['budget'] = (processedData['budget'] is num) ? (processedData['budget'] as num).toDouble() : 0.0; + processedData['description'] = processedData['description'] ?? ''; + processedData['status'] = processedData['status'] ?? 'draft'; + + final trip = Trip.fromMap(processedData); + return trip; + + } catch (e, stackTrace) { + print('❌ Erreur lors de la conversion du document $docId: $e'); + print('StackTrace: $stackTrace'); + return null; + } + } + // Mettre à jour un voyage Future updateTrip(Trip updatedTrip) async { try { - final trips = await loadTrips(); - final index = trips.indexWhere((trip) => trip.id == updatedTrip.id); + final tripData = updatedTrip.toMap(); + tripData['updatedAt'] = FieldValue.serverTimestamp(); + tripData.remove('id'); // Retirer l'ID des données à mettre à jour - if (index != -1) { - trips[index] = updatedTrip.copyWith(updatedAt: DateTime.now()); - await saveTrips(trips); - return true; - } - return false; + await _firestore + .collection(_tripsCollection) + .doc(updatedTrip.id) + .update(tripData); + return true; } catch (e) { - //print('Erreur lors de la mise à jour du voyage: $e'); + print('Erreur lors de la mise à jour du voyage: $e'); return false; } } @@ -91,18 +241,59 @@ class TripService { // Supprimer un voyage Future deleteTrip(String tripId) async { try { - final trips = await loadTrips(); - final initialLength = trips.length; - trips.removeWhere((trip) => trip.id == tripId); - - if (trips.length < initialLength) { - await saveTrips(trips); - return true; - } - return false; + await _firestore.collection(_tripsCollection).doc(tripId).delete(); + return true; } catch (e) { - //print('Erreur lors de la suppression du voyage: $e'); + print('Erreur lors de la suppression du voyage: $e'); + return false; + } + } + + // Obtenir un voyage par son ID + Future getTripById(String tripId) async { + try { + final DocumentSnapshot doc = await _firestore + .collection(_tripsCollection) + .doc(tripId) + .get(); + + if (doc.exists) { + final data = doc.data() as Map; + return Trip.fromMap({...data, 'id': doc.id}); + } + return null; + } catch (e) { + print('Erreur lors de la récupération du voyage: $e'); + return null; + } + } + + // Ajouter un participant à un voyage + Future addParticipant(String tripId, String userId) async { + try { + await _firestore.collection(_tripsCollection).doc(tripId).update({ + 'participants': FieldValue.arrayUnion([userId]), + 'updatedAt': FieldValue.serverTimestamp(), + }); + return true; + } catch (e) { + print('Erreur lors de l\'ajout du participant: $e'); + return false; + } + } + + // Retirer un participant d'un voyage + Future removeParticipant(String tripId, String userId) async { + try { + await _firestore.collection(_tripsCollection).doc(tripId).update({ + 'participants': FieldValue.arrayRemove([userId]), + 'updatedAt': FieldValue.serverTimestamp(), + }); + return true; + } catch (e) { + print('Erreur lors du retrait du participant: $e'); return false; } } } +