import 'dart:io'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'package:image_picker/image_picker.dart'; import 'package:provider/provider.dart'; import '../../../core/constants/design_system.dart'; import '../../../data/datasources/user_remote_data_source.dart'; import '../../../data/models/user_model.dart'; import '../../../data/providers/user_provider.dart'; import '../../../data/services/preferences_helper.dart'; import '../../../data/services/secure_storage.dart'; import '../../../domain/entities/user.dart'; import '../../widgets/animated_widgets.dart'; import '../../widgets/custom_snackbar.dart'; /// Écran d'édition de profil utilisateur avec design moderne. /// /// Permet à l'utilisateur de modifier: /// - Prénom et nom /// - Email /// - Photo de profil /// - Mot de passe class EditProfileScreen extends StatefulWidget { const EditProfileScreen({required this.user, super.key}); final User user; @override State createState() => _EditProfileScreenState(); } class _EditProfileScreenState extends State { final _formKey = GlobalKey(); final _firstNameController = TextEditingController(); final _lastNameController = TextEditingController(); final _emailController = TextEditingController(); final UserRemoteDataSource _dataSource = UserRemoteDataSource(http.Client()); final SecureStorage _secureStorage = SecureStorage(); final PreferencesHelper _preferencesHelper = PreferencesHelper(); final ImagePicker _imagePicker = ImagePicker(); bool _isLoading = false; bool _hasChanges = false; File? _newProfileImage; String? _newProfileImageUrl; @override void initState() { super.initState(); _firstNameController.text = widget.user.userFirstName; _lastNameController.text = widget.user.userLastName; _emailController.text = widget.user.email; // Écoute les changements pour activer/désactiver le bouton Sauvegarder _firstNameController.addListener(_onFieldChanged); _lastNameController.addListener(_onFieldChanged); _emailController.addListener(_onFieldChanged); } @override void dispose() { _firstNameController.dispose(); _lastNameController.dispose(); _emailController.dispose(); super.dispose(); } void _onFieldChanged() { final hasChanges = _firstNameController.text != widget.user.userFirstName || _lastNameController.text != widget.user.userLastName || _emailController.text != widget.user.email || _newProfileImage != null; if (hasChanges != _hasChanges) { setState(() { _hasChanges = hasChanges; }); } } Future _pickImage() async { try { final XFile? pickedFile = await _imagePicker.pickImage( source: ImageSource.gallery, maxWidth: 1024, maxHeight: 1024, imageQuality: 85, ); if (pickedFile != null) { setState(() { _newProfileImage = File(pickedFile.path); _hasChanges = true; }); } } catch (e) { if (mounted) { context.showError('Erreur lors de la sélection de l\'image'); } } } Future _saveChanges() async { if (!_formKey.currentState!.validate()) { return; } if (!_hasChanges) { context.showInfo('Aucune modification à enregistrer'); return; } setState(() { _isLoading = true; }); try { // TODO: Si une nouvelle image est sélectionnée, l'uploader d'abord // et récupérer l'URL depuis le backend String profileImageUrl = widget.user.profileImageUrl; if (_newProfileImage != null) { // Pour l'instant, on utilise l'URL existante // Dans une vraie implémentation, il faudrait uploader l'image // vers le backend et récupérer la nouvelle URL context.showWarning('L\'upload d\'image sera implémenté avec le backend'); } // Créer le modèle utilisateur mis à jour final updatedUser = UserModel( userId: widget.user.userId, userFirstName: _firstNameController.text.trim(), userLastName: _lastNameController.text.trim(), email: _emailController.text.trim(), motDePasse: widget.user.motDePasse, // Mot de passe inchangé profileImageUrl: profileImageUrl, ); // Envoyer la mise à jour au backend final result = await _dataSource.updateUser(updatedUser); if (mounted) { // Mettre à jour le provider final userProvider = Provider.of(context, listen: false); userProvider.setUser(result.toEntity()); // Mettre à jour le stockage local await _preferencesHelper.saveUserName(result.userFirstName); await _preferencesHelper.saveUserLastName(result.userLastName); setState(() { _isLoading = false; _hasChanges = false; }); context.showSuccess('Profil mis à jour avec succès'); Navigator.pop(context, true); // Retourne true pour indiquer la modification } } catch (e) { if (mounted) { setState(() { _isLoading = false; }); context.showError('Erreur lors de la mise à jour: ${e.toString()}'); } } } Future _changePassword() async { final result = await showDialog( context: context, builder: (context) => const _ChangePasswordDialog(), ); if (result == true && mounted) { context.showSuccess('Mot de passe modifié avec succès'); } } @override Widget build(BuildContext context) { final theme = Theme.of(context); return Scaffold( appBar: AppBar( title: const Text('Modifier le profil'), actions: [ if (_hasChanges && !_isLoading) TextButton( onPressed: _saveChanges, child: const Text( 'SAUVEGARDER', style: TextStyle(fontWeight: FontWeight.bold), ), ), ], ), body: _isLoading ? const Center(child: CircularProgressIndicator()) : SingleChildScrollView( padding: const EdgeInsets.all(DesignSystem.spacingLg), child: Form( key: _formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Photo de profil Center( child: Stack( children: [ Hero( tag: 'user_profile_avatar_${widget.user.userId}', child: CircleAvatar( radius: 60, backgroundImage: _newProfileImage != null ? FileImage(_newProfileImage!) : NetworkImage(widget.user.profileImageUrl) as ImageProvider, child: _newProfileImage == null && widget.user.profileImageUrl.isEmpty ? const Icon(Icons.person, size: 60) : null, ), ), Positioned( right: 0, bottom: 0, child: AnimatedScaleButton( onTap: _pickImage, child: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: theme.colorScheme.primary, shape: BoxShape.circle, boxShadow: DesignSystem.shadowMd, ), child: const Icon( Icons.camera_alt, color: Colors.white, size: 20, ), ), ), ), ], ), ), const SizedBox(height: DesignSystem.spacing2xl), // Prénom TextFormField( controller: _firstNameController, decoration: InputDecoration( labelText: 'Prénom', prefixIcon: const Icon(Icons.person), border: OutlineInputBorder( borderRadius: BorderRadius.circular(DesignSystem.radiusMd), ), ), validator: (value) { if (value == null || value.trim().isEmpty) { return 'Le prénom est requis'; } if (value.trim().length < 2) { return 'Le prénom doit contenir au moins 2 caractères'; } return null; }, ), const SizedBox(height: DesignSystem.spacingLg), // Nom TextFormField( controller: _lastNameController, decoration: InputDecoration( labelText: 'Nom', prefixIcon: const Icon(Icons.person_outline), border: OutlineInputBorder( borderRadius: BorderRadius.circular(DesignSystem.radiusMd), ), ), validator: (value) { if (value == null || value.trim().isEmpty) { return 'Le nom est requis'; } if (value.trim().length < 2) { return 'Le nom doit contenir au moins 2 caractères'; } return null; }, ), const SizedBox(height: DesignSystem.spacingLg), // Email TextFormField( controller: _emailController, keyboardType: TextInputType.emailAddress, decoration: InputDecoration( labelText: 'Email', prefixIcon: const Icon(Icons.email), border: OutlineInputBorder( borderRadius: BorderRadius.circular(DesignSystem.radiusMd), ), ), validator: (value) { if (value == null || value.trim().isEmpty) { return 'L\'email est requis'; } final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$'); if (!emailRegex.hasMatch(value.trim())) { return 'Email invalide'; } return null; }, ), const SizedBox(height: DesignSystem.spacing2xl), // Bouton Changer le mot de passe OutlinedButton.icon( onPressed: _changePassword, icon: const Icon(Icons.lock), label: const Text('Changer le mot de passe'), style: OutlinedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(DesignSystem.radiusMd), ), ), ), const SizedBox(height: DesignSystem.spacingLg), // Bouton Sauvegarder (grand bouton en bas) if (_hasChanges) FadeInWidget( child: ElevatedButton( onPressed: _isLoading ? null : _saveChanges, style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(DesignSystem.radiusMd), ), ), child: _isLoading ? const SizedBox( height: 20, width: 20, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white, ), ) : const Text( 'Enregistrer les modifications', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), ), ), ], ), ), ), ); } } /// Dialogue pour changer le mot de passe class _ChangePasswordDialog extends StatefulWidget { const _ChangePasswordDialog(); @override State<_ChangePasswordDialog> createState() => _ChangePasswordDialogState(); } class _ChangePasswordDialogState extends State<_ChangePasswordDialog> { final _formKey = GlobalKey(); final _currentPasswordController = TextEditingController(); final _newPasswordController = TextEditingController(); final _confirmPasswordController = TextEditingController(); bool _isLoading = false; bool _obscureCurrentPassword = true; bool _obscureNewPassword = true; bool _obscureConfirmPassword = true; @override void dispose() { _currentPasswordController.dispose(); _newPasswordController.dispose(); _confirmPasswordController.dispose(); super.dispose(); } Future _changePassword() async { if (!_formKey.currentState!.validate()) { return; } setState(() { _isLoading = true; }); try { // TODO: Appel API pour changer le mot de passe // await _dataSource.changePassword( // currentPassword: _currentPasswordController.text, // newPassword: _newPasswordController.text, // ); // Simulation pour l'instant await Future.delayed(const Duration(seconds: 1)); if (mounted) { Navigator.pop(context, true); } } catch (e) { if (mounted) { setState(() { _isLoading = false; }); context.showError('Erreur: ${e.toString()}'); } } } @override Widget build(BuildContext context) { return AlertDialog( title: const Text('Changer le mot de passe'), content: Form( key: _formKey, child: Column( mainAxisSize: MainAxisSize.min, children: [ // Mot de passe actuel TextFormField( controller: _currentPasswordController, obscureText: _obscureCurrentPassword, decoration: InputDecoration( labelText: 'Mot de passe actuel', prefixIcon: const Icon(Icons.lock_outline), suffixIcon: IconButton( icon: Icon( _obscureCurrentPassword ? Icons.visibility : Icons.visibility_off, ), onPressed: () { setState(() { _obscureCurrentPassword = !_obscureCurrentPassword; }); }, ), ), validator: (value) { if (value == null || value.isEmpty) { return 'Mot de passe requis'; } return null; }, ), const SizedBox(height: DesignSystem.spacingLg), // Nouveau mot de passe TextFormField( controller: _newPasswordController, obscureText: _obscureNewPassword, decoration: InputDecoration( labelText: 'Nouveau mot de passe', prefixIcon: const Icon(Icons.lock), suffixIcon: IconButton( icon: Icon( _obscureNewPassword ? Icons.visibility : Icons.visibility_off, ), onPressed: () { setState(() { _obscureNewPassword = !_obscureNewPassword; }); }, ), ), validator: (value) { if (value == null || value.isEmpty) { return 'Nouveau mot de passe requis'; } if (value.length < 8) { return 'Minimum 8 caractères'; } return null; }, ), const SizedBox(height: DesignSystem.spacingLg), // Confirmer nouveau mot de passe TextFormField( controller: _confirmPasswordController, obscureText: _obscureConfirmPassword, decoration: InputDecoration( labelText: 'Confirmer le mot de passe', prefixIcon: const Icon(Icons.lock), suffixIcon: IconButton( icon: Icon( _obscureConfirmPassword ? Icons.visibility : Icons.visibility_off, ), onPressed: () { setState(() { _obscureConfirmPassword = !_obscureConfirmPassword; }); }, ), ), validator: (value) { if (value == null || value.isEmpty) { return 'Confirmation requise'; } if (value != _newPasswordController.text) { return 'Les mots de passe ne correspondent pas'; } return null; }, ), ], ), ), actions: [ TextButton( onPressed: _isLoading ? null : () => Navigator.pop(context), child: const Text('Annuler'), ), ElevatedButton( onPressed: _isLoading ? null : _changePassword, child: _isLoading ? const SizedBox( height: 20, width: 20, child: CircularProgressIndicator(strokeWidth: 2), ) : const Text('Changer'), ), ], ); } }