feat: Add logger service and improve expense dialog with enhanced receipt management and calculation logic.

This commit is contained in:
Van Leemput Dayron
2025-11-28 12:54:54 +01:00
parent cad9d42128
commit fd710b8cb8
35 changed files with 2148 additions and 1296 deletions

View File

@@ -9,6 +9,7 @@ import '../../../blocs/user/user_bloc.dart';
import '../../../blocs/user/user_state.dart' as user_state;
import '../../../blocs/user/user_event.dart' as user_event;
import '../../../services/auth_service.dart';
import '../../../services/logger_service.dart';
class ProfileContent extends StatelessWidget {
ProfileContent({super.key});
@@ -19,7 +20,8 @@ class ProfileContent extends StatelessWidget {
Widget build(BuildContext context) {
return UserStateWrapper(
builder: (context, user) {
final isEmailAuth = user.authMethod == 'email' || user.authMethod == null;
final isEmailAuth =
user.authMethod == 'email' || user.authMethod == null;
return SingleChildScrollView(
child: Column(
@@ -40,10 +42,12 @@ class ProfileContent extends StatelessWidget {
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 8,
offset: Offset(0, 2),
)
),
],
),
child: user.profilePictureUrl != null && user.profilePictureUrl!.isNotEmpty
child:
user.profilePictureUrl != null &&
user.profilePictureUrl!.isNotEmpty
? CircleAvatar(
radius: 50,
backgroundImage: NetworkImage(
@@ -57,7 +61,9 @@ class ProfileContent extends StatelessWidget {
)
: CircleAvatar(
radius: 50,
backgroundColor: Theme.of(context).colorScheme.primary,
backgroundColor: Theme.of(
context,
).colorScheme.primary,
child: Text(
user.prenom.isNotEmpty
? user.prenom[0].toUpperCase()
@@ -88,10 +94,7 @@ class ProfileContent extends StatelessWidget {
// Email
Text(
user.email,
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
textAlign: TextAlign.center,
),
@@ -99,7 +102,10 @@ class ProfileContent extends StatelessWidget {
// Badge de méthode de connexion
Container(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6),
padding: EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
color: _getAuthMethodColor(user.authMethod, context),
borderRadius: BorderRadius.circular(12),
@@ -120,7 +126,10 @@ class ProfileContent extends StatelessWidget {
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: _getAuthMethodTextColor(user.authMethod, context),
color: _getAuthMethodTextColor(
user.authMethod,
context,
),
),
),
],
@@ -314,17 +323,13 @@ class ProfileContent extends StatelessWidget {
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: isDestructive
? Colors.red
color: isDestructive
? Colors.red
: (isDarkMode ? Colors.white : Colors.black87),
),
),
),
Icon(
Icons.arrow_forward_ios,
size: 16,
color: Colors.grey[400],
),
Icon(Icons.arrow_forward_ios, size: 16, color: Colors.grey[400]),
],
),
),
@@ -355,7 +360,7 @@ class ProfileContent extends StatelessWidget {
Color _getAuthMethodColor(String? authMethod, BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
switch (authMethod) {
case 'apple':
return isDarkMode ? Colors.white : Colors.black87;
@@ -368,7 +373,7 @@ class ProfileContent extends StatelessWidget {
Color _getAuthMethodTextColor(String? authMethod, BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
switch (authMethod) {
case 'apple':
return isDarkMode ? Colors.black87 : Colors.white;
@@ -401,7 +406,9 @@ class ProfileContent extends StatelessWidget {
children: [
CircleAvatar(
radius: 50,
backgroundColor: Theme.of(context).colorScheme.primary,
backgroundColor: Theme.of(
context,
).colorScheme.primary,
child: Text(
prenomController.text.isNotEmpty
? prenomController.text[0].toUpperCase()
@@ -422,7 +429,11 @@ class ProfileContent extends StatelessWidget {
color: Theme.of(context).colorScheme.primary,
),
child: IconButton(
icon: Icon(Icons.camera_alt, color: Colors.white, size: 20),
icon: Icon(
Icons.camera_alt,
color: Colors.white,
size: 20,
),
onPressed: () {
_showPhotoPickerDialog(dialogContext);
},
@@ -511,60 +522,66 @@ class ProfileContent extends StatelessWidget {
void _showPhotoPickerDialog(BuildContext context) {
// Récupérer les références AVANT que le modal ne se ferme
final userBloc = context.read<UserBloc>();
showModalBottomSheet(
context: context,
builder: (BuildContext sheetContext) {
return Container(
child: Wrap(
children: [
ListTile(
leading: Icon(Icons.photo_library),
title: Text('Galerie'),
onTap: () {
Navigator.pop(sheetContext);
_pickImageFromGallery(context, userBloc);
},
),
ListTile(
leading: Icon(Icons.camera_alt),
title: Text('Caméra'),
onTap: () {
Navigator.pop(sheetContext);
_pickImageFromCamera(context, userBloc);
},
),
ListTile(
leading: Icon(Icons.close),
title: Text('Annuler'),
onTap: () => Navigator.pop(sheetContext),
),
],
),
return Wrap(
children: [
ListTile(
leading: Icon(Icons.photo_library),
title: Text('Galerie'),
onTap: () {
Navigator.pop(sheetContext);
_pickImageFromGallery(context, userBloc);
},
),
ListTile(
leading: Icon(Icons.camera_alt),
title: Text('Caméra'),
onTap: () {
Navigator.pop(sheetContext);
_pickImageFromCamera(context, userBloc);
},
),
ListTile(
leading: Icon(Icons.close),
title: Text('Annuler'),
onTap: () => Navigator.pop(sheetContext),
),
],
);
},
);
}
Future<void> _pickImageFromGallery(BuildContext context, UserBloc userBloc) async {
Future<void> _pickImageFromGallery(
BuildContext context,
UserBloc userBloc,
) async {
try {
final ImagePicker picker = ImagePicker();
final XFile? image = await picker.pickImage(source: ImageSource.gallery);
if (image != null) {
if (image != null && context.mounted) {
await _uploadProfilePicture(context, image.path, userBloc);
}
} catch (e) {
_errorService.showError(message: 'Erreur lors de la sélection de l\'image');
_errorService.showError(
message: 'Erreur lors de la sélection de l\'image',
);
}
}
Future<void> _pickImageFromCamera(BuildContext context, UserBloc userBloc) async {
Future<void> _pickImageFromCamera(
BuildContext context,
UserBloc userBloc,
) async {
try {
final ImagePicker picker = ImagePicker();
final XFile? image = await picker.pickImage(source: ImageSource.camera);
if (image != null) {
if (image != null && context.mounted) {
await _uploadProfilePicture(context, image.path, userBloc);
}
} catch (e) {
@@ -572,17 +589,23 @@ class ProfileContent extends StatelessWidget {
}
}
Future<void> _uploadProfilePicture(BuildContext context, String imagePath, UserBloc userBloc) async {
Future<void> _uploadProfilePicture(
BuildContext context,
String imagePath,
UserBloc userBloc,
) async {
try {
final File imageFile = File(imagePath);
// Vérifier que le fichier existe
if (!await imageFile.exists()) {
_errorService.showError(message: 'Le fichier image n\'existe pas');
return;
}
print('DEBUG: Taille du fichier: ${imageFile.lengthSync()} bytes');
LoggerService.info(
'DEBUG: Taille du fichier: ${imageFile.lengthSync()} bytes',
);
final userState = userBloc.state;
if (userState is! user_state.UserLoaded) {
@@ -591,30 +614,35 @@ class ProfileContent extends StatelessWidget {
}
final user = userState.user;
// Créer un nom unique pour la photo
final String fileName = 'profile_${user.id}_${DateTime.now().millisecondsSinceEpoch}.jpg';
final String fileName =
'profile_${user.id}_${DateTime.now().millisecondsSinceEpoch}.jpg';
final Reference storageRef = FirebaseStorage.instance
.ref()
.child('profile_pictures')
.child(fileName);
print('DEBUG: Chemin Storage: ${storageRef.fullPath}');
print('DEBUG: Upload en cours pour $fileName');
LoggerService.info('DEBUG: Chemin Storage: ${storageRef.fullPath}');
LoggerService.info('DEBUG: Upload en cours pour $fileName');
// Uploader l'image avec gestion d'erreur détaillée
try {
final uploadTask = storageRef.putFile(imageFile);
// Écouter la progression
uploadTask.snapshotEvents.listen((TaskSnapshot snapshot) {
print('DEBUG: Progression: ${snapshot.bytesTransferred}/${snapshot.totalBytes}');
LoggerService.info(
'DEBUG: Progression: ${snapshot.bytesTransferred}/${snapshot.totalBytes}',
);
});
final snapshot = await uploadTask;
print('DEBUG: Upload terminé. État: ${snapshot.state}');
LoggerService.info('DEBUG: Upload terminé. État: ${snapshot.state}');
} on FirebaseException catch (e) {
print('DEBUG: FirebaseException lors de l\'upload: ${e.code} - ${e.message}');
LoggerService.error(
'DEBUG: FirebaseException lors de l\'upload: ${e.code} - ${e.message}',
);
if (context.mounted) {
_errorService.showError(
message: 'Erreur Firebase: ${e.code}\n${e.message}',
@@ -623,26 +651,22 @@ class ProfileContent extends StatelessWidget {
return;
}
print('DEBUG: Upload terminé, récupération de l\'URL');
LoggerService.info('DEBUG: Upload terminé, récupération de l\'URL');
// Récupérer l'URL
final String downloadUrl = await storageRef.getDownloadURL();
print('DEBUG: URL obtenue: $downloadUrl');
LoggerService.info('DEBUG: URL obtenue: $downloadUrl');
// Mettre à jour le profil avec l'URL en utilisant la référence sauvegardée du BLoC
print('DEBUG: Envoi de UserUpdated event au BLoC');
userBloc.add(
user_event.UserUpdated({
'profilePictureUrl': downloadUrl,
}),
);
LoggerService.info('DEBUG: Envoi de UserUpdated event au BLoC');
userBloc.add(user_event.UserUpdated({'profilePictureUrl': downloadUrl}));
// Attendre un peu que Firestore se mette à jour
await Future.delayed(Duration(milliseconds: 500));
if (context.mounted) {
print('DEBUG: Affichage du succès');
LoggerService.info('DEBUG: Affichage du succès');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Photo de profil mise à jour !'),
@@ -651,8 +675,8 @@ class ProfileContent extends StatelessWidget {
);
}
} catch (e, stackTrace) {
print('DEBUG: Erreur lors de l\'upload: $e');
print('DEBUG: Stack trace: $stackTrace');
LoggerService.error('DEBUG: Erreur lors de l\'upload: $e');
LoggerService.error('DEBUG: Stack trace: $stackTrace');
_errorService.logError(
'ProfileContent - _uploadProfilePicture',
'Erreur lors de l\'upload de la photo: $e\n$stackTrace',
@@ -738,13 +762,15 @@ class ProfileContent extends StatelessWidget {
email: user.email,
);
Navigator.of(dialogContext).pop();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Mot de passe changé !'),
backgroundColor: Colors.green,
),
);
if (context.mounted) {
Navigator.of(dialogContext).pop();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Mot de passe changé !'),
backgroundColor: Colors.green,
),
);
}
} catch (e) {
_errorService.showError(
message: 'Erreur: Mot de passe actuel incorrect',
@@ -801,13 +827,15 @@ class ProfileContent extends StatelessWidget {
email: user.email,
);
Navigator.of(dialogContext).pop();
context.read<UserBloc>().add(user_event.UserLoggedOut());
Navigator.pushNamedAndRemoveUntil(
context,
'/login',
(route) => false,
);
if (context.mounted) {
Navigator.of(dialogContext).pop();
context.read<UserBloc>().add(user_event.UserLoggedOut());
Navigator.pushNamedAndRemoveUntil(
context,
'/login',
(route) => false,
);
}
} catch (e) {
_errorService.showError(
message: 'Erreur: Mot de passe incorrect',