import 'dart:async'; import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:go_router/go_router.dart'; import 'package:p_tits_pas/services/bug_report_service.dart'; import '../../widgets/image_button.dart'; import '../../widgets/custom_app_text_field.dart'; import '../../services/auth_service.dart'; import '../../widgets/auth/change_password_dialog.dart'; class LoginScreen extends StatefulWidget { const LoginScreen({super.key}); @override State createState() => _LoginPageState(); } class _LoginPageState extends State with WidgetsBindingObserver { final _formKey = GlobalKey(); final _emailController = TextEditingController(); final _passwordController = TextEditingController(); bool _isLoading = false; String? _errorMessage; static const double _mobileBreakpoint = 900.0; @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); _emailController.dispose(); _passwordController.dispose(); super.dispose(); } @override void didChangeMetrics() { super.didChangeMetrics(); if (mounted) setState(() {}); } String? _validateEmail(String? value) { if (value == null || value.isEmpty) { return 'Veuillez entrer votre email'; } if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) { return 'Veuillez entrer un email valide'; } return null; } String? _validatePassword(String? value) { if (value == null || value.isEmpty) { return 'Veuillez entrer votre mot de passe'; } return null; } /// Gère la connexion de l'utilisateur Future _handleLogin() async { // Réinitialiser le message d'erreur setState(() { _errorMessage = null; }); if (!(_formKey.currentState?.validate() ?? false)) { return; } setState(() { _isLoading = true; }); try { // Appeler le service d'authentification final user = await AuthService.login( _emailController.text.trim(), _passwordController.text, ); if (!mounted) return; // Vérifier si l'utilisateur doit changer son mot de passe if (user.changementMdpObligatoire) { if (!mounted) return; // Afficher la modale de changement de mot de passe (non-dismissible) final result = await showDialog( context: context, barrierDismissible: false, builder: (context) => const ChangePasswordDialog(), ); // Si le changement de mot de passe a réussi, rafraîchir l'utilisateur if (result == true && mounted) { await AuthService.refreshCurrentUser(); } } if (!mounted) return; // Rediriger selon le rôle de l'utilisateur _redirectUserByRole(user.role); } catch (e) { setState(() { _isLoading = false; _errorMessage = e.toString().replaceAll('Exception: ', ''); }); } } /// Redirige l'utilisateur selon son rôle void _redirectUserByRole(String role) { switch (role.toLowerCase()) { case 'super_admin': case 'gestionnaire': Navigator.pushReplacementNamed(context, '/admin-dashboard'); break; case 'parent': Navigator.pushReplacementNamed(context, '/parent-dashboard'); break; case 'assistante_maternelle': Navigator.pushReplacementNamed(context, '/am-dashboard'); break; default: Navigator.pushReplacementNamed(context, '/home'); } } @override Widget build(BuildContext context) { final isMobile = MediaQuery.of(context).size.width < _mobileBreakpoint; if (isMobile) { return Scaffold( backgroundColor: Colors.transparent, body: _buildMobileLayout(context), ); } return Scaffold( backgroundColor: Colors.transparent, body: LayoutBuilder( builder: (context, constraints) { final w = constraints.maxWidth; final h = constraints.maxHeight; return FutureBuilder( future: _getImageDimensions(), builder: (context, snapshot) { if (!snapshot.hasData) { return const Center(child: CircularProgressIndicator()); } final imageDimensions = snapshot.data!; final imageHeight = h; final imageWidth = imageHeight * (imageDimensions.width / imageDimensions.height); final remainingWidth = w - imageWidth; final leftMargin = remainingWidth / 4; return Stack( children: [ // Fond en papier Positioned.fill( child: Image.asset( 'assets/images/paper2.png', fit: BoxFit.cover, repeat: ImageRepeat.repeat, ), ), // Image principale Positioned( left: leftMargin, top: 0, height: imageHeight, width: imageWidth, child: Image.asset( 'assets/images/river_logo_desktop.png', fit: BoxFit.contain, ), ), // Formulaire dans le cadran en bas à droite Positioned( right: 0, bottom: 0, width: w * 0.6, // 60% de la largeur de l'écran height: h * 0.5, // 50% de la hauteur de l'écran child: Padding( padding: EdgeInsets.all(w * 0.02), // 2% de padding child: Form( key: _formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Champs côte à côte Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: CustomAppTextField( controller: _emailController, labelText: 'Email', hintText: 'Votre adresse email', validator: _validateEmail, style: CustomAppTextFieldStyle.lavande, fieldHeight: 53, fieldWidth: double.infinity, ), ), const SizedBox(width: 20), Expanded( child: CustomAppTextField( controller: _passwordController, labelText: 'Mot de passe', hintText: 'Votre mot de passe', obscureText: true, validator: _validatePassword, style: CustomAppTextFieldStyle.jaune, fieldHeight: 53, fieldWidth: double.infinity, ), ), ], ), const SizedBox(height: 20), // Message d'erreur if (_errorMessage != null) Container( padding: const EdgeInsets.all(12), margin: const EdgeInsets.only(bottom: 15), decoration: BoxDecoration( color: Colors.red[50], borderRadius: BorderRadius.circular(10), border: Border.all(color: Colors.red[300]!), ), child: Row( children: [ Icon(Icons.error_outline, color: Colors.red[700], size: 20), const SizedBox(width: 10), Expanded( child: Text( _errorMessage!, style: GoogleFonts.merienda( fontSize: 12, color: Colors.red[700], ), ), ), ], ), ), // Bouton centré Center( child: _isLoading ? const CircularProgressIndicator() : ImageButton( bg: 'assets/images/bg_green.png', width: 300, height: 40, text: 'Se connecter', textColor: const Color(0xFF2D6A4F), onPressed: _handleLogin, ), ), const SizedBox(height: 10), // Lien mot de passe oublié Center( child: TextButton( onPressed: () { // TODO: Implémenter la logique de récupération de mot de passe }, child: Text( 'Mot de passe oublié ?', style: GoogleFonts.merienda( fontSize: 14, color: const Color(0xFF2D6A4F), decoration: TextDecoration.underline, ), ), ), ), const SizedBox(height: 20), // Lien de création de compte (version originale) Center( child: TextButton( onPressed: () { context.go('/register-choice'); }, child: Text( 'Créer un compte', style: GoogleFonts.merienda( fontSize: 16, color: const Color(0xFF2D6A4F), decoration: TextDecoration.underline, ), ), ), ), ], ), ), ), ), // Pied de page (Wrap pour éviter overflow sur petite largeur) Positioned( left: 0, right: 0, bottom: 0, child: Container( padding: const EdgeInsets.symmetric(vertical: 8.0), decoration: const BoxDecoration( color: Colors.transparent, ), child: Wrap( alignment: WrapAlignment.center, runSpacing: 8, children: [ _FooterLink( text: 'Contact support', onTap: () async { final Uri emailLaunchUri = Uri( scheme: 'mailto', path: 'support@supernounou.local', ); if (await canLaunchUrl(emailLaunchUri)) { await launchUrl(emailLaunchUri); } else { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( 'Impossible d\'ouvrir le client mail', style: GoogleFonts.merienda(), ), ), ); } }, ), _FooterLink( text: 'Signaler un bug', onTap: () { _showBugReportDialog(context); }, ), _FooterLink( text: 'Mentions légales', onTap: () { context.go('/legal'); }, ), _FooterLink( text: 'Politique de confidentialité', onTap: () { context.go('/privacy'); }, ), ], ), ), ), ], ); }, ); }, ), ); } /// Dimensions de river_logo_mobile.png (à mettre à jour si l'asset change). static const int _riverLogoMobileWidth = 600; static const int _riverLogoMobileHeight = 1080; /// Fraction de la hauteur de l'image où se termine visuellement le slogan (0 = haut, 1 = bas). static const double _sloganEndFraction = 0.42; static const double _gapBelowSlogan = 12.0; Widget _buildMobileLayout(BuildContext context) { return LayoutBuilder( builder: (context, constraints) { final h = constraints.maxHeight; final w = constraints.maxWidth; final imageAspectRatio = _riverLogoMobileHeight / _riverLogoMobileWidth; final formTop = w * imageAspectRatio * _sloganEndFraction + _gapBelowSlogan; return Stack( clipBehavior: Clip.none, children: [ Positioned.fill( child: Image.asset( 'assets/images/paper2.png', fit: BoxFit.cover, repeat: ImageRepeat.repeat, ), ), Positioned( top: 0, left: 0, right: 0, height: h * 1.2, child: OverflowBox( alignment: Alignment.topCenter, minWidth: w, maxWidth: w, minHeight: 0, maxHeight: h * 2.5, child: Image.asset( 'assets/images/river_logo_mobile.png', width: w, fit: BoxFit.fitWidth, ), ), ), Positioned( top: formTop, left: 0, right: 0, bottom: 0, child: SafeArea( top: false, child: Column( children: [ Expanded( child: SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 20), child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 400), child: Form( key: _formKey, child: Column( mainAxisSize: MainAxisSize.min, children: [ const SizedBox(height: 16), CustomAppTextField( controller: _emailController, labelText: 'Email', showLabel: false, hintText: 'Votre adresse email', validator: _validateEmail, style: CustomAppTextFieldStyle.lavande, fieldHeight: 48, fieldWidth: double.infinity, ), const SizedBox(height: 12), CustomAppTextField( controller: _passwordController, labelText: 'Mot de passe', showLabel: false, hintText: 'Votre mot de passe', obscureText: true, validator: _validatePassword, style: CustomAppTextFieldStyle.jaune, fieldHeight: 48, fieldWidth: double.infinity, ), if (_errorMessage != null) ...[ const SizedBox(height: 12), Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.red.shade50, borderRadius: BorderRadius.circular(10), border: Border.all(color: Colors.red.shade300), ), child: Row( children: [ Icon(Icons.error_outline, color: Colors.red.shade700, size: 20), const SizedBox(width: 10), Expanded( child: Text( _errorMessage!, style: GoogleFonts.merienda(fontSize: 12, color: Colors.red.shade700), ), ), ], ), ), ], const SizedBox(height: 12), _isLoading ? const CircularProgressIndicator() : ImageButton( bg: 'assets/images/bg_green.png', width: double.infinity, height: 44, text: 'Se connecter', textColor: const Color(0xFF2D6A4F), onPressed: _handleLogin, ), const SizedBox(height: 12), TextButton( onPressed: () { /* TODO */ }, child: Text( 'Mot de passe oublié ?', style: GoogleFonts.merienda( fontSize: 14, color: const Color(0xFF2D6A4F), decoration: TextDecoration.underline, ), ), ), TextButton( onPressed: () => context.go('/register-choice'), child: Text( 'Créer un compte', style: GoogleFonts.merienda( fontSize: 16, color: const Color(0xFF2D6A4F), decoration: TextDecoration.underline, ), ), ), ], ), ), ), ), ), Padding( padding: const EdgeInsets.only(bottom: 12, top: 8), child: Wrap( alignment: WrapAlignment.center, runSpacing: 6, spacing: 4, children: [ _FooterLink( text: 'Contact support', fontSize: 11, onTap: () async { final uri = Uri(scheme: 'mailto', path: 'support@supernounou.local'); if (await canLaunchUrl(uri)) { await launchUrl(uri); } else if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Impossible d\'ouvrir le client mail', style: GoogleFonts.merienda())), ); } }, ), _FooterLink( text: 'Signaler un bug', fontSize: 11, onTap: () => _showBugReportDialog(context), ), _FooterLink( text: 'Mentions légales', fontSize: 11, onTap: () => context.go('/legal'), ), _FooterLink( text: 'Politique de confidentialité', fontSize: 11, onTap: () => context.go('/privacy'), ), ], ), ), ], ), ), ), ], ); }, ); } void _showBugReportDialog(BuildContext context) { final TextEditingController controller = TextEditingController(); showDialog( context: context, builder: (context) => AlertDialog( title: Text( 'Signaler un bug', style: GoogleFonts.merienda(), ), content: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( controller: controller, maxLines: 5, decoration: InputDecoration( hintText: 'Décrivez le problème rencontré...', border: OutlineInputBorder( borderRadius: BorderRadius.circular(10), ), ), ), ], ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: Text( 'Annuler', style: GoogleFonts.merienda(), ), ), TextButton( onPressed: () async { if (controller.text.trim().isEmpty) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( 'Veuillez décrire le problème', style: GoogleFonts.merienda(), ), ), ); return; } try { await BugReportService.sendReport(controller.text); if (context.mounted) { Navigator.pop(context); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( 'Rapport envoyé avec succès', style: GoogleFonts.merienda(), ), ), ); } } catch (e) { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( 'Erreur lors de l\'envoi du rapport', style: GoogleFonts.merienda(), ), ), ); } } }, child: Text( 'Envoyer', style: GoogleFonts.merienda(), ), ), ], ), ); } Future _getImageDimensions() async { final image = Image.asset('assets/images/river_logo_desktop.png'); final completer = Completer(); image.image.resolve(const ImageConfiguration()).addListener( ImageStreamListener((info, _) { completer.complete(ImageDimensions( width: info.image.width.toDouble(), height: info.image.height.toDouble(), )); }), ); return completer.future; } } class ImageDimensions { final double width; final double height; ImageDimensions({required this.width, required this.height}); } // ─────────────────────────────────────────────────────────────── // Lien du pied de page // ─────────────────────────────────────────────────────────────── class _FooterLink extends StatelessWidget { final String text; final VoidCallback onTap; final double fontSize; const _FooterLink({ required this.text, required this.onTap, this.fontSize = 14.0, }); @override Widget build(BuildContext context) { return InkWell( onTap: onTap, child: Padding( padding: EdgeInsets.symmetric(horizontal: fontSize > 12 ? 8.0 : 4.0), child: Text( text, style: GoogleFonts.merienda( fontSize: fontSize, color: Colors.black87, decoration: TextDecoration.underline, ), ), ), ); } }