Files
TravelMate/lib/components/settings/profile/profile_content.dart

854 lines
29 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:travel_mate/components/widgets/user_state_widget.dart';
import 'package:travel_mate/services/error_service.dart';
import 'package:image_picker/image_picker.dart';
import 'package:firebase_storage/firebase_storage.dart';
import 'dart:io';
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});
final _errorService = ErrorService();
@override
Widget build(BuildContext context) {
return UserStateWrapper(
builder: (context, user) {
final isEmailAuth =
user.authMethod == 'email' || user.authMethod == null;
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// En-tête avec photo de profil
Container(
padding: EdgeInsets.symmetric(vertical: 24, horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// Photo de profil
Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 8,
offset: Offset(0, 2),
),
],
),
child:
user.profilePictureUrl != null &&
user.profilePictureUrl!.isNotEmpty
? CircleAvatar(
radius: 50,
backgroundImage: NetworkImage(
user.profilePictureUrl!,
// Force le rechargement avec un paramètre de cache
headers: {
'pragma': 'no-cache',
'cache-control': 'no-cache',
},
),
)
: CircleAvatar(
radius: 50,
backgroundColor: Theme.of(
context,
).colorScheme.primary,
child: Text(
user.prenom.isNotEmpty
? user.prenom[0].toUpperCase()
: 'U',
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
),
SizedBox(height: 16),
// Nom complet
Text(
'${user.prenom} ${user.nom ?? ''}',
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
SizedBox(height: 4),
// Email
Text(
user.email,
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
textAlign: TextAlign.center,
),
SizedBox(height: 12),
// Badge de méthode de connexion
Container(
padding: EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
color: _getAuthMethodColor(user.authMethod, context),
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (_getAuthMethodIcon(user.authMethod).isNotEmpty)
Image.asset(
_getAuthMethodIcon(user.authMethod),
height: 16,
width: 16,
),
if (_getAuthMethodIcon(user.authMethod).isNotEmpty)
SizedBox(width: 8),
Text(
_getAuthMethodLabel(user.authMethod),
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: _getAuthMethodTextColor(
user.authMethod,
context,
),
),
),
],
),
),
],
),
),
// Section Informations personnelles
Padding(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row(
children: [
Text(
'Informations personnelles',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
],
),
),
// Tuiles d'information
_buildInfoTile(
icon: Icons.person_outline,
label: 'Nom complet',
value: '${user.prenom} ${user.nom ?? ''}',
context: context,
),
_buildInfoTile(
icon: Icons.email_outlined,
label: 'Adresse e-mail',
value: user.email,
context: context,
),
_buildInfoTile(
icon: Icons.phone_outlined,
label: 'Téléphone',
value: user.phoneNumber ?? 'Non défini',
context: context,
),
// Option de changement de mot de passe seulement pour email
if (isEmailAuth)
_buildActionTile(
icon: Icons.lock_outlined,
label: 'Changer de mot de passe',
context: context,
onTap: () {
_showChangePasswordDialog(context, user);
},
),
SizedBox(height: 8),
// Option pour modifier le profil
_buildActionTile(
icon: Icons.edit_outlined,
label: 'Modifier le profil',
context: context,
onTap: () {
_showEditProfileDialog(context, user);
},
),
SizedBox(height: 8),
// Option pour supprimer le compte
_buildActionTile(
icon: Icons.delete_outline,
label: 'Supprimer le compte',
context: context,
isDestructive: true,
onTap: () {
_showDeleteAccountDialog(context, user);
},
),
SizedBox(height: 24),
],
),
);
},
);
}
Widget _buildInfoTile({
required IconData icon,
required String label,
required String value,
required BuildContext context,
}) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return Container(
margin: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[850] : Colors.grey[100],
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Container(
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[800] : Colors.grey[200],
borderRadius: BorderRadius.circular(6),
),
child: Icon(
icon,
size: 20,
color: Theme.of(context).colorScheme.primary,
),
),
SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 2),
Text(
value,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: isDarkMode ? Colors.white : Colors.black87,
),
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
);
}
Widget _buildActionTile({
required IconData icon,
required String label,
required BuildContext context,
required VoidCallback onTap,
bool isDestructive = false,
}) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
return GestureDetector(
onTap: onTap,
child: Container(
margin: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[850] : Colors.grey[100],
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Container(
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: isDarkMode ? Colors.grey[800] : Colors.grey[200],
borderRadius: BorderRadius.circular(6),
),
child: Icon(
icon,
size: 20,
color: isDestructive
? Colors.red
: Theme.of(context).colorScheme.primary,
),
),
SizedBox(width: 16),
Expanded(
child: Text(
label,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: isDestructive
? Colors.red
: (isDarkMode ? Colors.white : Colors.black87),
),
),
),
Icon(Icons.arrow_forward_ios, size: 16, color: Colors.grey[400]),
],
),
),
);
}
String _getAuthMethodLabel(String? authMethod) {
switch (authMethod) {
case 'apple':
return 'Connecté avec Apple';
case 'google':
return 'Connecté avec Google';
default:
return 'Connecté avec Email';
}
}
String _getAuthMethodIcon(String? authMethod) {
switch (authMethod) {
case 'apple':
return 'assets/icons/apple_white.png';
case 'google':
return 'assets/icons/google.png';
default:
return '';
}
}
Color _getAuthMethodColor(String? authMethod, BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
switch (authMethod) {
case 'apple':
return isDarkMode ? Colors.white : Colors.black87;
case 'google':
return isDarkMode ? Colors.white : Colors.black87;
default:
return isDarkMode ? Colors.white : Colors.blue;
}
}
Color _getAuthMethodTextColor(String? authMethod, BuildContext context) {
final isDarkMode = Theme.of(context).brightness == Brightness.dark;
switch (authMethod) {
case 'apple':
return isDarkMode ? Colors.black87 : Colors.white;
case 'google':
return isDarkMode ? Colors.black87 : Colors.white;
default:
return isDarkMode ? Colors.black87 : Colors.white;
}
}
void _showEditProfileDialog(BuildContext context, user_state.UserModel user) {
final nomController = TextEditingController(text: user.nom);
final prenomController = TextEditingController(text: user.prenom);
final phoneController = TextEditingController(text: user.phoneNumber ?? '');
showDialog(
context: context,
builder: (BuildContext dialogContext) {
return StatefulBuilder(
builder: (context, setState) {
return AlertDialog(
title: Text('Modifier le profil'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Photo de profil avec option de changement
Center(
child: Stack(
children: [
CircleAvatar(
radius: 50,
backgroundColor: Theme.of(
context,
).colorScheme.primary,
child: Text(
prenomController.text.isNotEmpty
? prenomController.text[0].toUpperCase()
: 'U',
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
Positioned(
bottom: 0,
right: 0,
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Theme.of(context).colorScheme.primary,
),
child: IconButton(
icon: Icon(
Icons.camera_alt,
color: Colors.white,
size: 20,
),
onPressed: () {
_showPhotoPickerDialog(dialogContext);
},
),
),
),
],
),
),
SizedBox(height: 24),
// Champ Prénom
TextField(
controller: prenomController,
decoration: InputDecoration(
labelText: 'Prénom',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.person_outline),
),
onChanged: (_) {
setState(() {});
},
),
SizedBox(height: 16),
// Champ Nom
TextField(
controller: nomController,
decoration: InputDecoration(
labelText: 'Nom',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.person_outline),
),
),
SizedBox(height: 16),
// Champ Téléphone
TextField(
controller: phoneController,
decoration: InputDecoration(
labelText: 'Numéro de téléphone',
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.phone_outlined),
hintText: '+33 6 12 34 56 78',
),
keyboardType: TextInputType.phone,
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(),
child: Text('Annuler'),
),
TextButton(
onPressed: () {
if (prenomController.text.trim().isNotEmpty) {
context.read<UserBloc>().add(
user_event.UserUpdated({
'prenom': prenomController.text.trim(),
'nom': nomController.text.trim(),
'phoneNumber': phoneController.text.trim(),
}),
);
Navigator.of(dialogContext).pop();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Profil mis à jour !'),
backgroundColor: Colors.green,
),
);
}
},
child: Text('Sauvegarder'),
),
],
);
},
);
},
);
}
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 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 {
try {
final ImagePicker picker = ImagePicker();
final XFile? image = await picker.pickImage(source: ImageSource.gallery);
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',
);
}
}
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 && context.mounted) {
await _uploadProfilePicture(context, image.path, userBloc);
}
} catch (e) {
_errorService.showError(message: 'Erreur lors de la prise de photo');
}
}
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;
}
LoggerService.info(
'DEBUG: Taille du fichier: ${imageFile.lengthSync()} bytes',
);
final userState = userBloc.state;
if (userState is! user_state.UserLoaded) {
_errorService.showError(message: 'Utilisateur non connecté');
return;
}
final user = userState.user;
// Créer un nom unique pour la photo
final String fileName =
'profile_${user.id}_${DateTime.now().millisecondsSinceEpoch}.jpg';
final Reference storageRef = FirebaseStorage.instance
.ref()
.child('profile_pictures')
.child(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) {
LoggerService.info(
'DEBUG: Progression: ${snapshot.bytesTransferred}/${snapshot.totalBytes}',
);
});
final snapshot = await uploadTask;
LoggerService.info('DEBUG: Upload terminé. État: ${snapshot.state}');
} on FirebaseException catch (e) {
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}',
);
}
return;
}
LoggerService.info('DEBUG: Upload terminé, récupération de l\'URL');
// Récupérer l'URL
final String downloadUrl = await storageRef.getDownloadURL();
LoggerService.info('DEBUG: URL obtenue: $downloadUrl');
// Mettre à jour le profil avec l'URL en utilisant la référence sauvegardée du BLoC
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) {
LoggerService.info('DEBUG: Affichage du succès');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Photo de profil mise à jour !'),
backgroundColor: Colors.green,
),
);
}
} catch (e, 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',
);
if (context.mounted) {
_errorService.showError(message: 'Erreur: ${e.toString()}');
}
}
}
void _showChangePasswordDialog(
BuildContext context,
user_state.UserModel user,
) {
final currentPasswordController = TextEditingController();
final newPasswordController = TextEditingController();
final confirmPasswordController = TextEditingController();
final authService = AuthService();
showDialog(
context: context,
builder: (BuildContext dialogContext) {
return AlertDialog(
title: Text('Changer le mot de passe'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: currentPasswordController,
obscureText: true,
decoration: InputDecoration(labelText: 'Mot de passe actuel'),
),
SizedBox(height: 16),
TextField(
controller: newPasswordController,
obscureText: true,
decoration: InputDecoration(labelText: 'Nouveau mot de passe'),
),
SizedBox(height: 16),
TextField(
controller: confirmPasswordController,
obscureText: true,
decoration: InputDecoration(
labelText: 'Confirmer le mot de passe',
),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(),
child: Text('Annuler'),
),
TextButton(
onPressed: () async {
if (currentPasswordController.text.isEmpty ||
newPasswordController.text.isEmpty ||
confirmPasswordController.text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Tous les champs sont requis'),
backgroundColor: Colors.red,
),
);
return;
}
if (newPasswordController.text !=
confirmPasswordController.text) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Les mots de passe ne correspondent pas'),
backgroundColor: Colors.red,
),
);
return;
}
try {
await authService.resetPasswordFromCurrentPassword(
currentPassword: currentPasswordController.text,
newPassword: newPasswordController.text,
email: user.email,
);
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',
);
}
},
child: Text('Changer'),
),
],
);
},
);
}
void _showDeleteAccountDialog(
BuildContext context,
user_state.UserModel user,
) {
final passwordController = TextEditingController();
final authService = AuthService();
showDialog(
context: context,
builder: (BuildContext dialogContext) {
return AlertDialog(
title: Text('Supprimer le compte'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Êtes-vous sûr de vouloir supprimer votre compte ? Cette action est irréversible.',
),
SizedBox(height: 16),
TextField(
controller: passwordController,
obscureText: true,
decoration: InputDecoration(
labelText: 'Confirmez votre mot de passe',
border: OutlineInputBorder(),
),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(),
child: Text('Annuler'),
),
TextButton(
onPressed: () async {
try {
await authService.deleteAccount(
password: passwordController.text,
email: user.email,
);
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',
);
}
},
style: TextButton.styleFrom(foregroundColor: Colors.red),
child: Text('Supprimer'),
),
],
);
},
);
}
}