diff --git a/lib/features/auth/data/auth_service.dart b/lib/features/auth/data/auth_service.dart index b38bfb4..a36d1aa 100644 --- a/lib/features/auth/data/auth_service.dart +++ b/lib/features/auth/data/auth_service.dart @@ -69,6 +69,26 @@ class AuthService extends ChangeNotifier { notifyListeners(); } + /// Réinitialisation du mot de passe + Future resetPassword(String email) async { + _isLoading = true; + notifyListeners(); + + // Simulation d'une requête réseau + await Future.delayed(const Duration(milliseconds: 1500)); + + // Validation basique de l'email + if (email.trim().isEmpty || !email.contains('@')) { + _isLoading = false; + notifyListeners(); + return AuthResult.error('Email invalide'); + } + + _isLoading = false; + notifyListeners(); + return AuthResult.success(); + } + /// Vérifier si l'utilisateur est connecté au démarrage Future checkAuthStatus() async { await Future.delayed(const Duration(milliseconds: 500)); diff --git a/lib/features/auth/presentation/screens/login_screen.dart b/lib/features/auth/presentation/screens/login_screen.dart index 2ab8b29..ebab4a9 100644 --- a/lib/features/auth/presentation/screens/login_screen.dart +++ b/lib/features/auth/presentation/screens/login_screen.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; import '../../../../core/router/app_router.dart'; import '../../../../core/theme/app_colors.dart'; @@ -6,6 +7,7 @@ import '../../../../core/theme/app_text_styles.dart'; import '../../../../core/theme/app_theme.dart'; import '../../../../shared/widgets/custom_button.dart'; import '../../../../shared/widgets/custom_text_field.dart'; +import '../../data/auth_service.dart'; /// Écran de connexion moderne et élégant /// @@ -74,16 +76,97 @@ class _LoginScreenState extends State setState(() => _isLoading = true); - // Simulation d'une requête réseau - await Future.delayed(const Duration(seconds: 1)); + // Utiliser le service d'authentification + final authService = context.read(); + final result = await authService.login( + _emailController.text.trim(), + _passwordController.text, + ); if (!mounted) return; - // TODO: Le Lead Auth remplacera par la vraie logique setState(() => _isLoading = false); - // Navigation vers les tâches - context.goToTasks(); + if (result.success) { + // Navigation vers les tâches + context.goToTasks(); + } else { + // Afficher un message d'erreur + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(result.errorMessage ?? 'Erreur lors de la connexion'), + backgroundColor: Colors.red, + ), + ); + } + } + + /// Fonction pour gérer le mot de passe oublié + Future _handleForgotPassword() async { + final emailController = TextEditingController(); + final formKey = GlobalKey(); + + final emailToReset = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Réinitialiser le mot de passe'), + content: Form( + key: formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + 'Entrez votre adresse email et nous vous enverrons un lien pour réinitialiser votre mot de passe.', + ), + const SizedBox(height: 16), + CustomTextField( + controller: emailController, + label: 'Email', + hint: 'votre.email@exemple.com', + keyboardType: TextInputType.emailAddress, + prefixIcon: Icons.email_outlined, + validator: _validateEmail, + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Annuler'), + ), + ElevatedButton( + onPressed: () { + if (formKey.currentState!.validate()) { + Navigator.of(context).pop(emailController.text); + } + }, + child: const Text('Envoyer'), + ), + ], + ), + ); + + if (emailToReset != null && mounted) { + // Simuler l'envoi de l'email + setState(() => _isLoading = true); + + await Future.delayed(const Duration(seconds: 1)); + + if (!mounted) return; + + setState(() => _isLoading = false); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Un email de réinitialisation a été envoyé à $emailToReset', + ), + backgroundColor: Colors.green, + duration: const Duration(seconds: 4), + ), + ); + } } @override @@ -257,19 +340,6 @@ class _LoginScreenState extends State isLoading: _isLoading, child: const Text('Se connecter'), ), - - const SizedBox(height: 16), - - // Lien d'inscription - TextButton( - onPressed: () { - // Navigation vers inscription si nécessaire - }, - child: Text( - 'Pas encore de compte ? S\'inscrire', - style: AppTextStyles.bodyMedium(context), - ), - ), ], ), ), @@ -291,12 +361,7 @@ class _LoginScreenState extends State // Lien mot de passe oublié TextButton( - onPressed: () { - // TODO: Implémenter la récupération de mot de passe - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Fonctionnalité à venir')), - ); - }, + onPressed: _handleForgotPassword, child: const Text( 'Mot de passe oublié ?', style: TextStyle(color: Colors.white70), diff --git a/lib/features/auth/presentation/screens/register_screen.dart b/lib/features/auth/presentation/screens/register_screen.dart index 7a76282..e8f59fe 100644 --- a/lib/features/auth/presentation/screens/register_screen.dart +++ b/lib/features/auth/presentation/screens/register_screen.dart @@ -1,27 +1,382 @@ import 'package:flutter/material.dart'; import '../../../../core/router/app_router.dart'; +import '../../../../core/theme/app_colors.dart'; +import '../../../../core/theme/app_text_styles.dart'; +import '../../../../core/theme/app_theme.dart'; +import '../../../../shared/widgets/custom_button.dart'; +import '../../../../shared/widgets/custom_text_field.dart'; +import '../../data/auth_service.dart'; -class RegisterScreen extends StatelessWidget { +/// Écran d'inscription moderne et élégant +/// +/// Fonctionnalités : +/// - Design moderne avec gradient +/// - Formulaire avec validation +/// - Animation et feedback utilisateur +/// - Navigation fluide +class RegisterScreen extends StatefulWidget { const RegisterScreen({super.key}); + @override + State createState() => _RegisterScreenState(); +} + +class _RegisterScreenState extends State + with SingleTickerProviderStateMixin { + // Contrôleurs pour les champs de texte + final TextEditingController _nameController = TextEditingController(); + final TextEditingController _emailController = TextEditingController(); + final TextEditingController _passwordController = TextEditingController(); + final TextEditingController _confirmPasswordController = + TextEditingController(); + final GlobalKey _formKey = GlobalKey(); + + // États du formulaire + bool _isLoading = false; + bool _obscurePassword = true; + bool _obscureConfirmPassword = true; + + // Animation + late AnimationController _animationController; + late Animation _fadeAnimation; + late Animation _slideAnimation; + + @override + void initState() { + super.initState(); + + // Configuration des animations + _animationController = AnimationController( + duration: const Duration(milliseconds: 800), + vsync: this, + ); + + _fadeAnimation = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation(parent: _animationController, curve: Curves.easeOut), + ); + + _slideAnimation = + Tween(begin: const Offset(0, 0.3), end: Offset.zero).animate( + CurvedAnimation(parent: _animationController, curve: Curves.easeOut), + ); + + // Démarrer l'animation + _animationController.forward(); + } + + @override + void dispose() { + _nameController.dispose(); + _emailController.dispose(); + _passwordController.dispose(); + _confirmPasswordController.dispose(); + _animationController.dispose(); + super.dispose(); + } + + /// Fonction d'inscription + Future _handleRegister() async { + if (!_formKey.currentState!.validate()) return; + + setState(() => _isLoading = true); + + // Utiliser le service d'authentification + final authService = AuthService(); + final result = await authService.register( + _emailController.text.trim(), + _passwordController.text, + _nameController.text.trim(), + ); + + if (!mounted) return; + + setState(() => _isLoading = false); + + if (result.success) { + // Afficher un message de succès + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Inscription réussie !'), + backgroundColor: Colors.green, + ), + ); + + // Navigation vers les tâches + context.goToTasks(); + } else { + // Afficher un message d'erreur + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(result.errorMessage ?? 'Erreur lors de l\'inscription'), + backgroundColor: Colors.red, + ), + ); + } + } + @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: const Text('Inscription')), - body: Center( + body: Container( + decoration: const BoxDecoration(gradient: AppColors.primaryGradient), + child: SafeArea( + child: AnimatedBuilder( + animation: _animationController, + builder: (context, child) { + return FadeTransition( + opacity: _fadeAnimation, + child: SlideTransition( + position: _slideAnimation, + child: _buildContent(), + ), + ); + }, + ), + ), + ), + ); + } + + Widget _buildContent() { + return SingleChildScrollView( + padding: AppTheme.paddingLarge, + child: Column( + children: [ + const SizedBox(height: 40), + + // ===== HEADER AVEC LOGO ===== + _buildHeader(), + + const SizedBox(height: 40), + + // ===== FORMULAIRE D'INSCRIPTION ===== + _buildRegisterForm(), + + const SizedBox(height: 24), + + // ===== LIEN VERS CONNEXION ===== + _buildLoginLink(), + ], + ), + ); + } + + /// Header avec logo et titre + Widget _buildHeader() { + return Column( + children: [ + // Logo de l'app + Container( + width: 100, + height: 100, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(30), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: const Icon( + Icons.person_add_rounded, + size: 50, + color: AppColors.primary, + ), + ), + + const SizedBox(height: 24), + + // Titre principal + const Text( + 'Créer un compte', + style: TextStyle( + fontSize: 32, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + + const SizedBox(height: 8), + + // Sous-titre + const Text( + 'Rejoignez-nous et organisez vos tâches', + style: TextStyle(fontSize: 16, color: Colors.white70), + textAlign: TextAlign.center, + ), + ], + ); + } + + /// Formulaire d'inscription + Widget _buildRegisterForm() { + return Container( + padding: AppTheme.paddingLarge, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: AppTheme.radiusLarge, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: Form( + key: _formKey, child: Column( - mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - const Text('Écran d\'inscription'), - const SizedBox(height: 20), - ElevatedButton( - onPressed: () => context.goToLogin(), - child: const Text('Retour à la connexion'), + // Titre du formulaire + Text( + 'Inscription', + style: AppTextStyles.titleLarge(context), + textAlign: TextAlign.center, + ), + + const SizedBox(height: 8), + + Text( + 'Remplissez les informations ci-dessous', + style: AppTextStyles.bodyMedium(context), + textAlign: TextAlign.center, + ), + + const SizedBox(height: 32), + + // Champ nom + CustomTextField( + controller: _nameController, + label: 'Nom complet', + hint: 'Votre nom complet', + keyboardType: TextInputType.name, + prefixIcon: Icons.person_outlined, + validator: _validateName, + ), + + const SizedBox(height: 16), + + // Champ email + CustomTextField( + controller: _emailController, + label: 'Email', + hint: 'votre.email@exemple.com', + keyboardType: TextInputType.emailAddress, + prefixIcon: Icons.email_outlined, + validator: _validateEmail, + ), + + const SizedBox(height: 16), + + // Champ mot de passe + CustomTextField( + controller: _passwordController, + label: 'Mot de passe', + hint: 'Votre mot de passe', + prefixIcon: Icons.lock_outlined, + obscureText: _obscurePassword, + suffixIcon: IconButton( + icon: Icon( + _obscurePassword ? Icons.visibility_off : Icons.visibility, + ), + onPressed: () => + setState(() => _obscurePassword = !_obscurePassword), + ), + validator: _validatePassword, + ), + + const SizedBox(height: 16), + + // Champ confirmation mot de passe + CustomTextField( + controller: _confirmPasswordController, + label: 'Confirmer le mot de passe', + hint: 'Confirmez votre mot de passe', + prefixIcon: Icons.lock_outlined, + obscureText: _obscureConfirmPassword, + suffixIcon: IconButton( + icon: Icon( + _obscureConfirmPassword + ? Icons.visibility_off + : Icons.visibility, + ), + onPressed: () => setState( + () => _obscureConfirmPassword = !_obscureConfirmPassword), + ), + validator: _validateConfirmPassword, + ), + + const SizedBox(height: 24), + + // Bouton d'inscription + CustomButton( + onPressed: _isLoading ? null : _handleRegister, + isLoading: _isLoading, + child: const Text('S\'inscrire'), ), ], ), ), ); } + + /// Lien vers la page de connexion + Widget _buildLoginLink() { + return TextButton( + onPressed: () => context.goToLogin(), + child: const Text( + 'Déjà un compte ? Connectez-vous', + style: TextStyle(color: Colors.white), + ), + ); + } + + /// Validation du nom + String? _validateName(String? value) { + if (value == null || value.isEmpty) { + return 'Veuillez saisir votre nom'; + } + if (value.length < 2) { + return 'Le nom doit contenir au moins 2 caractères'; + } + return null; + } + + /// Validation de l'email + String? _validateEmail(String? value) { + if (value == null || value.isEmpty) { + return 'Veuillez saisir votre email'; + } + if (!RegExp(r'^[^@]+@[^@]+\.[^@]+').hasMatch(value)) { + return 'Format d\'email invalide'; + } + return null; + } + + /// Validation du mot de passe + String? _validatePassword(String? value) { + if (value == null || value.isEmpty) { + return 'Veuillez saisir votre mot de passe'; + } + if (value.length < 6) { + return 'Le mot de passe doit contenir au moins 6 caractères'; + } + return null; + } + + /// Validation de la confirmation du mot de passe + String? _validateConfirmPassword(String? value) { + if (value == null || value.isEmpty) { + return 'Veuillez confirmer votre mot de passe'; + } + if (value != _passwordController.text) { + return 'Les mots de passe ne correspondent pas'; + } + return null; + } } diff --git a/lib/features/tasks/presentation/screens/task_list_screen.dart b/lib/features/tasks/presentation/screens/task_list_screen.dart index 1cc485d..44bca30 100644 --- a/lib/features/tasks/presentation/screens/task_list_screen.dart +++ b/lib/features/tasks/presentation/screens/task_list_screen.dart @@ -173,6 +173,37 @@ class _TaskListScreenState extends State ), ), actions: [ + // Affichage de l'utilisateur connecté + Consumer( + builder: (context, authService, child) { + final email = authService.currentUserEmail; + if (email != null) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 8.0), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.person, + size: 18, + color: Colors.white, + ), + const SizedBox(width: 6), + Text( + email, + style: const TextStyle( + color: Colors.white, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ); + } + return const SizedBox.shrink(); + }, + ), const TaskSortButton(), // ✅ DEBUG : Voir l'état du thème Consumer(