From 83aed85fea644d7088016aa7a10ccadd8851c9e5 Mon Sep 17 00:00:00 2001 From: Van Leemput Dayron Date: Mon, 3 Nov 2025 11:33:25 +0100 Subject: [PATCH] feat: Integrate ErrorService for improved error handling and add imageUrl field to Trip model --- lib/components/home/home_content.dart | 339 ++++++++++-------- .../home/show_trip_details_content.dart | 13 +- lib/models/trip.dart | 45 ++- 3 files changed, 223 insertions(+), 174 deletions(-) diff --git a/lib/components/home/home_content.dart b/lib/components/home/home_content.dart index d0ac910..df375f0 100644 --- a/lib/components/home/home_content.dart +++ b/lib/components/home/home_content.dart @@ -10,7 +10,7 @@ import '../../blocs/trip/trip_event.dart'; import '../../models/trip.dart'; /// Home content widget for the main application dashboard. -/// +/// /// This widget serves as the primary content area of the home screen, /// displaying user trips and providing navigation to trip management /// features. Key functionality includes: @@ -18,7 +18,7 @@ import '../../models/trip.dart'; /// - Creating new trips /// - Viewing trip details /// - Managing trip state with proper user authentication -/// +/// /// The widget maintains state persistence using AutomaticKeepAliveClientMixin /// to preserve content when switching between tabs. class HomeContent extends StatefulWidget { @@ -29,11 +29,12 @@ class HomeContent extends StatefulWidget { State createState() => _HomeContentState(); } -class _HomeContentState extends State with AutomaticKeepAliveClientMixin { +class _HomeContentState extends State + with AutomaticKeepAliveClientMixin { /// Preserves widget state when switching between tabs @override bool get wantKeepAlive => true; - + /// Flag to prevent duplicate trip loading operations bool _hasLoadedTrips = false; @@ -47,7 +48,7 @@ class _HomeContentState extends State with AutomaticKeepAliveClient } /// Loads trips if a user is currently loaded and trips haven't been loaded yet. - /// + /// /// Checks the current user state and initiates trip loading if the user is /// authenticated and trips haven't been loaded previously. This prevents /// duplicate loading operations. @@ -56,7 +57,9 @@ class _HomeContentState extends State with AutomaticKeepAliveClient final userState = context.read().state; if (userState is UserLoaded) { _hasLoadedTrips = true; - context.read().add(LoadTripsByUserId(userId: userState.user.id)); + context.read().add( + LoadTripsByUserId(userId: userState.user.id), + ); } } } @@ -64,15 +67,11 @@ class _HomeContentState extends State with AutomaticKeepAliveClient @override Widget build(BuildContext context) { super.build(context); // Important pour AutomaticKeepAliveClientMixin - + return BlocBuilder( builder: (context, userState) { if (userState is UserLoading) { - return Scaffold( - body: Center( - child: CircularProgressIndicator(), - ), - ); + return Scaffold(body: Center(child: CircularProgressIndicator())); } if (userState is UserError) { @@ -91,11 +90,7 @@ class _HomeContentState extends State with AutomaticKeepAliveClient } if (userState is! UserLoaded) { - return Scaffold( - body: Center( - child: Text('Veuillez vous connecter'), - ), - ); + return Scaffold(body: Center(child: Text('Veuillez vous connecter'))); } final user = userState.user; @@ -137,7 +132,9 @@ class _HomeContentState extends State with AutomaticKeepAliveClient return Scaffold( body: RefreshIndicator( onRefresh: () async { - context.read().add(LoadTripsByUserId(userId: user.id)); + context.read().add( + LoadTripsByUserId(userId: user.id), + ); await Future.delayed(Duration(milliseconds: 500)); }, child: SingleChildScrollView( @@ -149,21 +146,21 @@ class _HomeContentState extends State with AutomaticKeepAliveClient Text( 'Bonjour ${user.prenom} !', style: TextStyle( - fontSize: 28, + fontSize: 28, fontWeight: FontWeight.bold, - color: Theme.of(context).brightness == Brightness.dark - ? Colors.white - : Colors.black, + color: Theme.of(context).brightness == Brightness.dark + ? Colors.white + : Colors.black, ), ), const SizedBox(height: 8), Text( 'Vos voyages', style: TextStyle( - fontSize: 16, - color: Theme.of(context).brightness == Brightness.dark - ? Colors.white70 - : Colors.grey[600], + fontSize: 16, + color: Theme.of(context).brightness == Brightness.dark + ? Colors.white70 + : Colors.grey[600], ), ), const SizedBox(height: 20), @@ -192,7 +189,9 @@ class _HomeContentState extends State with AutomaticKeepAliveClient final result = await Navigator.push( context, - MaterialPageRoute(builder: (context) => const CreateTripContent()), + MaterialPageRoute( + builder: (context) => const CreateTripContent(), + ), ); if (result == true && mounted) { @@ -203,7 +202,8 @@ class _HomeContentState extends State with AutomaticKeepAliveClient foregroundColor: Colors.white, child: const Icon(Icons.add), ), - floatingActionButtonLocation: FloatingActionButtonLocation.endFloat, + floatingActionButtonLocation: + FloatingActionButtonLocation.endFloat, ); }, ); @@ -274,9 +274,7 @@ class _HomeContentState extends State with AutomaticKeepAliveClient } Widget _buildTripsList(List trips) { - return Column( - children: trips.map((trip) => _buildTripCard(trip)).toList(), - ); + return Column(children: trips.map((trip) => _buildTripCard(trip)).toList()); } Widget _buildTripCard(Trip trip) { @@ -287,141 +285,186 @@ class _HomeContentState extends State with AutomaticKeepAliveClient return Card( margin: EdgeInsets.only(bottom: 12), elevation: 2, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - child: InkWell( - onTap: () async { - final result = await Navigator.push( - context, - MaterialPageRoute( - builder: (context) => ShowTripDetailsContent(trip: trip), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Image en haut de la carte + Container( + height: 200, + width: double.infinity, + decoration: BoxDecoration( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(12), + topRight: Radius.circular(12), + ), + color: Colors.grey[300], ), - ); - - if (result == true && mounted) { - final userState = context.read().state; - if (userState is UserLoaded) { - context.read().add(LoadTripsByUserId(userId: userState.user.id)); - } - } - }, - borderRadius: BorderRadius.circular(12), - child: Padding( - padding: EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - trip.title, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: textColor, + child: ClipRRect( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(12), + topRight: Radius.circular(12), + ), + child: trip.imageUrl != null && trip.imageUrl!.isNotEmpty + ? Image.network( + trip.imageUrl!, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Container( + color: Colors.grey[300], + child: Icon( + Icons.image_not_supported, + size: 50, + color: Colors.grey[600], ), - ), - SizedBox(height: 4), - Row( - children: [ - Icon(Icons.location_on, size: 16, color: subtextColor), - SizedBox(width: 4), - Text( - trip.location, - style: TextStyle(color: subtextColor), - ), - ], - ), - ], - ), - ), - Container( - padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6), - decoration: BoxDecoration( - color: _getStatusColor(trip).withValues(alpha: 0.2), - borderRadius: BorderRadius.circular(12), - ), - child: Text( - _getStatusText(trip), - style: TextStyle( - color: _getStatusColor(trip), - fontWeight: FontWeight.bold, - fontSize: 12, + ); + }, + ) + : Container( + color: Colors.grey[300], + child: Icon( + Icons.travel_explore, + size: 50, + color: Colors.grey[600], ), ), + ), + ), + + // Contenu de la carte + Padding( + padding: EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Nom du voyage + Text( + trip.title, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: textColor, ), - ], - ), - 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), + + // Section dates, participants et bouton Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Icon(Icons.euro, size: 16, color: subtextColor), - SizedBox(width: 4), - Text( - '${trip.budget!.toStringAsFixed(2)} €', - style: TextStyle(fontSize: 14, color: subtextColor), + // Colonne gauche : dates et participants + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Dates + 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, + ), + ), + ], + ), + + SizedBox(height: 8), + + // Nombre de participants + 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, + ), + ), + ], + ), + ], + ), + ), + + // Bouton "Voir" à droite + ElevatedButton( + onPressed: () async { + final result = await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + ShowTripDetailsContent(trip: trip), + ), + ); + + if (result == true && mounted) { + final userState = context.read().state; + if (userState is UserLoaded) { + context.read().add( + LoadTripsByUserId(userId: userState.user.id), + ); + } + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.primary, + foregroundColor: Theme.of( + context, + ).colorScheme.onPrimary, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + padding: EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + ), + child: Text('Voir', style: TextStyle(fontSize: 14)), ), ], ), ], - 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), - ), - ], - ), - ], + ), ), - ), + ], ), ); } - Color _getStatusColor(Trip trip) { - final now = DateTime.now(); - if (now.isBefore(trip.startDate)) { - return Colors.blue; - } else if (now.isAfter(trip.endDate)) { - return Colors.grey; - } else { - return Colors.green; - } - } + // Color _getStatusColor(Trip trip) { + // final now = DateTime.now(); + // 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 (now.isBefore(trip.startDate)) { - return 'À venir'; - } else if (now.isAfter(trip.endDate)) { - return 'Terminé'; - } else { - return 'En cours'; - } - } + // String _getStatusText(Trip trip) { + // final now = DateTime.now(); + // 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}'; + return '${date.day.toString().padLeft(2, '0')}/${date.month.toString().padLeft(2, '0')}/${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 00e64fc..a03e014 100644 --- a/lib/components/home/show_trip_details_content.dart +++ b/lib/components/home/show_trip_details_content.dart @@ -4,6 +4,7 @@ import 'package:travel_mate/blocs/trip/trip_bloc.dart'; import 'package:travel_mate/blocs/trip/trip_event.dart'; import 'package:travel_mate/components/home/create_trip_content.dart'; import 'package:travel_mate/models/trip.dart'; +import 'package:travel_mate/services/error_service.dart'; import 'package:url_launcher/url_launcher.dart'; // Ajouter cet import import 'package:travel_mate/components/map/map_content.dart'; // Ajouter cet import si la page carte existe @@ -16,6 +17,8 @@ class ShowTripDetailsContent extends StatefulWidget { } class _ShowTripDetailsContentState extends State { + final ErrorService _errorService = ErrorService(); + // Méthode pour ouvrir la carte interne void _openInternalMap() { Navigator.push( @@ -45,13 +48,9 @@ class _ShowTripDetailsContentState extends State { } else { // Si rien ne marche, afficher un message d'erreur if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text( - 'Impossible d\'ouvrir Google Maps. Vérifiez que l\'application est installée.', - ), - backgroundColor: Colors.red, - ), + _errorService.showError( + message: + 'Impossible d\'ouvrir Google Maps, veuillez vérifier que l\'application est installée.', ); } } diff --git a/lib/models/trip.dart b/lib/models/trip.dart index 4853194..514d0f6 100644 --- a/lib/models/trip.dart +++ b/lib/models/trip.dart @@ -2,49 +2,51 @@ import 'dart:convert'; import 'package:cloud_firestore/cloud_firestore.dart'; /// Model representing a travel trip in the application. -/// +/// /// This class encapsulates all trip-related information including dates, /// location, participants, and budget details. It provides serialization /// methods for Firebase operations and supports trip lifecycle management. class Trip { /// Unique identifier for the trip (usually Firestore document ID). final String? id; - + /// Title or name of the trip. final String title; - + /// Detailed description of the trip. final String description; - + /// Trip destination or location. final String location; - + /// Trip start date and time. final DateTime startDate; - + /// Trip end date and time. final DateTime endDate; - + /// Optional budget for the trip in the local currency. final double? budget; - + /// List of participant user IDs. final List participants; - + /// User ID of the trip creator. final String createdBy; - + /// Timestamp when the trip was created. final DateTime createdAt; - + /// Timestamp when the trip was last updated. final DateTime updatedAt; - + /// Current status of the trip (e.g., 'draft', 'active', 'completed'). final String status; + final String? imageUrl; + /// Creates a new [Trip] instance. - /// + /// /// Most fields are required except [id] and [budget]. /// [status] defaults to 'draft' for new trips. Trip({ @@ -60,32 +62,33 @@ class Trip { required this.createdAt, required this.updatedAt, this.status = 'draft', + this.imageUrl, }); // 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(); } - + // Si c'est un int (millisecondes depuis epoch) if (value is int) { return DateTime.fromMillisecondsSinceEpoch(value); } - + // Si c'est un String (ISO 8601) if (value is String) { return DateTime.parse(value); } - + // Si c'est déjà un DateTime if (value is DateTime) { return value; } - + // Par défaut return DateTime.now(); } @@ -106,6 +109,7 @@ class Trip { createdAt: _parseDateTime(map['createdAt']), updatedAt: _parseDateTime(map['updatedAt']), status: map['status'] as String? ?? 'draft', + imageUrl: map['imageUrl'] as String?, ); } catch (e) { rethrow; @@ -126,6 +130,7 @@ class Trip { 'createdAt': Timestamp.fromDate(createdAt), 'updatedAt': Timestamp.fromDate(updatedAt), 'status': status, + 'imageUrl': imageUrl, }; } @@ -148,6 +153,7 @@ class Trip { DateTime? createdAt, DateTime? updatedAt, String? status, + String? imageUrl, }) { return Trip( id: id ?? this.id, @@ -162,6 +168,7 @@ class Trip { createdAt: createdAt ?? this.createdAt, updatedAt: updatedAt ?? this.updatedAt, status: status ?? this.status, + imageUrl: imageUrl ?? this.imageUrl, ); } @@ -236,4 +243,4 @@ class Trip { @override int get hashCode => id.hashCode; -} \ No newline at end of file +}