import 'package:flutter/material.dart'; import 'dart:math' as math; import 'package:flutter/foundation.dart' show kIsWeb, kDebugMode; import 'dart:js' as js; import 'package:go_router/go_router.dart'; import 'package:go_router/src/state.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:geosector_app/core/repositories/user_repository.dart'; import 'package:geosector_app/presentation/widgets/custom_button.dart'; import 'package:geosector_app/presentation/widgets/custom_text_field.dart'; import 'package:geosector_app/core/services/location_service.dart'; import 'package:geosector_app/core/services/connectivity_service.dart'; import 'package:geosector_app/presentation/widgets/connectivity_indicator.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; // Type de connexion (utilisateur ou administrateur) late String _loginType; // État des permissions de géolocalisation bool _checkingPermission = true; bool _hasLocationPermission = false; String? _locationErrorMessage; // État de la connexion Internet bool _isConnected = false; @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'); } } // Vérifier les permissions de géolocalisation au démarrage seulement sur mobile if (!kIsWeb) { _checkLocationPermission(); } else { // En version web, on considère que les permissions sont accordées setState(() { _checkingPermission = false; _hasLocationPermission = true; }); } // Initialiser l'état de la connexion WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { setState(() { _isConnected = connectivityService.isConnected; }); } }); // 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 as int; } // 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'); } } }); } /// Vérifie les permissions de géolocalisation Future _checkLocationPermission() async { // Ne pas vérifier les permissions en version web if (kIsWeb) { setState(() { _hasLocationPermission = true; _checkingPermission = false; }); return; } setState(() { _checkingPermission = true; }); // Vérifier si les services de localisation sont activés et si l'application a la permission final hasPermission = await LocationService.checkAndRequestPermission(); final errorMessage = await LocationService.getLocationErrorMessage(); setState(() { _hasLocationPermission = hasPermission; _locationErrorMessage = errorMessage; _checkingPermission = false; }); } @override void dispose() { _usernameController.dispose(); _passwordController.dispose(); _usernameFocusNode.dispose(); super.dispose(); } /// Construit l'écran de chargement pendant la vérification des permissions Widget _buildLoadingScreen(ThemeData theme) { return Scaffold( body: SafeArea( child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ // Logo simlifié Image.asset( 'assets/images/logo-geosector-1024.png', height: 160, ), const SizedBox(height: 32), const CircularProgressIndicator(), const SizedBox(height: 24), Text( 'Vérification des permissions...', style: theme.textTheme.titleMedium, textAlign: TextAlign.center, ), ], ), ), ), ); } /// Construit l'écran de demande de permission de géolocalisation Widget _buildLocationPermissionScreen(ThemeData theme) { 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: Container(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é Image.asset( 'assets/images/logo-geosector-1024.png', height: 160, ), const SizedBox(height: 24), Text( 'Accès à la localisation requis', style: theme.textTheme.headlineMedium?.copyWith( fontWeight: FontWeight.bold, color: theme.colorScheme.primary, ), textAlign: TextAlign.center, ), const SizedBox(height: 24), // Message d'erreur Container( 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.location_disabled, color: theme.colorScheme.error, size: 48, ), const SizedBox(height: 16), Text( _locationErrorMessage ?? 'L\'accès à la localisation est nécessaire pour utiliser cette application.', style: theme.textTheme.bodyLarge, textAlign: TextAlign.center, ), const SizedBox(height: 16), Text( 'Cette application utilise votre position pour enregistrer les passages et assurer le suivi des secteurs géographiques. Sans cette permission, l\'application ne peut pas fonctionner correctement.', style: theme.textTheme.bodyMedium, textAlign: TextAlign.center, ), ], ), ), const SizedBox(height: 32), // Instructions pour activer la localisation Text( 'Comment activer la localisation :', style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, ), textAlign: TextAlign.center, ), const SizedBox(height: 16), _buildInstructionStep(theme, 1, 'Ouvrez les paramètres de votre appareil'), _buildInstructionStep(theme, 2, 'Accédez aux paramètres de confidentialité ou de localisation'), _buildInstructionStep(theme, 3, 'Recherchez GEOSECTOR dans la liste des applications'), _buildInstructionStep(theme, 4, 'Activez l\'accès à la localisation pour cette application'), const SizedBox(height: 32), // Boutons d'action CustomButton( onPressed: () async { // Ouvrir les paramètres de l'application await LocationService.openAppSettings(); }, text: 'Ouvrir les paramètres de l\'application', icon: Icons.settings, ), const SizedBox(height: 16), CustomButton( onPressed: () async { // Ouvrir les paramètres de localisation await LocationService.openLocationSettings(); }, text: 'Ouvrir les paramètres de localisation', icon: Icons.location_on, backgroundColor: theme.colorScheme.secondary, ), const SizedBox(height: 16), CustomButton( onPressed: () { // Vérifier à nouveau les permissions _checkLocationPermission(); }, text: 'Vérifier à nouveau', icon: Icons.refresh, backgroundColor: theme.colorScheme.tertiary, ), ], ), ), ), ), ), ), ), ], ), ); } /// Construit une étape d'instruction pour activer la localisation Widget _buildInstructionStep( ThemeData theme, int stepNumber, String instruction) { return Padding( padding: const EdgeInsets.only(bottom: 8.0), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( width: 24, height: 24, decoration: BoxDecoration( color: theme.colorScheme.primary, shape: BoxShape.circle, ), child: Center( child: Text( '$stepNumber', style: TextStyle( color: theme.colorScheme.onPrimary, fontWeight: FontWeight.bold, ), ), ), ), const SizedBox(width: 12), Expanded( child: Text( instruction, style: theme.textTheme.bodyMedium, ), ), ], ), ); } @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; // Afficher l'écran de permission de géolocalisation si l'utilisateur n'a pas accordé la permission (sauf en version web) if (!kIsWeb && _checkingPermission) { return _buildLoadingScreen(theme); } else if (!kIsWeb && !_hasLocationPermission) { return _buildLocationPermissionScreen(theme); } 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: Container(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, ), // Ajouter un texte de débogage uniquement en mode développement if (kDebugMode) Text( 'Type de connexion: $_loginType', style: 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.onBackground .withOpacity(0.7), ), textAlign: TextAlign.center, ), const SizedBox(height: 16), // Indicateur de connectivité ConnectivityIndicator(), 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 as int; } 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: () { // Naviguer vers la page de récupération de mot de passe }, 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()) { // Vérifier à nouveau les permissions de géolocalisation avant de se connecter (sauf en version web) if (!kIsWeb) { await _checkLocationPermission(); // Si l'utilisateur n'a toujours pas accordé les permissions, ne pas continuer if (!_hasLocationPermission) { ScaffoldMessenger.of(context) .showSnackBar( const SnackBar( content: Text( 'L\'accès à la localisation est nécessaire pour utiliser cette application.'), backgroundColor: Colors.red, ), ); return; } } // 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 as int; } 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: Text( 'Inscription Administrateur', style: TextStyle( color: theme.colorScheme.tertiary, fontWeight: FontWeight.bold, ), ), ), ], ), // Lien vers la page d'accueil TextButton( onPressed: () { context.go('/'); }, child: Text( 'Retour à l\'accueil', style: TextStyle( color: theme.colorScheme.secondary, ), ), ), ], ), ), ], ), ), ), ), ), ), ), ], ), ); } }