feat: Implement centralized error handling with ErrorService; replace print statements with logging in services and blocs

feat: Add ErrorContent widget for displaying error messages in dialogs and bottom sheets
refactor: Update GroupBloc and GroupRepository to utilize ErrorService for error logging
refactor: Enhance user and trip services to log errors using ErrorService
refactor: Clean up debug print statements in GroupContent and related components
This commit is contained in:
Dayron
2025-10-15 11:43:21 +02:00
parent 03ed85bf98
commit 0162eb67f5
12 changed files with 422 additions and 197 deletions

View File

@@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:travel_mate/services/error_service.dart';
import 'group_event.dart'; import 'group_event.dart';
import 'group_state.dart'; import 'group_state.dart';
import '../../repositories/group_repository.dart'; import '../../repositories/group_repository.dart';
@@ -8,6 +9,7 @@ import '../../data/models/group.dart';
class GroupBloc extends Bloc<GroupEvent, GroupState> { class GroupBloc extends Bloc<GroupEvent, GroupState> {
final GroupRepository _repository; final GroupRepository _repository;
StreamSubscription? _groupsSubscription; StreamSubscription? _groupsSubscription;
final _errorService = ErrorService();
GroupBloc(this._repository) : super(GroupInitial()) { GroupBloc(this._repository) : super(GroupInitial()) {
on<LoadGroupsByUserId>(_onLoadGroupsByUserId); on<LoadGroupsByUserId>(_onLoadGroupsByUserId);
@@ -25,32 +27,19 @@ class GroupBloc extends Bloc<GroupEvent, GroupState> {
LoadGroupsByUserId event, LoadGroupsByUserId event,
Emitter<GroupState> emit, Emitter<GroupState> emit,
) async { ) async {
print('===== GroupBloc: _onLoadGroupsByUserId START =====');
try { try {
emit(GroupLoading()); emit(GroupLoading());
print('>>> GroupBloc: État GroupLoading émis');
await _groupsSubscription?.cancel(); await _groupsSubscription?.cancel();
print('>>> GroupBloc: Ancien subscription annulé');
_groupsSubscription = _repository.getGroupsByUserId(event.userId).listen( _groupsSubscription = _repository.getGroupsByUserId(event.userId).listen(
(groups) { (groups) {
print('===== GroupBloc: Stream reçu ${groups.length} groupes =====');
// Utiliser un événement interne au lieu d'émettre directement
add(_GroupsUpdated(groups)); add(_GroupsUpdated(groups));
}, },
onError: (error) { onError: (error) {
print('===== GroupBloc: Erreur stream: $error =====');
add(_GroupsUpdated([], error: error.toString())); add(_GroupsUpdated([], error: error.toString()));
}, },
); );
print('>>> GroupBloc: Subscription créé avec succès');
} catch (e, stackTrace) { } catch (e, stackTrace) {
print('===== GroupBloc: Exception _onLoadGroupsByUserId ====='); _errorService.logError(e.toString(), stackTrace);
print('Exception: $e');
print('StackTrace: $stackTrace');
emit(GroupError(e.toString())); emit(GroupError(e.toString()));
} }
} }
@@ -60,16 +49,11 @@ class GroupBloc extends Bloc<GroupEvent, GroupState> {
_GroupsUpdated event, _GroupsUpdated event,
Emitter<GroupState> emit, Emitter<GroupState> emit,
) async { ) async {
print('===== GroupBloc: _onGroupsUpdated =====');
print('Groupes reçus: ${event.groups.length}');
if (event.error != null) { if (event.error != null) {
print('>>> Émission GroupError: ${event.error}'); _errorService.logError(event.error!, StackTrace.current);
emit(GroupError(event.error!)); emit(GroupError(event.error!));
} else { } else {
print('>>> Émission GroupsLoaded avec ${event.groups.length} groupes');
emit(GroupsLoaded(event.groups)); emit(GroupsLoaded(event.groups));
print('>>> GroupsLoaded émis avec succès !');
} }
} }
@@ -172,7 +156,6 @@ class GroupBloc extends Bloc<GroupEvent, GroupState> {
@override @override
Future<void> close() { Future<void> close() {
print('===== GroupBloc: close() =====');
_groupsSubscription?.cancel(); _groupsSubscription?.cancel();
return super.close(); return super.close();
} }

View File

@@ -0,0 +1,237 @@
import 'package:flutter/material.dart';
class ErrorContent extends StatelessWidget {
final String title;
final String message;
final VoidCallback? onRetry;
final VoidCallback? onClose;
final IconData icon;
final Color? iconColor;
const ErrorContent({
super.key,
this.title = 'Une erreur est survenue',
required this.message,
this.onRetry,
this.onClose,
this.icon = Icons.error_outline,
this.iconColor,
});
@override
Widget build(BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
final defaultIconColor = iconColor ?? Colors.red[400];
return Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[900] : Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 10,
spreadRadius: 2,
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Bouton fermer en haut à droite
if (onClose != null)
Align(
alignment: Alignment.topRight,
child: IconButton(
icon: const Icon(Icons.close),
onPressed: onClose,
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
),
const SizedBox(height: 8),
// Icône d'erreur
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: defaultIconColor?.withValues(alpha: 0.1),
shape: BoxShape.circle,
),
child: Icon(
icon,
size: 48,
color: defaultIconColor,
),
),
const SizedBox(height: 24),
// Titre
Text(
title,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: isDarkMode ? Colors.white : Colors.black87,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 12),
// Message d'erreur
Text(
message,
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
height: 1.5,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
// Boutons d'action
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (onRetry != null) ...[
ElevatedButton.icon(
onPressed: onRetry,
icon: const Icon(Icons.refresh),
label: const Text('Réessayer'),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 12,
),
),
),
if (onClose != null) const SizedBox(width: 12),
],
if (onClose != null)
OutlinedButton.icon(
onPressed: onClose,
icon: const Icon(Icons.close),
label: const Text('Fermer'),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 12,
),
),
),
],
),
],
),
);
}
}
// Fonction helper pour afficher l'erreur en dialog
void showErrorDialog(
BuildContext context, {
String title = 'Une erreur est survenue',
required String message,
VoidCallback? onRetry,
IconData icon = Icons.error_outline,
Color? iconColor,
bool barrierDismissible = true,
}) {
showDialog(
context: context,
barrierDismissible: barrierDismissible,
builder: (BuildContext dialogContext) {
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: ErrorContent(
title: title,
message: message,
icon: icon,
iconColor: iconColor,
onRetry: onRetry != null
? () {
Navigator.of(dialogContext).pop();
onRetry();
}
: null,
onClose: () => Navigator.of(dialogContext).pop(),
),
);
},
);
}
// Fonction helper pour afficher l'erreur en bottom sheet
void showErrorBottomSheet(
BuildContext context, {
String title = 'Une erreur est survenue',
required String message,
VoidCallback? onRetry,
IconData icon = Icons.error_outline,
Color? iconColor,
bool isDismissible = true,
}) {
showModalBottomSheet(
context: context,
isDismissible: isDismissible,
enableDrag: isDismissible,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (BuildContext sheetContext) {
return SafeArea(
child: Padding(
padding: const EdgeInsets.all(16),
child: ErrorContent(
title: title,
message: message,
icon: icon,
iconColor: iconColor,
onRetry: onRetry != null
? () {
Navigator.of(sheetContext).pop();
onRetry();
}
: null,
onClose: () => Navigator.of(sheetContext).pop(),
),
),
);
},
);
}
// Fonction helper pour afficher en SnackBar (pour erreurs mineures)
void showErrorSnackBar(
BuildContext context, {
required String message,
VoidCallback? onRetry,
Duration duration = const Duration(seconds: 4),
}) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: Colors.red[400],
duration: duration,
action: onRetry != null
? SnackBarAction(
label: 'Réessayer',
textColor: Colors.white,
onPressed: onRetry,
)
: null,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
);
}

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:travel_mate/components/error/error_content.dart';
import '../../blocs/user/user_bloc.dart'; import '../../blocs/user/user_bloc.dart';
import '../../blocs/user/user_state.dart' as user_state; import '../../blocs/user/user_state.dart' as user_state;
import '../../blocs/group/group_bloc.dart'; import '../../blocs/group/group_bloc.dart';
@@ -18,55 +19,39 @@ class _GroupContentState extends State<GroupContent> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
print('===== GroupContent: initState =====');
// Charger immédiatement sans attendre le prochain frame // Charger immédiatement sans attendre le prochain frame
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
print('===== PostFrameCallback déclenché =====');
_loadInitialData(); _loadInitialData();
}); });
} }
void _loadInitialData() { void _loadInitialData() {
print('===== _loadInitialData START =====');
try { try {
final userState = context.read<UserBloc>().state; final userState = context.read<UserBloc>().state;
print('UserBloc state type: ${userState.runtimeType}');
print('UserBloc state: $userState');
if (userState is user_state.UserLoaded) { if (userState is user_state.UserLoaded) {
final userId = userState.user.id; final userId = userState.user.id;
print('✓ User chargé, ID: $userId');
print('>>> Envoi de LoadGroupsByUserId <<<');
context.read<GroupBloc>().add(LoadGroupsByUserId(userId)); context.read<GroupBloc>().add(LoadGroupsByUserId(userId));
print('>>> LoadGroupsByUserId envoyé <<<');
} else { } else {
print('✗ UserState n\'est pas UserLoaded'); throw Exception('Utilisateur non connecté');
} }
} catch (e, stackTrace) { } catch (e) {
print('===== ERREUR _loadInitialData ====='); _buildErrorState(e.toString(), '', true);
print('Exception: $e');
print('StackTrace: $stackTrace');
} }
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
print('===== GroupContent: build =====');
return BlocBuilder<UserBloc, user_state.UserState>( return BlocBuilder<UserBloc, user_state.UserState>(
builder: (context, userState) { builder: (context, userState) {
print('>>> UserBloc builder - state type: ${userState.runtimeType}');
if (userState is user_state.UserLoading) { if (userState is user_state.UserLoading) {
print('État: UserLoading');
return const Scaffold( return const Scaffold(
body: Center(child: CircularProgressIndicator()), body: Center(child: CircularProgressIndicator()),
); );
} }
if (userState is user_state.UserError) { if (userState is user_state.UserError) {
print('État: UserError - ${userState.message}');
return Scaffold( return Scaffold(
body: Center( body: Center(
child: Column( child: Column(
@@ -82,23 +67,15 @@ class _GroupContentState extends State<GroupContent> {
} }
if (userState is! user_state.UserLoaded) { if (userState is! user_state.UserLoaded) {
print('État: Utilisateur non connecté');
return const Scaffold( return const Scaffold(
body: Center(child: Text('Utilisateur non connecté')), body: Center(child: Text('Utilisateur non connecté')),
); );
} }
print('✓ État: UserLoaded');
final user = userState.user; final user = userState.user;
return BlocConsumer<GroupBloc, GroupState>( return BlocConsumer<GroupBloc, GroupState>(
listener: (context, groupState) { listener: (context, groupState) {
print('===== GroupBloc LISTENER =====');
print('State type: ${groupState.runtimeType}');
print('State: $groupState');
if (groupState is GroupOperationSuccess) { if (groupState is GroupOperationSuccess) {
print('>>> GroupOperationSuccess: ${groupState.message}');
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text(groupState.message), content: Text(groupState.message),
@@ -106,7 +83,6 @@ class _GroupContentState extends State<GroupContent> {
), ),
); );
} else if (groupState is GroupError) { } else if (groupState is GroupError) {
print('>>> GroupError: ${groupState.message}');
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text(groupState.message), content: Text(groupState.message),
@@ -116,19 +92,6 @@ class _GroupContentState extends State<GroupContent> {
} }
}, },
builder: (context, groupState) { builder: (context, groupState) {
print('===== GroupBloc BUILDER =====');
print('State type: ${groupState.runtimeType}');
print('State: $groupState');
// TEST: Afficher le type exact
if (groupState is GroupsLoaded) {
print('✓✓✓ GroupsLoaded détecté ! ✓✓✓');
print('Nombre de groupes: ${groupState.groups.length}');
for (var i = 0; i < groupState.groups.length; i++) {
print(' Groupe $i: ${groupState.groups[i].name}');
}
}
return Scaffold( return Scaffold(
body: SafeArea( body: SafeArea(
child: _buildContent(groupState, user.id), child: _buildContent(groupState, user.id),
@@ -141,11 +104,7 @@ class _GroupContentState extends State<GroupContent> {
} }
Widget _buildContent(GroupState groupState, String userId) { Widget _buildContent(GroupState groupState, String userId) {
print('===== _buildContent =====');
print('State type: ${groupState.runtimeType}');
if (groupState is GroupLoading) { if (groupState is GroupLoading) {
print('>>> Affichage: Loading');
return const Center( return const Center(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
@@ -159,24 +118,18 @@ class _GroupContentState extends State<GroupContent> {
} }
if (groupState is GroupError) { if (groupState is GroupError) {
print('>>> Affichage: Error'); return _buildErrorState(groupState.message, userId, true);
return _buildErrorState(groupState.message, userId);
} }
if (groupState is GroupsLoaded) { if (groupState is GroupsLoaded) {
print('>>> Affichage: GroupsLoaded');
print('Groupes: ${groupState.groups.length}');
if (groupState.groups.isEmpty) { if (groupState.groups.isEmpty) {
print('>>> Affichage: Empty');
return _buildEmptyState(); return _buildEmptyState();
} }
print('>>> Affichage: Liste des groupes');
return _buildGroupsList(groupState.groups, userId); return _buildGroupsList(groupState.groups, userId);
} }
print('>>> Affichage: Initial/Unknown');
return Center( return Center(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
@@ -185,7 +138,6 @@ class _GroupContentState extends State<GroupContent> {
const SizedBox(height: 16), const SizedBox(height: 16),
ElevatedButton( ElevatedButton(
onPressed: () { onPressed: () {
print('>>> Bouton refresh cliqué');
context.read<GroupBloc>().add(LoadGroupsByUserId(userId)); context.read<GroupBloc>().add(LoadGroupsByUserId(userId));
}, },
child: const Text('Charger les groupes'), child: const Text('Charger les groupes'),
@@ -196,12 +148,8 @@ class _GroupContentState extends State<GroupContent> {
} }
Widget _buildGroupsList(List<Group> groups, String userId) { Widget _buildGroupsList(List<Group> groups, String userId) {
print('===== _buildGroupsList =====');
print('Nombre de groupes à afficher: ${groups.length}');
return RefreshIndicator( return RefreshIndicator(
onRefresh: () async { onRefresh: () async {
print('>>> Pull to refresh');
context.read<GroupBloc>().add(LoadGroupsByUserId(userId)); context.read<GroupBloc>().add(LoadGroupsByUserId(userId));
await Future.delayed(const Duration(milliseconds: 500)); await Future.delayed(const Duration(milliseconds: 500));
}, },
@@ -221,20 +169,17 @@ class _GroupContentState extends State<GroupContent> {
// IMPORTANT: Utiliser un simple Column au lieu de GridView pour tester // IMPORTANT: Utiliser un simple Column au lieu de GridView pour tester
...groups.map((group) { ...groups.map((group) {
print('Création widget pour: ${group.name}');
return Padding( return Padding(
padding: const EdgeInsets.only(bottom: 12), padding: const EdgeInsets.only(bottom: 12),
child: _buildSimpleGroupCard(group), child: _buildSimpleGroupCard(group),
); );
}).toList(), })
], ],
), ),
); );
} }
Widget _buildSimpleGroupCard(Group group) { Widget _buildSimpleGroupCard(Group group) {
print('===== _buildSimpleGroupCard: ${group.name} =====');
try { try {
final colors = [Colors.blue, Colors.purple, Colors.green, Colors.orange]; final colors = [Colors.blue, Colors.purple, Colors.green, Colors.orange];
final color = colors[group.name.hashCode.abs() % colors.length]; final color = colors[group.name.hashCode.abs() % colors.length];
@@ -249,8 +194,6 @@ class _GroupContentState extends State<GroupContent> {
memberInfo += '\n$names'; memberInfo += '\n$names';
} }
print('Card créée avec succès');
return Card( return Card(
elevation: 2, elevation: 2,
child: ListTile( child: ListTile(
@@ -265,14 +208,11 @@ class _GroupContentState extends State<GroupContent> {
subtitle: Text(memberInfo), subtitle: Text(memberInfo),
trailing: const Icon(Icons.chevron_right), trailing: const Icon(Icons.chevron_right),
onTap: () { onTap: () {
print('Tap sur: ${group.name}');
_openGroupChat(group); _openGroupChat(group);
}, },
), ),
); );
} catch (e, stackTrace) { } catch (e) {
print('ERREUR dans _buildSimpleGroupCard: $e');
print('StackTrace: $stackTrace');
return Card( return Card(
color: Colors.red[100], color: Colors.red[100],
child: const ListTile( child: const ListTile(
@@ -284,7 +224,6 @@ class _GroupContentState extends State<GroupContent> {
} }
Widget _buildEmptyState() { Widget _buildEmptyState() {
print('===== _buildEmptyState =====');
return Center( return Center(
child: Padding( child: Padding(
padding: const EdgeInsets.all(32), padding: const EdgeInsets.all(32),
@@ -306,39 +245,49 @@ class _GroupContentState extends State<GroupContent> {
); );
} }
Widget _buildErrorState(String error, String userId) { Widget _buildErrorState(String error, String userId, bool retry) {
print('===== _buildErrorState ====='); WidgetsBinding.instance.addPostFrameCallback((_) {
return Center( if (mounted) {
child: Padding( if (retry) {
padding: const EdgeInsets.all(32), if (userId == '') {
child: Column( showErrorDialog(
mainAxisAlignment: MainAxisAlignment.center, context,
children: [ title: 'Erreur utilisateur',
const Icon(Icons.error, size: 64, color: Colors.red), message: 'Utilisateur non connecté. Veuillez vous reconnecter.',
const SizedBox(height: 16), icon: Icons.error,
const Text('Erreur', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500)), iconColor: Colors.red,
const SizedBox(height: 8), onRetry: () {
Text(error, textAlign: TextAlign.center, style: const TextStyle(fontSize: 12)), Navigator.of(context).popUntil((route) => route.isFirst);
const SizedBox(height: 16), },
ElevatedButton.icon( );
onPressed: () { } else {
print('>>> Bouton réessayer cliqué'); showErrorDialog(
context,
title: 'Erreur de chargement',
message: error,
icon: Icons.cloud_off,
iconColor: Colors.orange,
onRetry: () {
context.read<GroupBloc>().add(LoadGroupsByUserId(userId)); context.read<GroupBloc>().add(LoadGroupsByUserId(userId));
}, },
icon: const Icon(Icons.refresh), );
label: const Text('Réessayer'), }
), } else {
], showErrorDialog(
), context,
), title: 'Erreur',
); message: error,
icon: Icons.error,
iconColor: Colors.red,
);
}
}
});
return const Center(child: CircularProgressIndicator());
} }
void _openGroupChat(Group group) { void _openGroupChat(Group group) {
print('===== _openGroupChat: ${group.name} =====');
print('Group ID: ${group.id}');
print('Group members: ${group.members.length}');
try { try {
// Afficher juste un message, pas de navigation pour l'instant // Afficher juste un message, pas de navigation pour l'instant
if (mounted) { if (mounted) {
@@ -359,7 +308,7 @@ class _GroupContentState extends State<GroupContent> {
// ); // );
} catch (e) { } catch (e) {
print('ERREUR openGroupChat: $e'); _buildErrorState(e.toString(), '', false);
} }
} }
} }

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:travel_mate/data/models/trip.dart'; import 'package:travel_mate/data/models/trip.dart';
import 'package:travel_mate/services/error_service.dart';
import '../../blocs/user/user_bloc.dart'; import '../../blocs/user/user_bloc.dart';
import '../../blocs/user/user_state.dart' as user_state; import '../../blocs/user/user_state.dart' as user_state;
import '../../blocs/trip/trip_bloc.dart'; import '../../blocs/trip/trip_bloc.dart';
@@ -19,6 +20,7 @@ class CreateTripContent extends StatefulWidget {
} }
class _CreateTripContentState extends State<CreateTripContent> { class _CreateTripContentState extends State<CreateTripContent> {
final _errorService = ErrorService();
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
final _titleController = TextEditingController(); final _titleController = TextEditingController();
final _descriptionController = TextEditingController(); final _descriptionController = TextEditingController();
@@ -528,7 +530,7 @@ class _CreateTripContentState extends State<CreateTripContent> {
} }
} }
} catch (e) { } catch (e) {
print('Erreur lors de la récupération de l\'utilisateur: $e'); _errorService.logError('Erreur lors de la récupération de l\'utilisateur $email: $e', StackTrace.current);
} }
} }

View File

@@ -62,7 +62,6 @@ class Trip {
// Essayer de parser comme ISO 8601 // Essayer de parser comme ISO 8601
return DateTime.parse(dateValue); return DateTime.parse(dateValue);
} catch (e) { } catch (e) {
print('Erreur parsing date string: $dateValue - $e');
return DateTime.now(); return DateTime.now();
} }
} }
@@ -72,12 +71,9 @@ class Trip {
// Traiter comme millisecondes // Traiter comme millisecondes
return DateTime.fromMillisecondsSinceEpoch(dateValue); return DateTime.fromMillisecondsSinceEpoch(dateValue);
} catch (e) { } catch (e) {
print('Erreur parsing date int: $dateValue - $e');
return DateTime.now(); return DateTime.now();
} }
} }
print('Type de date non supporté: ${dateValue.runtimeType} - $dateValue');
return DateTime.now(); return DateTime.now();
} }

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_core/firebase_core.dart';
import 'package:travel_mate/services/error_service.dart';
import 'blocs/auth/auth_bloc.dart'; import 'blocs/auth/auth_bloc.dart';
import 'blocs/auth/auth_event.dart'; import 'blocs/auth/auth_event.dart';
import 'blocs/auth/auth_state.dart'; import 'blocs/auth/auth_state.dart';
@@ -69,6 +70,7 @@ class MyApp extends StatelessWidget {
builder: (context, themeState) { builder: (context, themeState) {
return MaterialApp( return MaterialApp(
title: 'Travel Mate', title: 'Travel Mate',
navigatorKey: ErrorService.navigatorKey,
themeMode: themeState.themeMode, themeMode: themeState.themeMode,
theme: ThemeData( theme: ThemeData(
colorScheme: ColorScheme.fromSeed( colorScheme: ColorScheme.fromSeed(

View File

@@ -1,9 +1,11 @@
import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:travel_mate/services/error_service.dart';
import '../data/models/group.dart'; import '../data/models/group.dart';
import '../data/models/group_member.dart'; import '../data/models/group_member.dart';
class GroupRepository { class GroupRepository {
final FirebaseFirestore _firestore = FirebaseFirestore.instance; final FirebaseFirestore _firestore = FirebaseFirestore.instance;
final _errorService = ErrorService();
CollectionReference get _groupsCollection => _firestore.collection('groups'); CollectionReference get _groupsCollection => _firestore.collection('groups');
@@ -35,21 +37,15 @@ class GroupRepository {
} }
Stream<List<Group>> getGroupsByUserId(String userId) { Stream<List<Group>> getGroupsByUserId(String userId) {
print('===== GroupRepository: getGroupsByUserId START =====');
print('UserId recherché: $userId');
return _groupsCollection return _groupsCollection
.snapshots() .snapshots()
.asyncMap((snapshot) async { .asyncMap((snapshot) async {
print('===== GroupRepository: Nouveau snapshot (${DateTime.now()}) =====');
print('Nombre de documents: ${snapshot.docs.length}');
List<Group> userGroups = []; List<Group> userGroups = [];
for (var groupDoc in snapshot.docs) { for (var groupDoc in snapshot.docs) {
try { try {
final groupId = groupDoc.id; final groupId = groupDoc.id;
print('--- Vérification groupe: $groupId ---');
// Vérifier si l'utilisateur est membre // Vérifier si l'utilisateur est membre
final memberDoc = await groupDoc.reference final memberDoc = await groupDoc.reference
@@ -57,34 +53,24 @@ class GroupRepository {
.doc(userId) .doc(userId)
.get(); .get();
print('Membre existe dans $groupId: ${memberDoc.exists}');
if (memberDoc.exists) { if (memberDoc.exists) {
print('✓ Utilisateur trouvé dans $groupId');
final groupData = groupDoc.data() as Map<String, dynamic>; final groupData = groupDoc.data() as Map<String, dynamic>;
final group = Group.fromMap(groupData, groupId); final group = Group.fromMap(groupData, groupId);
final members = await getGroupMembers(groupId); final members = await getGroupMembers(groupId);
print('${members.length} membres chargés pour $groupId');
userGroups.add(group.copyWith(members: members)); userGroups.add(group.copyWith(members: members));
} else { } else {
print('Utilisateur NON membre de $groupId'); _errorService.logInfo('group_repository.dart','Utilisateur NON membre de $groupId');
} }
} catch (e, stackTrace) { } catch (e, stackTrace) {
print('ERREUR groupe ${groupDoc.id}: $e'); _errorService.logError(e.toString(), stackTrace);
print('StackTrace: $stackTrace');
} }
} }
print('===== Retour: ${userGroups.length} groupes =====');
return userGroups; return userGroups;
}) })
.distinct((prev, next) { .distinct((prev, next) {
// Comparer les listes pour éviter les doublons // Comparer les listes pour éviter les doublons
if (prev.length != next.length) { if (prev.length != next.length) {
print('>>> Changement détecté: ${prev.length} -> ${next.length} groupes');
return false; return false;
} }
@@ -95,17 +81,10 @@ class GroupRepository {
final identical = prevIds.difference(nextIds).isEmpty && final identical = prevIds.difference(nextIds).isEmpty &&
nextIds.difference(prevIds).isEmpty; nextIds.difference(prevIds).isEmpty;
if (!identical) {
print('>>> Changement détecté: IDs différents');
} else {
print('>>> Données identiques, émission ignorée');
}
return identical; return identical;
}) })
.handleError((error, stackTrace) { .handleError((error, stackTrace) {
print('ERREUR stream: $error'); _errorService.logError(error, stackTrace);
print('StackTrace: $stackTrace');
return <Group>[]; return <Group>[];
}); });
} }
@@ -146,10 +125,7 @@ class GroupRepository {
Future<List<GroupMember>> getGroupMembers(String groupId) async { Future<List<GroupMember>> getGroupMembers(String groupId) async {
try { try {
print('Chargement membres pour: $groupId');
final snapshot = await _membersCollection(groupId).get(); final snapshot = await _membersCollection(groupId).get();
print('${snapshot.docs.length} membres trouvés');
return snapshot.docs return snapshot.docs
.map((doc) { .map((doc) {
return GroupMember.fromMap( return GroupMember.fromMap(
@@ -159,7 +135,6 @@ class GroupRepository {
}) })
.toList(); .toList();
} catch (e) { } catch (e) {
print('ERREUR getGroupMembers: $e');
throw Exception('Erreur lors de la récupération des membres: $e'); throw Exception('Erreur lors de la récupération des membres: $e');
} }
} }

View File

@@ -1,7 +1,9 @@
import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_auth/firebase_auth.dart';
import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart';
import 'package:travel_mate/services/error_service.dart';
class AuthService { class AuthService {
final _errorService = ErrorService();
final FirebaseAuth firebaseAuth = FirebaseAuth.instance; final FirebaseAuth firebaseAuth = FirebaseAuth.instance;
User? get currentUser => firebaseAuth.currentUser; User? get currentUser => firebaseAuth.currentUser;
@@ -89,13 +91,13 @@ class AuthService {
} }
} on GoogleSignInException catch (e) { } on GoogleSignInException catch (e) {
print('Erreur lors de l\'initialisation de Google Sign-In: $e'); _errorService.logError('Erreur Google Sign-In: $e', StackTrace.current);
rethrow; rethrow;
} on FirebaseAuthException catch (e) { } on FirebaseAuthException catch (e) {
print('Erreur Firebase lors de l\'initialisation de Google Sign-In: $e'); _errorService.logError('Erreur Firebase lors de l\'initialisation de Google Sign-In: $e', StackTrace.current);
rethrow; rethrow;
} catch (e) { } catch (e) {
print('Erreur inconnue lors de l\'initialisation de Google Sign-In: $e'); _errorService.logError('Erreur inconnue lors de l\'initialisation de Google Sign-In: $e', StackTrace.current);
rethrow; rethrow;
} }
} }

View File

@@ -0,0 +1,82 @@
import 'package:flutter/material.dart';
import '../components/error/error_content.dart';
class ErrorService {
static final ErrorService _instance = ErrorService._internal();
factory ErrorService() => _instance;
ErrorService._internal();
// GlobalKey pour accéder au context depuis n'importe où
static GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
// Afficher une erreur en dialog
void showError({
required String message,
String title = 'Erreur',
VoidCallback? onRetry,
IconData icon = Icons.error_outline,
Color? iconColor,
}) {
final context = navigatorKey.currentContext;
if (context != null) {
showErrorDialog(
context,
title: title,
message: message,
icon: icon,
iconColor: iconColor,
onRetry: onRetry,
);
}
}
// Afficher une erreur en snackbar
void showSnackbar({
required String message,
VoidCallback? onRetry,
bool isError = true,
}) {
final context = navigatorKey.currentContext;
if (context != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: isError ? Colors.red[400] : Colors.green[600],
duration: const Duration(seconds: 4),
action: onRetry != null
? SnackBarAction(
label: 'Réessayer',
textColor: Colors.white,
onPressed: onRetry,
)
: null,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
);
}
}
// Logger dans la console (développement)
void logError(String source, dynamic error, [StackTrace? stackTrace]) {
print('═══════════════════════════════════');
print('❌ ERREUR dans $source');
print('Message: $error');
if (stackTrace != null) {
print('StackTrace: $stackTrace');
}
print('═══════════════════════════════════');
}
// Logger une info (développement)
void logInfo(String source, String message) {
print(' [$source] $message');
}
// Logger un succès
void logSuccess(String source, String message) {
print('✅ [$source] $message');
}
}

View File

@@ -1,7 +1,9 @@
import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:travel_mate/data/models/group.dart'; import 'package:travel_mate/data/models/group.dart';
import 'package:travel_mate/services/error_service.dart';
class GroupService { class GroupService {
final _errorService = ErrorService();
final FirebaseFirestore _firestore = FirebaseFirestore.instance; final FirebaseFirestore _firestore = FirebaseFirestore.instance;
Stream<List<Group>> getGroupsStream() { Stream<List<Group>> getGroupsStream() {
@@ -17,7 +19,7 @@ class GroupService {
await _firestore.collection('groups').add(group.toMap()); await _firestore.collection('groups').add(group.toMap());
return true; return true;
} catch (e) { } catch (e) {
print('Erreur lors de la création du groupe: $e'); _errorService.logError('Erreur lors de la création du groupe: $e', StackTrace.current);
return false; return false;
} }
} }
@@ -27,7 +29,7 @@ class GroupService {
await _firestore.collection('groups').doc(group.id).update(group.toMap()); await _firestore.collection('groups').doc(group.id).update(group.toMap());
return true; return true;
} catch (e) { } catch (e) {
print('Erreur lors de la mise à jour du groupe: $e'); _errorService.logError('Erreur lors de la mise à jour du groupe: $e', StackTrace.current);
return false; return false;
} }
} }
@@ -37,7 +39,7 @@ class GroupService {
await _firestore.collection('groups').doc(groupId).delete(); await _firestore.collection('groups').doc(groupId).delete();
return true; return true;
} catch (e) { } catch (e) {
print('Erreur lors de la suppression du groupe: $e'); _errorService.logError('Erreur lors de la suppression du groupe: $e', StackTrace.current);
return false; return false;
} }
} }
@@ -49,10 +51,9 @@ class GroupService {
.where('members', arrayContains: userId) .where('members', arrayContains: userId)
.snapshots() .snapshots()
.map((snapshot) { .map((snapshot) {
print('Groupes trouvés pour l\'utilisateur $userId: ${snapshot.docs.length}');
return snapshot.docs.map((doc) { return snapshot.docs.map((doc) {
final group = Group.fromMap(doc.data(), doc.id); final group = Group.fromMap(doc.data(), doc.id);
print('Groupe: ${group.name}, Membres: ${group.members.length}'); _errorService.logError('Groupe: ${group.name}, Membres: ${group.members.length}', StackTrace.current);
return group; return group;
}).toList(); }).toList();
}); });
@@ -65,6 +66,4 @@ class GroupService {
Future<void> addMemberToGroup(String groupId, String memberId) async { Future<void> addMemberToGroup(String groupId, String memberId) async {
// TODO: Implémenter l'ajout d'un membre à un groupe // TODO: Implémenter l'ajout d'un membre à un groupe
} }
} }

View File

@@ -1,7 +1,9 @@
import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:travel_mate/services/error_service.dart';
import '../data/models/trip.dart'; import '../data/models/trip.dart';
class TripService { class TripService {
final _errorService = ErrorService();
final FirebaseFirestore _firestore = FirebaseFirestore.instance; final FirebaseFirestore _firestore = FirebaseFirestore.instance;
static const String _tripsCollection = 'trips'; static const String _tripsCollection = 'trips';
@@ -18,7 +20,7 @@ class TripService {
return Trip.fromMap({...data, 'id': doc.id}); return Trip.fromMap({...data, 'id': doc.id});
}).toList(); }).toList();
} catch (e) { } catch (e) {
print('Erreur lors du chargement des voyages: $e'); _errorService.logError('Erreur lors du chargement des voyages: $e', StackTrace.current);
return []; return [];
} }
} }
@@ -39,16 +41,13 @@ class TripService {
await _firestore.collection(_tripsCollection).add(tripData); await _firestore.collection(_tripsCollection).add(tripData);
return true; return true;
} catch (e) { } catch (e) {
print('Erreur lors de l\'ajout du voyage: $e'); _errorService.logError('Erreur lors de l\'ajout du voyage: $e', StackTrace.current);
return false; return false;
} }
} }
// Stream pour écouter les voyages d'un utilisateur en temps réel // Stream pour écouter les voyages d'un utilisateur en temps réel
Stream<List<Trip>> getTripsStreamByUser(String userId, String userEmail) { Stream<List<Trip>> getTripsStreamByUser(String userId, String userEmail) {
print('=== STREAM CRÉÉ ===');
print('UserId: $userId');
return _firestore return _firestore
.collection(_tripsCollection) .collection(_tripsCollection)
.snapshots() .snapshots()
@@ -88,8 +87,7 @@ class TripService {
} }
} }
} catch (e, stackTrace) { } catch (e, stackTrace) {
print('Erreur lors du traitement du document ${doc.id}: $e'); _errorService.logError('Erreur lors du traitement du document ${doc.id}: $e', stackTrace);
print('StackTrace: $stackTrace');
} }
} }
@@ -98,15 +96,14 @@ class TripService {
try { try {
return b.createdAt.compareTo(a.createdAt); return b.createdAt.compareTo(a.createdAt);
} catch (e) { } catch (e) {
print('Erreur lors du tri: $e'); _errorService.logError('Erreur lors du tri: $e', StackTrace.current);
return 0; return 0;
} }
}); });
return trips; return trips;
}).handleError((error, stackTrace) { }).handleError((error, stackTrace) {
print('Erreur dans le stream: $error'); _errorService.logError('Erreur dans le stream: $error', stackTrace);
print('StackTrace: $stackTrace');
return <Trip>[]; return <Trip>[];
}); });
} }
@@ -130,7 +127,7 @@ class TripService {
trips.add(trip); trips.add(trip);
} }
} catch (e) { } catch (e) {
print('Erreur lors de la conversion du voyage créé ${doc.id}: $e'); _errorService.logError('Erreur lors de la conversion du voyage créé ${doc.id}: $e', StackTrace.current);
} }
} }
@@ -144,7 +141,7 @@ class TripService {
}); });
return trips; return trips;
} catch (e) { } catch (e) {
print('Erreur lors de la récupération des voyages: $e'); _errorService.logError('Erreur lors de la récupération des voyages: $e', StackTrace.current);
return []; return [];
} }
} }
@@ -183,8 +180,7 @@ class TripService {
return trip; return trip;
} catch (e, stackTrace) { } catch (e, stackTrace) {
print('Erreur lors de la conversion du document $docId: $e'); _errorService.logError('Erreur lors de la conversion du document $docId: $e', stackTrace);
print('StackTrace: $stackTrace');
return null; return null;
} }
} }
@@ -202,7 +198,7 @@ class TripService {
.update(tripData); .update(tripData);
return true; return true;
} catch (e) { } catch (e) {
print('Erreur lors de la mise à jour du voyage: $e'); _errorService.logError('Erreur lors de la mise à jour du voyage: $e', StackTrace.current);
return false; return false;
} }
} }
@@ -213,7 +209,7 @@ class TripService {
await _firestore.collection(_tripsCollection).doc(tripId).delete(); await _firestore.collection(_tripsCollection).doc(tripId).delete();
return true; return true;
} catch (e) { } catch (e) {
print('Erreur lors de la suppression du voyage: $e'); _errorService.logError('Erreur lors de la suppression du voyage: $e', StackTrace.current);
return false; return false;
} }
} }
@@ -232,7 +228,7 @@ class TripService {
} }
return null; return null;
} catch (e) { } catch (e) {
print('Erreur lors de la récupération du voyage: $e'); _errorService.logError('Erreur lors de la récupération du voyage: $e', StackTrace.current);
return null; return null;
} }
} }
@@ -246,7 +242,7 @@ class TripService {
}); });
return true; return true;
} catch (e) { } catch (e) {
print('Erreur lors de l\'ajout du participant: $e'); _errorService.logError('Erreur lors de l\'ajout du participant: $e', StackTrace.current);
return false; return false;
} }
} }
@@ -260,7 +256,7 @@ class TripService {
}); });
return true; return true;
} catch (e) { } catch (e) {
print('Erreur lors du retrait du participant: $e'); _errorService.logError('Erreur lors du retrait du participant: $e', StackTrace.current);
return false; return false;
} }
} }

View File

@@ -1,8 +1,10 @@
import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_auth/firebase_auth.dart';
import 'package:travel_mate/services/error_service.dart';
import '../blocs/user/user_state.dart'; import '../blocs/user/user_state.dart';
class UserService { class UserService {
final _errorService = ErrorService();
final FirebaseFirestore _firestore; final FirebaseFirestore _firestore;
final FirebaseAuth _auth; final FirebaseAuth _auth;
static const String _usersCollection = 'users'; static const String _usersCollection = 'users';
@@ -32,7 +34,7 @@ class UserService {
.set(user.toJson()); .set(user.toJson());
return true; return true;
} catch (e) { } catch (e) {
print('Erreur lors de la création de l\'utilisateur: $e'); _errorService.logError('Erreur lors de la création de l\'utilisateur: $e', StackTrace.current);
return false; return false;
} }
} }
@@ -53,7 +55,7 @@ class UserService {
} }
return null; return null;
} catch (e) { } catch (e) {
print('Erreur lors de la récupération de l\'utilisateur: $e'); _errorService.logError('Erreur lors de la récupération de l\'utilisateur: $e', StackTrace.current);
return null; return null;
} }
} }
@@ -76,7 +78,7 @@ class UserService {
} }
return null; return null;
} catch (e) { } catch (e) {
print('Erreur lors de la récupération de l\'utilisateur par email: $e'); _errorService.logError('Erreur lors de la récupération de l\'utilisateur par email: $e', StackTrace.current);
return null; return null;
} }
} }
@@ -95,7 +97,7 @@ class UserService {
} }
return null; return null;
} catch (e) { } catch (e) {
print('Erreur lors de la récupération de l\'ID utilisateur: $e'); _errorService.logError('Erreur lors de la récupération de l\'ID utilisateur: $e', StackTrace.current);
return null; return null;
} }
} }
@@ -109,7 +111,7 @@ class UserService {
.update(userData); .update(userData);
return true; return true;
} catch (e) { } catch (e) {
print('Erreur lors de la mise à jour de l\'utilisateur: $e'); _errorService.logError('Erreur lors de la mise à jour de l\'utilisateur: $e', StackTrace.current);
return false; return false;
} }
} }
@@ -123,7 +125,7 @@ class UserService {
.delete(); .delete();
return true; return true;
} catch (e) { } catch (e) {
print('Erreur lors de la suppression de l\'utilisateur: $e'); _errorService.logError('Erreur lors de la suppression de l\'utilisateur: $e', StackTrace.current);
return false; return false;
} }
} }
@@ -139,7 +141,7 @@ class UserService {
return querySnapshot.docs.isNotEmpty; return querySnapshot.docs.isNotEmpty;
} catch (e) { } catch (e) {
print('Erreur lors de la vérification de l\'email: $e'); _errorService.logError('Erreur lors de la vérification de l\'email: $e', StackTrace.current);
return false; return false;
} }
} }
@@ -171,7 +173,7 @@ class UserService {
return users; return users;
} catch (e) { } catch (e) {
print('Erreur lors de la récupération des utilisateurs: $e'); _errorService.logError('Erreur lors de la récupération des utilisateurs: $e', StackTrace.current);
return []; return [];
} }
} }
@@ -190,7 +192,7 @@ class UserService {
}); });
}).toList(); }).toList();
} catch (e) { } catch (e) {
print('Erreur lors de la récupération de tous les utilisateurs: $e'); _errorService.logError('Erreur lors de la récupération de tous les utilisateurs: $e', StackTrace.current);
return []; return [];
} }
} }
@@ -254,7 +256,7 @@ class UserService {
return usersMap.values.toList(); return usersMap.values.toList();
} catch (e) { } catch (e) {
print('Erreur lors de la recherche d\'utilisateurs: $e'); _errorService.logError('Erreur lors de la recherche d\'utilisateurs: $e', StackTrace.current);
return []; return [];
} }
} }
@@ -270,7 +272,7 @@ class UserService {
}); });
return true; return true;
} catch (e) { } catch (e) {
print('Erreur lors de la mise à jour de la dernière connexion: $e'); _errorService.logError('Erreur lors de la mise à jour de la dernière connexion: $e', StackTrace.current);
return false; return false;
} }
} }
@@ -284,7 +286,7 @@ class UserService {
.get(); .get();
return doc.exists; return doc.exists;
} catch (e) { } catch (e) {
print('Erreur lors de la vérification de l\'existence de l\'utilisateur: $e'); _errorService.logError('Erreur lors de la vérification de l\'existence de l\'utilisateur: $e', StackTrace.current);
return false; return false;
} }
} }