Add UserStateWrapper and ProfileImageService for user state management and profile image handling

This commit is contained in:
Van Leemput Dayron
2025-11-05 09:31:58 +01:00
parent 30dca05e15
commit 75c12e35a5
6 changed files with 275 additions and 131 deletions

View File

@@ -237,16 +237,16 @@ class _AddActivityBottomSheetState extends State<AddActivityBottomSheet> {
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: isDarkMode
? Colors.white.withOpacity(0.2)
: Colors.black.withOpacity(0.2),
? Colors.white.withValues(alpha: 0.2)
: Colors.black.withValues(alpha: 0.2),
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: isDarkMode
? Colors.white.withOpacity(0.2)
: Colors.black.withOpacity(0.2),
? Colors.white.withValues(alpha: 0.2)
: Colors.black.withValues(alpha: 0.2),
),
),
focusedBorder: OutlineInputBorder(
@@ -267,7 +267,9 @@ class _AddActivityBottomSheetState extends State<AddActivityBottomSheet> {
decoration: BoxDecoration(
color: theme.colorScheme.surface,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: theme.colorScheme.outline.withOpacity(0.5)),
border: Border.all(
color: theme.colorScheme.outline.withValues(alpha: 0.5),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -275,7 +277,7 @@ class _AddActivityBottomSheetState extends State<AddActivityBottomSheet> {
Text(
'Sélectionnez une catégorie',
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface.withOpacity(0.7),
color: theme.colorScheme.onSurface.withValues(alpha: 0.7),
),
),
const SizedBox(height: 12),

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:travel_mate/components/widgets/user_state_widget.dart';
import '../../blocs/user/user_bloc.dart';
import '../../blocs/user/user_state.dart' as user_state;
import '../../blocs/user/user_event.dart' as user_event;
@@ -10,22 +11,8 @@ class ProfileContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<UserBloc, user_state.UserState>(
builder: (context, state) {
if (state is user_state.UserLoading) {
return Center(child: CircularProgressIndicator());
}
if (state is user_state.UserError) {
return Center(child: Text('Erreur: ${state.message}'));
}
if (state is! user_state.UserLoaded) {
return Center(child: Text('Aucun utilisateur connecté'));
}
final user = state.user;
return UserStateWrapper(
builder: (context, user) {
return Column(
children: [
// Section titre
@@ -181,7 +168,10 @@ class ProfileContent extends StatelessWidget {
);
}
void _showChangePasswordDialog(BuildContext context, user_state.UserModel user) {
void _showChangePasswordDialog(
BuildContext context,
user_state.UserModel user,
) {
final currentPasswordController = TextEditingController();
final newPasswordController = TextEditingController();
final confirmPasswordController = TextEditingController();
@@ -210,7 +200,9 @@ class ProfileContent extends StatelessWidget {
TextField(
controller: confirmPasswordController,
obscureText: true,
decoration: InputDecoration(labelText: 'Confirmer le mot de passe'),
decoration: InputDecoration(
labelText: 'Confirmer le mot de passe',
),
),
],
),
@@ -233,7 +225,8 @@ class ProfileContent extends StatelessWidget {
return;
}
if (newPasswordController.text != confirmPasswordController.text) {
if (newPasswordController.text !=
confirmPasswordController.text) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Les mots de passe ne correspondent pas'),
@@ -274,7 +267,10 @@ class ProfileContent extends StatelessWidget {
);
}
void _showDeleteAccountDialog(BuildContext context, user_state.UserModel user) {
void _showDeleteAccountDialog(
BuildContext context,
user_state.UserModel user,
) {
final passwordController = TextEditingController();
final authService = AuthService();

View File

@@ -0,0 +1,42 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../blocs/user/user_bloc.dart';
import '../../blocs/user/user_state.dart' as user_state;
class UserStateWrapper extends StatelessWidget {
final Widget Function(BuildContext context, dynamic user) builder;
final Widget? loadingWidget;
final Widget? errorWidget;
final Widget? noUserWidget;
const UserStateWrapper({
super.key,
required this.builder,
this.loadingWidget,
this.errorWidget,
this.noUserWidget,
});
@override
Widget build(BuildContext context) {
return BlocBuilder<UserBloc, user_state.UserState>(
builder: (context, state) {
if (state is user_state.UserLoading) {
return loadingWidget ??
const Center(child: CircularProgressIndicator());
}
if (state is user_state.UserError) {
return errorWidget ?? Center(child: Text('Erreur: ${state.message}'));
}
if (state is! user_state.UserLoaded) {
return noUserWidget ??
const Center(child: Text('Aucun utilisateur connecté.'));
}
return builder(context, state.user);
},
);
}
}

View File

@@ -6,7 +6,8 @@ import '../services/error_service.dart';
/// Service pour rechercher des activités touristiques via Google Places API
class ActivityPlacesService {
static final ActivityPlacesService _instance = ActivityPlacesService._internal();
static final ActivityPlacesService _instance =
ActivityPlacesService._internal();
factory ActivityPlacesService() => _instance;
ActivityPlacesService._internal();
@@ -23,14 +24,16 @@ class ActivityPlacesService {
int offset = 0,
}) async {
try {
print('ActivityPlacesService: Recherche d\'activités pour: $destination (max: $maxResults, offset: $offset)');
print(
'ActivityPlacesService: Recherche d\'activités pour: $destination (max: $maxResults, offset: $offset)',
);
// 1. Géocoder la destination
final coordinates = await _geocodeDestination(destination);
// 2. Rechercher les activités par catégorie ou toutes les catégories
List<Activity> allActivities = [];
if (category != null) {
final activities = await _searchByCategory(
coordinates['lat']!,
@@ -66,22 +69,30 @@ class ActivityPlacesService {
final uniqueActivities = _removeDuplicates(allActivities);
uniqueActivities.sort((a, b) => (b.rating ?? 0).compareTo(a.rating ?? 0));
print('ActivityPlacesService: ${uniqueActivities.length} activités trouvées au total');
print(
'ActivityPlacesService: ${uniqueActivities.length} activités trouvées au total',
);
// 4. Appliquer la pagination
final startIndex = offset;
final endIndex = (startIndex + maxResults).clamp(0, uniqueActivities.length);
final endIndex = (startIndex + maxResults).clamp(
0,
uniqueActivities.length,
);
if (startIndex >= uniqueActivities.length) {
print('ActivityPlacesService: Offset $startIndex dépasse le nombre total (${uniqueActivities.length})');
print(
'ActivityPlacesService: Offset $startIndex dépasse le nombre total (${uniqueActivities.length})',
);
return [];
}
final paginatedResults = uniqueActivities.sublist(startIndex, endIndex);
print('ActivityPlacesService: Retour de ${paginatedResults.length} activités (offset: $offset, max: $maxResults)');
return paginatedResults;
final paginatedResults = uniqueActivities.sublist(startIndex, endIndex);
print(
'ActivityPlacesService: Retour de ${paginatedResults.length} activités (offset: $offset, max: $maxResults)',
);
return paginatedResults;
} catch (e) {
print('ActivityPlacesService: Erreur lors de la recherche: $e');
_errorService.logError('activity_places_service', e);
@@ -99,19 +110,20 @@ class ActivityPlacesService {
}
final encodedDestination = Uri.encodeComponent(destination);
final url = 'https://maps.googleapis.com/maps/api/geocode/json?address=$encodedDestination&key=$_apiKey';
final url =
'https://maps.googleapis.com/maps/api/geocode/json?address=$encodedDestination&key=$_apiKey';
print('ActivityPlacesService: Géocodage de "$destination"');
print('ActivityPlacesService: URL = $url');
final response = await http.get(Uri.parse(url));
print('ActivityPlacesService: Status code = ${response.statusCode}');
if (response.statusCode == 200) {
final data = json.decode(response.body);
print('ActivityPlacesService: Réponse géocodage = ${data['status']}');
if (data['status'] == 'OK' && data['results'].isNotEmpty) {
final location = data['results'][0]['geometry']['location'];
final coordinates = {
@@ -121,13 +133,17 @@ class ActivityPlacesService {
print('ActivityPlacesService: Coordonnées trouvées = $coordinates');
return coordinates;
} else {
print('ActivityPlacesService: Erreur API = ${data['error_message'] ?? data['status']}');
print(
'ActivityPlacesService: Erreur API = ${data['error_message'] ?? data['status']}',
);
if (data['status'] == 'REQUEST_DENIED') {
throw Exception('🔑 Clé API non autorisée. Activez les APIs suivantes dans Google Cloud Console:\n'
'• Geocoding API\n'
'• Places API\n'
'• Maps JavaScript API\n'
'Puis ajoutez des restrictions appropriées.');
throw Exception(
'🔑 Clé API non autorisée. Activez les APIs suivantes dans Google Cloud Console:\n'
'• Geocoding API\n'
'• Places API\n'
'• Maps JavaScript API\n'
'Puis ajoutez des restrictions appropriées.',
);
} else if (data['status'] == 'ZERO_RESULTS') {
throw Exception('Aucun résultat trouvé pour cette destination');
} else {
@@ -139,7 +155,7 @@ class ActivityPlacesService {
}
} catch (e) {
print('ActivityPlacesService: Erreur géocodage: $e');
throw e; // Rethrow pour permettre la gestion d'erreur en amont
rethrow; // Rethrow pour permettre la gestion d'erreur en amont
}
}
@@ -152,23 +168,28 @@ class ActivityPlacesService {
int radius,
) async {
try {
final url = 'https://maps.googleapis.com/maps/api/place/nearbysearch/json'
final url =
'https://maps.googleapis.com/maps/api/place/nearbysearch/json'
'?location=$lat,$lng'
'&radius=$radius'
'&type=${category.googlePlaceType}'
'&key=$_apiKey';
final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) {
final data = json.decode(response.body);
if (data['status'] == 'OK') {
final List<Activity> activities = [];
for (final place in data['results']) {
try {
final activity = await _convertPlaceToActivity(place, tripId, category);
final activity = await _convertPlaceToActivity(
place,
tripId,
category,
);
if (activity != null) {
activities.add(activity);
}
@@ -176,11 +197,11 @@ class ActivityPlacesService {
print('ActivityPlacesService: Erreur conversion place: $e');
}
}
return activities;
}
}
return [];
} catch (e) {
print('ActivityPlacesService: Erreur recherche par catégorie: $e');
@@ -200,15 +221,16 @@ class ActivityPlacesService {
// Récupérer les détails supplémentaires
final details = await _getPlaceDetails(placeId);
final geometry = place['geometry']?['location'];
final photos = place['photos'] as List?;
// Obtenir une image de qualité
String? imageUrl;
if (photos != null && photos.isNotEmpty) {
final photoReference = photos.first['photo_reference'];
imageUrl = 'https://maps.googleapis.com/maps/api/place/photo'
imageUrl =
'https://maps.googleapis.com/maps/api/place/photo'
'?maxwidth=800'
'&photoreference=$photoReference'
'&key=$_apiKey';
@@ -218,9 +240,10 @@ class ActivityPlacesService {
id: '', // Sera généré lors de la sauvegarde
tripId: tripId,
name: place['name'] ?? 'Activité inconnue',
description: details?['editorial_summary']?['overview'] ??
details?['formatted_address'] ??
'Découvrez cette activité incontournable !',
description:
details?['editorial_summary']?['overview'] ??
details?['formatted_address'] ??
'Découvrez cette activité incontournable !',
category: category.displayName,
imageUrl: imageUrl,
rating: place['rating']?.toDouble(),
@@ -236,7 +259,6 @@ class ActivityPlacesService {
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
} catch (e) {
print('ActivityPlacesService: Erreur conversion place: $e');
return null;
@@ -246,20 +268,21 @@ class ActivityPlacesService {
/// Récupère les détails d'un lieu
Future<Map<String, dynamic>?> _getPlaceDetails(String placeId) async {
try {
final url = 'https://maps.googleapis.com/maps/api/place/details/json'
final url =
'https://maps.googleapis.com/maps/api/place/details/json'
'?place_id=$placeId'
'&fields=formatted_address,formatted_phone_number,website,opening_hours,editorial_summary'
'&key=$_apiKey';
final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) {
final data = json.decode(response.body);
if (data['status'] == 'OK') {
return data['result'];
}
}
return null;
} catch (e) {
print('ActivityPlacesService: Erreur récupération détails: $e');
@@ -277,10 +300,10 @@ class ActivityPlacesService {
/// Parse les heures d'ouverture
List<String> _parseOpeningHours(Map<String, dynamic>? openingHours) {
if (openingHours == null) return [];
final weekdayText = openingHours['weekday_text'] as List?;
if (weekdayText == null) return [];
return weekdayText.cast<String>();
}
@@ -303,33 +326,40 @@ class ActivityPlacesService {
int radius = 5000,
}) async {
try {
print('ActivityPlacesService: Recherche textuelle: $query à $destination');
print(
'ActivityPlacesService: Recherche textuelle: $query à $destination',
);
// Géocoder la destination
final coordinates = await _geocodeDestination(destination);
final encodedQuery = Uri.encodeComponent(query);
final url = 'https://maps.googleapis.com/maps/api/place/textsearch/json'
final url =
'https://maps.googleapis.com/maps/api/place/textsearch/json'
'?query=$encodedQuery in $destination'
'&location=${coordinates['lat']},${coordinates['lng']}'
'&radius=$radius'
'&key=$_apiKey';
final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) {
final data = json.decode(response.body);
if (data['status'] == 'OK') {
final List<Activity> activities = [];
for (final place in data['results']) {
try {
// Déterminer la catégorie basée sur les types du lieu
final types = List<String>.from(place['types'] ?? []);
final category = _determineCategoryFromTypes(types);
final activity = await _convertPlaceToActivity(place, tripId, category);
final activity = await _convertPlaceToActivity(
place,
tripId,
category,
);
if (activity != null) {
activities.add(activity);
}
@@ -337,11 +367,11 @@ class ActivityPlacesService {
print('ActivityPlacesService: Erreur conversion place: $e');
}
}
return activities;
}
}
return [];
} catch (e) {
print('ActivityPlacesService: Erreur recherche textuelle: $e');
@@ -358,7 +388,7 @@ class ActivityPlacesService {
}
}
}
// Catégories par défaut basées sur des types communs
if (types.contains('restaurant') || types.contains('food')) {
return ActivityCategory.restaurant;
@@ -371,7 +401,7 @@ class ActivityPlacesService {
} else if (types.contains('night_club') || types.contains('bar')) {
return ActivityCategory.nightlife;
}
return ActivityCategory.attraction; // Par défaut
}
@@ -388,14 +418,18 @@ class ActivityPlacesService {
}) async {
try {
double lat, lng;
// Utiliser les coordonnées fournies ou géocoder la destination
if (latitude != null && longitude != null) {
lat = latitude;
lng = longitude;
print('ActivityPlacesService: Utilisation des coordonnées pré-géolocalisées: $lat, $lng');
print(
'ActivityPlacesService: Utilisation des coordonnées pré-géolocalisées: $lat, $lng',
);
} else if (destination != null) {
print('ActivityPlacesService: Géolocalisation de la destination: $destination');
print(
'ActivityPlacesService: Géolocalisation de la destination: $destination',
);
final coordinates = await _geocodeDestination(destination);
lat = coordinates['lat']!;
lng = coordinates['lng']!;
@@ -403,7 +437,9 @@ class ActivityPlacesService {
throw Exception('Destination ou coordonnées requises');
}
print('ActivityPlacesService: Recherche paginée aux coordonnées: $lat, $lng (page: ${nextPageToken ?? "première"})');
print(
'ActivityPlacesService: Recherche paginée aux coordonnées: $lat, $lng (page: ${nextPageToken ?? "première"})',
);
// 2. Rechercher les activités par catégorie avec pagination
if (category != null) {
@@ -449,7 +485,8 @@ class ActivityPlacesService {
String? nextPageToken,
) async {
try {
String url = 'https://maps.googleapis.com/maps/api/place/nearbysearch/json'
String url =
'https://maps.googleapis.com/maps/api/place/nearbysearch/json'
'?location=$lat,$lng'
'&radius=$radius'
'&type=${category.googlePlaceType}'
@@ -460,20 +497,24 @@ class ActivityPlacesService {
}
final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) {
final data = json.decode(response.body);
if (data['status'] == 'OK') {
final List<Activity> activities = [];
final results = data['results'] as List? ?? [];
// Limiter à pageSize résultats
final limitedResults = results.take(pageSize).toList();
for (final place in limitedResults) {
try {
final activity = await _convertPlaceToActivity(place, tripId, category);
final activity = await _convertPlaceToActivity(
place,
tripId,
category,
);
if (activity != null) {
activities.add(activity);
}
@@ -481,7 +522,7 @@ class ActivityPlacesService {
print('ActivityPlacesService: Erreur conversion place: $e');
}
}
return {
'activities': activities,
'nextPageToken': data['next_page_token'],
@@ -489,7 +530,7 @@ class ActivityPlacesService {
};
}
}
return {
'activities': <Activity>[],
'nextPageToken': null,
@@ -516,7 +557,8 @@ class ActivityPlacesService {
) async {
try {
// Pour toutes les catégories, on utilise une recherche plus générale
String url = 'https://maps.googleapis.com/maps/api/place/nearbysearch/json'
String url =
'https://maps.googleapis.com/maps/api/place/nearbysearch/json'
'?location=$lat,$lng'
'&radius=$radius'
'&type=tourist_attraction'
@@ -527,24 +569,28 @@ class ActivityPlacesService {
}
final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) {
final data = json.decode(response.body);
if (data['status'] == 'OK') {
final List<Activity> activities = [];
final results = data['results'] as List? ?? [];
// Limiter à pageSize résultats
final limitedResults = results.take(pageSize).toList();
for (final place in limitedResults) {
try {
// Déterminer la catégorie basée sur les types du lieu
final types = List<String>.from(place['types'] ?? []);
final category = _determineCategoryFromTypes(types);
final activity = await _convertPlaceToActivity(place, tripId, category);
final activity = await _convertPlaceToActivity(
place,
tripId,
category,
);
if (activity != null) {
activities.add(activity);
}
@@ -552,7 +598,7 @@ class ActivityPlacesService {
print('ActivityPlacesService: Erreur conversion place: $e');
}
}
return {
'activities': activities,
'nextPageToken': data['next_page_token'],
@@ -560,14 +606,16 @@ class ActivityPlacesService {
};
}
}
return {
'activities': <Activity>[],
'nextPageToken': null,
'hasMoreData': false,
};
} catch (e) {
print('ActivityPlacesService: Erreur recherche toutes catégories paginée: $e');
print(
'ActivityPlacesService: Erreur recherche toutes catégories paginée: $e',
);
return {
'activities': <Activity>[],
'nextPageToken': null,
@@ -575,4 +623,4 @@ class ActivityPlacesService {
};
}
}
}
}

View File

@@ -0,0 +1,54 @@
import 'dart:io';
import 'package:firebase_storage/firebase_storage.dart';
import 'package:image_picker/image_picker.dart';
import 'error_service.dart';
class ProfileImageService {
static final FirebaseStorage _storage = FirebaseStorage.instance;
final ImagePicker _picker = ImagePicker();
final ErrorService _errorService = ErrorService();
Future<String?> uploadCustomProfileImage(
String userId,
XFile imageFile,
) async {
try {
final ref = _storage.ref().child('profile_images').child('$userId.jpg');
final uploadTask = ref.putFile(File(imageFile.path));
final snapshot = await uploadTask;
return await snapshot.ref.getDownloadURL();
} catch (e) {
_errorService.logError(
'ProfileImageService',
'Erreur lors du téléchargement de l\'image de profil pour $userId: $e',
);
return null;
}
}
Future<XFile?> pickProfileImageFromGallery() async {
try {
final XFile? image = await _picker.pickImage(source: ImageSource.gallery);
return image;
} catch (e) {
_errorService.logError(
'ProfileImageService',
'Erreur lors de la sélection de l\'image depuis la galerie: $e',
);
return null;
}
}
Future<XFile?> takePhoto() async {
try {
final XFile? photo = await _picker.pickImage(source: ImageSource.camera);
return photo;
} catch (e) {
_errorService.logError(
'ProfileImageService',
'Erreur lors de la prise de photo: $e',
);
return null;
}
}
}

View File

@@ -11,17 +11,17 @@ class TripImageService {
/// Charge les images manquantes pour une liste de voyages
Future<void> loadMissingImages(List<Trip> trips) async {
final tripsWithoutImage = trips.where(
(trip) => trip.imageUrl == null || trip.imageUrl!.isEmpty
).toList();
final tripsWithoutImage = trips
.where((trip) => trip.imageUrl == null || trip.imageUrl!.isEmpty)
.toList();
if (tripsWithoutImage.isEmpty) {
return;
}
}
for (final trip in tripsWithoutImage) {
try {
await _loadImageForTrip(trip);
// Petite pause entre les requêtes pour éviter de surcharger l'API
await Future.delayed(const Duration(milliseconds: 500));
} catch (e) {
@@ -35,42 +35,40 @@ class TripImageService {
/// Charge l'image pour un voyage spécifique
Future<void> _loadImageForTrip(Trip trip) async {
// D'abord vérifier si une image existe déjà dans le Storage
String? imageUrl = await _placeImageService.getExistingImageUrl(trip.location);
String? imageUrl = await _placeImageService.getExistingImageUrl(
trip.location,
);
// Si aucune image n'existe, en télécharger une nouvelle
if (imageUrl == null) {
imageUrl = await _placeImageService.getPlaceImageUrl(trip.location);
}
imageUrl ??= await _placeImageService.getPlaceImageUrl(trip.location);
if (imageUrl != null && trip.id != null) {
// Mettre à jour le voyage avec l'image (existante ou nouvelle)
final updatedTrip = trip.copyWith(
imageUrl: imageUrl,
updatedAt: DateTime.now(),
);
await _tripRepository.updateTrip(trip.id!, updatedTrip);
} else {
}
} else {}
}
/// Recharge l'image d'un voyage spécifique (force le rechargement)
Future<String?> reloadImageForTrip(Trip trip) async {
try {
final imageUrl = await _placeImageService.getPlaceImageUrl(trip.location);
if (imageUrl != null && trip.id != null) {
final updatedTrip = trip.copyWith(
imageUrl: imageUrl,
updatedAt: DateTime.now(),
);
await _tripRepository.updateTrip(trip.id!, updatedTrip);
return imageUrl;
}
return null;
} catch (e) {
_errorService.logError(
@@ -87,16 +85,15 @@ class TripImageService {
// Récupérer tous les voyages de l'utilisateur
final tripsStream = _tripRepository.getTripsByUserId(userId);
final trips = await tripsStream.first;
// Extraire toutes les URLs d'images utilisées
final usedImageUrls = trips
.where((trip) => trip.imageUrl != null && trip.imageUrl!.isNotEmpty)
.map((trip) => trip.imageUrl!)
.toList();
// Nettoyer les images inutilisées
await _placeImageService.cleanupUnusedImages(usedImageUrls);
} catch (e) {
_errorService.logError(
'TripImageService',
@@ -110,10 +107,12 @@ class TripImageService {
try {
final tripsStream = _tripRepository.getTripsByUserId(userId);
final trips = await tripsStream.first;
final tripsWithImages = trips.where((trip) => trip.imageUrl != null && trip.imageUrl!.isNotEmpty).length;
final tripsWithImages = trips
.where((trip) => trip.imageUrl != null && trip.imageUrl!.isNotEmpty)
.length;
final tripsWithoutImages = trips.length - tripsWithImages;
return {
'totalTrips': trips.length,
'tripsWithImages': tripsWithImages,
@@ -121,7 +120,10 @@ class TripImageService {
'timestamp': DateTime.now().toIso8601String(),
};
} catch (e) {
_errorService.logError('TripImageService', 'Erreur lors de l\'obtention des statistiques: $e');
_errorService.logError(
'TripImageService',
'Erreur lors de l\'obtention des statistiques: $e',
);
return {
'error': e.toString(),
'timestamp': DateTime.now().toIso8601String(),
@@ -140,4 +142,4 @@ class TripImageService {
);
}
}
}
}