import 'package:flutter/material.dart'; import 'dart:math' as math; import 'dart:convert'; import 'package:flutter/foundation.dart' show kIsWeb, kDebugMode; import 'package:geosector_app/core/services/js_stub.dart' if (dart.library.js) 'dart:js' as js; import 'package:go_router/go_router.dart'; import 'package:http/http.dart' as http; import 'package:geosector_app/core/services/app_info_service.dart'; import 'package:geosector_app/presentation/widgets/custom_button.dart'; import 'package:geosector_app/presentation/widgets/custom_text_field.dart'; import 'package:geosector_app/presentation/widgets/connectivity_indicator.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales class LoginPage extends StatefulWidget { final String? loginType; const LoginPage({super.key, this.loginType}); @override State createState() => _LoginPageState(); } // Class pour dessiner les petits points blancs sur le fond class DotsPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { final paint = Paint() ..color = Colors.white.withOpacity(0.5) ..style = PaintingStyle.fill; final random = math.Random(42); // Seed fixe pour consistance final numberOfDots = (size.width * size.height) ~/ 1500; for (int i = 0; i < numberOfDots; i++) { final x = random.nextDouble() * size.width; final y = random.nextDouble() * size.height; final radius = 1.0 + random.nextDouble() * 2.0; canvas.drawCircle(Offset(x, y), radius, paint); } } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => false; } class _LoginPageState extends State { final _formKey = GlobalKey(); final _usernameController = TextEditingController(); final _passwordController = TextEditingController(); final _usernameFocusNode = FocusNode(); bool _obscurePassword = true; String _appVersion = ''; // Type de connexion (utilisateur ou administrateur) late String _loginType; // État de la connexion Internet bool _isConnected = false; Future _getAppVersion() async { try { final packageInfo = await PackageInfo.fromPlatform(); if (mounted) { setState(() { _appVersion = packageInfo.version; }); } } catch (e) { debugPrint('Erreur lors de la récupération de la version: $e'); // Fallback sur la version du AppInfoService si elle existe if (mounted) { setState(() { _appVersion = AppInfoService.fullVersion .split(' ') .last; // Extraire juste le numéro }); } } } Future _checkConnectivity() async { await connectivityService.checkConnectivity(); if (mounted) { setState(() { _isConnected = connectivityService.isConnected; }); } } @override void initState() { super.initState(); // Vérification du type de connexion if (widget.loginType == null) { // Si aucun type n'est spécifié, naviguer vers la splash page print( 'LoginPage: Aucun type de connexion spécifié, navigation vers splash page'); WidgetsBinding.instance.addPostFrameCallback((_) { GoRouter.of(context).go('/'); }); _loginType = ''; } else { _loginType = widget.loginType!; print('LoginPage: Type de connexion utilisé: $_loginType'); } // En mode web, essayer de détecter le paramètre dans l'URL directement // UNIQUEMENT si le loginType n'est pas déjà 'user' if (kIsWeb && _loginType != 'user') { try { final uri = Uri.parse(Uri.base.toString()); // 1. Vérifier d'abord si nous avons déjà le paramètre 'type=user' final typeParam = uri.queryParameters['type']; if (typeParam != null && typeParam.trim().toLowerCase() == 'user') { setState(() { _loginType = 'user'; }); } // 2. Sinon, vérifier le fragment d'URL (hash) else if (uri.fragment.trim().toLowerCase() == 'user') { setState(() { _loginType = 'user'; }); } // 3. Enfin, si toujours pas de type 'user', vérifier le sessionStorage if (_loginType != 'user') { WidgetsBinding.instance.addPostFrameCallback((_) { try { // Utiliser une approche plus robuste pour accéder au sessionStorage // Éviter d'utiliser hasProperty qui peut causer des erreurs final result = js.context.callMethod('eval', [ ''' (function() { try { if (window.sessionStorage) { var value = sessionStorage.getItem('loginType'); return value; } return null; } catch (e) { console.error('Error accessing sessionStorage:', e); return null; } })() ''' ]); if (result != null && result is String && result.toLowerCase() == 'user') { setState(() { _loginType = 'user'; print( 'LoginPage: Type détecté depuis sessionStorage: $_loginType'); }); } } catch (e) { print('LoginPage: Erreur lors de l\'accès au sessionStorage: $e'); } }); } } catch (e) { print('Erreur lors de la récupération des paramètres d\'URL: $e'); } } // Les permissions sont maintenant vérifiées dans splash_page // Initialiser l'état de la connexion WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { setState(() { _isConnected = connectivityService.isConnected; }); } }); // Récupérer la version de l'application _getAppVersion(); // Vérification de connectivité au démarrage _checkConnectivity(); // Pré-remplir le champ username avec l'identifiant du dernier utilisateur connecté // seulement si le rôle correspond au type de login WidgetsBinding.instance.addPostFrameCallback((_) { final users = userRepository.getAllUsers(); if (users.isNotEmpty) { // Trouver l'utilisateur le plus récent (celui avec la date de dernière connexion la plus récente) users.sort((a, b) => (b.lastSyncedAt).compareTo(a.lastSyncedAt)); final lastUser = users.first; // Convertir le rôle en int si nécessaire int roleValue; if (lastUser.role is String) { roleValue = int.tryParse(lastUser.role as String) ?? 0; } else { roleValue = lastUser.role; } // Vérifier si le rôle correspond au type de login bool roleMatches = false; if (_loginType == 'user' && roleValue == 1) { roleMatches = true; debugPrint('Rôle utilisateur (1) correspond au type de login (user)'); } else if (_loginType == 'admin' && roleValue > 1) { roleMatches = true; debugPrint( 'Rôle administrateur ($roleValue) correspond au type de login (admin)'); } // Pré-remplir le champ username seulement si le rôle correspond if (roleMatches) { // Utiliser le username s'il existe, sinon utiliser l'email comme fallback if (lastUser.username != null && lastUser.username!.isNotEmpty) { _usernameController.text = lastUser.username!; // Déplacer le focus sur le champ mot de passe puisque le username est déjà rempli _usernameFocusNode.unfocus(); debugPrint('Champ username pré-rempli avec: ${lastUser.username}'); } else if (lastUser.email.isNotEmpty) { _usernameController.text = lastUser.email; _usernameFocusNode.unfocus(); debugPrint( 'Champ username pré-rempli avec email: ${lastUser.email}'); } } else { debugPrint( 'Le rôle ($roleValue) ne correspond pas au type de login ($_loginType), champ username non pré-rempli'); } } }); } @override void dispose() { _usernameController.dispose(); _passwordController.dispose(); _usernameFocusNode.dispose(); super.dispose(); } @override Widget build(BuildContext context) { print('DEBUG BUILD: Reconstruction de LoginPage avec type: $_loginType'); // Utiliser l'instance globale de userRepository final theme = Theme.of(context); final size = MediaQuery.of(context).size; // Les permissions sont maintenant gérées dans splash_page // On n'a plus besoin de ces vérifications ici return Scaffold( body: Stack( children: [ // Fond dégradé avec petits points blancs AnimatedContainer( duration: const Duration(milliseconds: 500), decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: _loginType == 'user' ? [Colors.white, Colors.green.shade300] : [Colors.white, Colors.red.shade300], ), ), child: CustomPaint( painter: DotsPainter(), child: const SizedBox( width: double.infinity, height: double.infinity), ), ), SafeArea( child: Center( child: SingleChildScrollView( padding: const EdgeInsets.all(24), child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 500), child: Card( elevation: 8, shadowColor: _loginType == 'user' ? Colors.green.withOpacity(0.5) : Colors.red.withOpacity(0.5), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16.0)), child: Padding( padding: const EdgeInsets.all(20.0), child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Logo simplifié avec chemin direct Image.asset( 'assets/images/logo-geosector-1024.png', height: 140, ), const SizedBox(height: 24), Text( _loginType == 'user' ? 'Connexion Utilisateur' : 'Connexion Administrateur', style: theme.textTheme.headlineMedium?.copyWith( fontWeight: FontWeight.bold, color: _loginType == 'user' ? Colors.green : Colors.red, ), textAlign: TextAlign.center, ), const SizedBox(height: 8), // Ajouter un texte de débogage uniquement en mode développement if (kDebugMode) Text( 'Type de connexion: $_loginType', style: const TextStyle( fontSize: 10, color: Colors.grey), textAlign: TextAlign.center, ), const SizedBox(height: 8), Text( 'Bienvenue sur GEOSECTOR', style: theme.textTheme.bodyLarge?.copyWith( color: theme.colorScheme.onSurface.withOpacity(0.7), ), textAlign: TextAlign.center, ), const SizedBox(height: 16), // Indicateur de connectivité const ConnectivityIndicator(), const SizedBox(height: 16), if (!kIsWeb && !_isConnected) Container( margin: const EdgeInsets.only(top: 16), padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: theme.colorScheme.error.withOpacity(0.1), borderRadius: BorderRadius.circular(12), border: Border.all( color: theme.colorScheme.error.withOpacity(0.3), ), ), child: Column( children: [ Icon(Icons.signal_wifi_off, color: theme.colorScheme.error, size: 32), const SizedBox(height: 8), Text('Connexion Internet requise', style: theme.textTheme.titleMedium ?.copyWith( fontWeight: FontWeight.bold, color: theme.colorScheme.error)), const SizedBox(height: 8), const Text( 'Veuillez vous connecter à Internet (WiFi ou données mobiles) pour pouvoir vous connecter.'), ], ), ), const SizedBox(height: 16), // Formulaire de connexion Form( key: _formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ CustomTextField( controller: _usernameController, label: 'Identifiant', hintText: 'Entrez votre identifiant', prefixIcon: Icons.person_outline, keyboardType: TextInputType.text, autofocus: true, focusNode: _usernameFocusNode, validator: (value) { if (value == null || value.isEmpty) { return 'Veuillez entrer votre identifiant'; } return null; }, ), const SizedBox(height: 16), CustomTextField( controller: _passwordController, label: 'Mot de passe', hintText: 'Entrez votre mot de passe', prefixIcon: Icons.lock_outline, obscureText: _obscurePassword, suffixIcon: IconButton( icon: Icon( _obscurePassword ? Icons.visibility_outlined : Icons.visibility_off_outlined, ), onPressed: () { setState(() { _obscurePassword = !_obscurePassword; }); }, ), validator: (value) { if (value == null || value.isEmpty) { return 'Veuillez entrer votre mot de passe'; } return null; }, onFieldSubmitted: (_) async { if (!userRepository.isLoading && _formKey.currentState!.validate()) { // Vérifier que le type de connexion est spécifié if (_loginType.isEmpty) { print( 'Login: Type non spécifié, redirection vers la page de démarrage'); context.go('/'); return; } print( 'Login: Tentative avec type: $_loginType'); final success = await userRepository.login( _usernameController.text.trim(), _passwordController.text, type: _loginType, ); if (success && mounted) { // Récupérer directement le rôle de l'utilisateur final user = userRepository.getCurrentUser(); if (user == null) { debugPrint( 'ERREUR: Utilisateur non trouvé après connexion réussie'); ScaffoldMessenger.of(context) .showSnackBar( const SnackBar( content: Text( 'Erreur de connexion. Veuillez réessayer.'), backgroundColor: Colors.red, ), ); return; } // Convertir le rôle en int si nécessaire int roleValue; if (user.role is String) { roleValue = int.tryParse( user.role as String) ?? 1; } else { roleValue = user.role; } debugPrint( 'Role de l\'utilisateur: $roleValue'); // Redirection simple basée sur le rôle if (roleValue > 1) { debugPrint( 'Redirection vers /admin (rôle > 1)'); context.go('/admin'); } else { debugPrint( 'Redirection vers /user (rôle = 1)'); context.go('/user'); } } else if (mounted) { ScaffoldMessenger.of(context) .showSnackBar( const SnackBar( content: Text( 'Échec de la connexion. Vérifiez vos identifiants.'), backgroundColor: Colors.red, ), ); } } }, ), const SizedBox(height: 8), // Mot de passe oublié Align( alignment: Alignment.centerRight, child: TextButton( onPressed: () { _showForgotPasswordDialog(context); }, child: Text( 'Mot de passe oublié ?', style: TextStyle( color: theme.colorScheme.primary, ), ), ), ), const SizedBox(height: 24), // Bouton de connexion CustomButton( onPressed: (userRepository.isLoading || !_isConnected) ? null : () async { if (_formKey.currentState! .validate()) { // Les permissions sont déjà vérifiées dans splash_page // Vérifier la connexion Internet await connectivityService .checkConnectivity(); if (!connectivityService .isConnected) { ScaffoldMessenger.of(context) .showSnackBar( SnackBar( content: const Text( 'Aucune connexion Internet. La connexion n\'est pas possible hors ligne.'), backgroundColor: theme.colorScheme.error, duration: const Duration( seconds: 3), action: SnackBarAction( label: 'Réessayer', onPressed: () async { await connectivityService .checkConnectivity(); if (connectivityService .isConnected && mounted) { ScaffoldMessenger.of( context) .showSnackBar( SnackBar( content: Text( 'Connexion Internet ${connectivityService.connectionType} détectée.'), backgroundColor: Colors.green, ), ); } }, ), ), ); return; } // Vérifier que le type de connexion est spécifié if (_loginType.isEmpty) { print( 'Login: Type non spécifié, redirection vers la page de démarrage'); context.go('/'); return; } print( 'Login: Tentative avec type: $_loginType'); // Utiliser directement userRepository avec l'overlay de chargement final success = await userRepository .loginWithUI( context, _usernameController.text.trim(), _passwordController.text, type: _loginType, ); if (success && mounted) { debugPrint( 'Connexion réussie, tentative de redirection...'); // Récupérer directement le rôle de l'utilisateur final user = userRepository .getCurrentUser(); if (user == null) { debugPrint( 'ERREUR: Utilisateur non trouvé après connexion réussie'); ScaffoldMessenger.of(context) .showSnackBar( const SnackBar( content: Text( 'Erreur de connexion. Veuillez réessayer.'), backgroundColor: Colors.red, ), ); return; } // Convertir le rôle en int si nécessaire int roleValue; if (user.role is String) { roleValue = int.tryParse( user.role as String) ?? 1; } else { roleValue = user.role; } debugPrint( 'Role de l\'utilisateur: $roleValue'); // Redirection simple basée sur le rôle if (roleValue > 1) { debugPrint( 'Redirection vers /admin (rôle > 1)'); context.go('/admin'); } else { debugPrint( 'Redirection vers /user (rôle = 1)'); context.go('/user'); } } else if (mounted) { ScaffoldMessenger.of(context) .showSnackBar( const SnackBar( content: Text( 'Échec de la connexion. Vérifiez vos identifiants.'), backgroundColor: Colors.red, ), ); } } }, text: _isConnected ? 'Se connecter' : 'Connexion Internet requise', isLoading: userRepository.isLoading, ), const SizedBox(height: 24), // Inscription administrateur uniquement Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( 'Pas encore de compte ?', style: theme.textTheme.bodyMedium, ), TextButton( onPressed: () { context.go('/register'); }, child: const Text( 'Inscription Administrateur', style: TextStyle( color: Colors.blue, fontWeight: FontWeight.bold, ), ), ), ], ), // Lien vers la page d'accueil TextButton( onPressed: () { context.go('/'); }, child: Text( 'Retour à l\'accueil', style: TextStyle( color: theme.colorScheme.secondary, ), ), ), ], ), ), ], ), ), ), ), ), ), ), // Badge de version en bas à droite if (_appVersion.isNotEmpty) Positioned( bottom: 16, right: 16, child: Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4, ), decoration: BoxDecoration( color: theme.colorScheme.primary.withOpacity(0.1), borderRadius: BorderRadius.circular(12), border: Border.all( color: theme.colorScheme.primary.withOpacity(0.3), width: 1, ), ), child: Text( 'v$_appVersion', style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.primary.withOpacity(0.8), fontSize: 10, fontWeight: FontWeight.w500, ), ), ), ), ], ), ); } // Affiche la boîte de dialogue pour la récupération de mot de passe void _showForgotPasswordDialog(BuildContext context) { final emailController = TextEditingController(); final formKey = GlobalKey(); bool isLoading = false; showDialog( context: context, barrierDismissible: false, builder: (BuildContext context) { return StatefulBuilder(builder: (context, setState) { return AlertDialog( title: const Row( children: [ Icon(Icons.lock_reset, color: Colors.blue), SizedBox(width: 10), Text('Récupération de mot de passe'), ], ), content: Form( key: formKey, child: Column( mainAxisSize: MainAxisSize.min, children: [ const Text( 'Veuillez entrer votre adresse email pour recevoir un nouveau mot de passe.', style: TextStyle(fontSize: 14), ), const SizedBox(height: 16), CustomTextField( controller: emailController, label: 'Email', hintText: 'Entrez votre email', prefixIcon: Icons.email_outlined, keyboardType: TextInputType.emailAddress, validator: (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; }, ), ], ), ), actions: [ TextButton( onPressed: () { Navigator.of(context).pop(); }, child: const Text('Annuler'), ), ElevatedButton( onPressed: isLoading ? null : () async { if (formKey.currentState!.validate()) { setState(() { isLoading = true; }); try { // Vérifier la connexion Internet await connectivityService.checkConnectivity(); if (!connectivityService.isConnected) { throw Exception('Aucune connexion Internet'); } // Construire l'URL de l'API final baseUrl = Uri.base.origin; final apiUrl = '$baseUrl/api/lostpassword'; print('Envoi de la requête à: $apiUrl'); print('Email: ${emailController.text.trim()}'); http.Response? response; try { // Envoyer la requête à l'API response = await http.post( Uri.parse(apiUrl), headers: {'Content-Type': 'application/json'}, body: json.encode({ 'email': emailController.text.trim(), }), ); print('Réponse reçue: ${response.statusCode}'); print('Corps de la réponse: ${response.body}'); // Si la réponse est 404, c'est peut-être un problème de route if (response.statusCode == 404) { // Essayer avec une URL alternative final alternativeUrl = '$baseUrl/api/index.php/lostpassword'; print( 'Tentative avec URL alternative: $alternativeUrl'); final alternativeResponse = await http.post( Uri.parse(alternativeUrl), headers: {'Content-Type': 'application/json'}, body: json.encode({ 'email': emailController.text.trim(), }), ); print( 'Réponse alternative reçue: ${alternativeResponse.statusCode}'); print( 'Corps de la réponse alternative: ${alternativeResponse.body}'); // Si la réponse alternative est un succès, utiliser cette réponse if (alternativeResponse.statusCode == 200) { response = alternativeResponse; } } } catch (e) { print( 'Erreur lors de l\'envoi de la requête: $e'); throw Exception('Erreur de connexion: $e'); } // Traiter la réponse if (response.statusCode == 200) { // Modifier le contenu de la boîte de dialogue pour afficher le message de succès setState(() { isLoading = false; }); // Remplacer le contenu de la boîte de dialogue par un message de succès showDialog( context: context, barrierDismissible: false, builder: (BuildContext context) { // Fermer automatiquement la boîte de dialogue après 2 secondes Future.delayed(const Duration(seconds: 2), () { if (Navigator.of(context).canPop()) { Navigator.of(context).pop(); } }); return const AlertDialog( content: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.check_circle, color: Colors.green, size: 48, ), SizedBox(height: 16), Text( 'Vous recevrez un nouveau mot de passe par email', textAlign: TextAlign.center, style: TextStyle(fontSize: 16), ), ], ), ); }, ); } else { // Fermer la boîte de dialogue actuelle Navigator.of(context).pop(); // Afficher un message d'erreur final responseData = json.decode(response.body); throw Exception(responseData['message'] ?? 'Erreur lors de la récupération du mot de passe'); } } catch (e) { // Afficher un message d'erreur ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(e .toString() .contains('Exception:') ? e.toString().split('Exception: ')[1] : 'Erreur lors de la récupération du mot de passe'), backgroundColor: Colors.red, ), ); } finally { if (mounted) { setState(() { isLoading = false; }); } } } }, style: ElevatedButton.styleFrom( backgroundColor: Colors.blue, foregroundColor: Colors.white, ), child: isLoading ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation(Colors.white), ), ) : const Text('Recevoir un nouveau mot de passe'), ), ], ); }); }, ); } }