import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:geosector_app/core/services/app_info_service.dart'; import 'package:geosector_app/core/services/hive_service.dart'; import 'package:geosector_app/core/services/location_service.dart'; import 'package:geosector_app/core/constants/app_keys.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'dart:async'; import 'dart:math' as math; import 'package:flutter/foundation.dart' show kIsWeb; import 'package:url_launcher/url_launcher.dart'; // Import conditionnel pour le web import 'package:universal_html/html.dart' as html; // Import des repositories pour reset du cache import 'package:geosector_app/app.dart' show passageRepository, sectorRepository, membreRepository; // Import des services pour la gestion de session F5 import 'package:geosector_app/core/services/current_user_service.dart'; import 'package:geosector_app/core/services/api_service.dart'; import 'package:geosector_app/core/services/data_loading_service.dart'; class SplashPage extends StatefulWidget { /// Action à effectuer après l'initialisation (login ou register) final String? action; /// Type de login/register (user ou admin) - ignoré pour register final String? type; const SplashPage({super.key, this.action, this.type}); @override State createState() => _SplashPageState(); } // 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 _SplashPageState extends State with SingleTickerProviderStateMixin { late AnimationController _animationController; late Animation _scaleAnimation; bool _isInitializing = true; String _statusMessage = "Initialisation..."; double _progress = 0.0; bool _showButtons = false; String _appVersion = ''; bool _showLocationError = false; String? _locationErrorMessage; bool _isCleaningCache = false; Future _getAppVersion() async { // Utilise directement AppInfoService (remplace package_info_plus) if (mounted) { setState(() { _appVersion = AppInfoService.version; }); } } /// Effectue un nettoyage sélectif du cache /// Préserve la box pending_requests et les données critiques Future _performSelectiveCleanup({bool manual = false}) async { debugPrint('🧹 === DÉBUT DU NETTOYAGE DU CACHE === 🧹'); debugPrint('📌 Type: ${manual ? "MANUEL" : "AUTOMATIQUE"}'); debugPrint('📱 Platform: ${kIsWeb ? "WEB" : "MOBILE"}'); debugPrint('📦 Version actuelle: $_appVersion'); try { if (mounted) { setState(() { _isCleaningCache = true; _statusMessage = "Nettoyage du cache en cours..."; _progress = 0.1; }); } // Étape 1: Nettoyer le Service Worker (Web uniquement) if (kIsWeb) { debugPrint('🔄 Nettoyage du Service Worker...'); try { // Désenregistrer tous les service workers final registrations = await html.window.navigator.serviceWorker?.getRegistrations(); if (registrations != null) { for (final registration in registrations) { await registration.unregister(); debugPrint('✅ Service Worker désenregistré'); } } // Nettoyer les caches du navigateur if (html.window.caches != null) { final cacheNames = await html.window.caches!.keys(); for (final cacheName in cacheNames) { await html.window.caches!.delete(cacheName); debugPrint('✅ Cache "$cacheName" supprimé'); } } } catch (e) { debugPrint('⚠️ Erreur lors du nettoyage Service Worker: $e'); } } if (mounted) { setState(() { _statusMessage = "Fermeture des bases de données..."; _progress = 0.3; }); } // Étape 2: Sauvegarder les données critiques (pending_requests + app_version) debugPrint('💾 Sauvegarde des données critiques...'); List? pendingRequests; String? savedAppVersion; try { // Sauvegarder pending_requests if (Hive.isBoxOpen(AppKeys.pendingRequestsBoxName)) { final pendingBox = Hive.box(AppKeys.pendingRequestsBoxName); pendingRequests = pendingBox.values.toList(); debugPrint('📊 ${pendingRequests.length} requêtes en attente sauvegardées'); await pendingBox.close(); } // Sauvegarder app_version pour éviter de perdre l'info de version if (Hive.isBoxOpen(AppKeys.settingsBoxName)) { final settingsBox = Hive.box(AppKeys.settingsBoxName); savedAppVersion = settingsBox.get('app_version') as String?; if (savedAppVersion != null) { debugPrint('📦 Version sauvegardée: $savedAppVersion'); } } } catch (e) { debugPrint('⚠️ Erreur lors de la sauvegarde: $e'); } if (mounted) { setState(() { _statusMessage = "Nettoyage des données locales..."; _progress = 0.5; }); } // Étape 3: Lister toutes les boxes à nettoyer (SAUF pending_requests) final boxesToClean = [ AppKeys.userBoxName, AppKeys.operationsBoxName, AppKeys.passagesBoxName, AppKeys.sectorsBoxName, AppKeys.membresBoxName, AppKeys.amicaleBoxName, AppKeys.clientsBoxName, AppKeys.userSectorBoxName, AppKeys.settingsBoxName, AppKeys.chatRoomsBoxName, AppKeys.chatMessagesBoxName, ]; // Étape 4: Fermer et supprimer les boxes debugPrint('🗑️ Nettoyage des boxes Hive...'); for (final boxName in boxesToClean) { try { if (Hive.isBoxOpen(boxName)) { await Hive.box(boxName).close(); debugPrint('📦 Box "$boxName" fermée'); } await Hive.deleteBoxFromDisk(boxName); debugPrint('✅ Box "$boxName" supprimée'); } catch (e) { debugPrint('⚠️ Erreur lors du nettoyage de "$boxName": $e'); } } if (mounted) { setState(() { _statusMessage = "Réinitialisation de Hive..."; _progress = 0.7; }); } // Étape 5: Réinitialiser Hive proprement debugPrint('🔄 Réinitialisation de Hive...'); await Hive.close(); await Future.delayed(const Duration(milliseconds: 500)); await Hive.initFlutter(); // Étape 6: Restaurer les données critiques if (pendingRequests != null && pendingRequests.isNotEmpty) { debugPrint('♻️ Restauration des requêtes en attente...'); final pendingBox = await Hive.openBox(AppKeys.pendingRequestsBoxName); for (final request in pendingRequests) { await pendingBox.add(request); } debugPrint('✅ ${pendingRequests.length} requêtes restaurées'); } // Restaurer app_version pour maintenir la détection de changement de version if (savedAppVersion != null) { debugPrint('♻️ Restauration de la version...'); final settingsBox = await Hive.openBox(AppKeys.settingsBoxName); await settingsBox.put('app_version', savedAppVersion); debugPrint('✅ Version restaurée: $savedAppVersion'); } if (mounted) { setState(() { _statusMessage = "Nettoyage terminé !"; _progress = 1.0; }); } debugPrint('🎉 === NETTOYAGE TERMINÉ AVEC SUCCÈS === 🎉'); // Petit délai pour voir le message de succès await Future.delayed(const Duration(milliseconds: 500)); if (mounted) { setState(() { _isCleaningCache = false; _progress = 0.0; }); } } catch (e) { debugPrint('❌ ERREUR CRITIQUE lors du nettoyage: $e'); if (mounted) { setState(() { _isCleaningCache = false; _statusMessage = "Erreur lors du nettoyage"; _progress = 0.0; }); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Erreur lors du nettoyage: $e'), backgroundColor: Colors.red, duration: const Duration(seconds: 5), ), ); } } } /// Réinitialise le cache de tous les repositories après nettoyage complet void _resetAllRepositoriesCache() { try { debugPrint('🔄 === RESET DU CACHE DES REPOSITORIES === 🔄'); // Reset du cache des 3 repositories qui utilisent le pattern de cache passageRepository.resetCache(); sectorRepository.resetCache(); membreRepository.resetCache(); debugPrint('✅ Cache de tous les repositories réinitialisé'); } catch (e) { debugPrint('⚠️ Erreur lors du reset des caches: $e'); // Ne pas faire échouer le processus si le reset échoue } } /// Détecte et gère le refresh (F5) avec session existante /// Retourne true si une session a été restaurée, false sinon Future _handleSessionRefreshIfNeeded() async { if (!kIsWeb) { debugPrint('📱 Plateforme mobile - pas de gestion F5'); return false; } try { debugPrint('🔍 Vérification d\'une session existante (F5)...'); // Charger l'utilisateur depuis Hive await CurrentUserService.instance.loadFromHive(); final isLoggedIn = CurrentUserService.instance.isLoggedIn; final displayMode = CurrentUserService.instance.displayMode; final sessionId = CurrentUserService.instance.sessionId; if (!isLoggedIn || sessionId == null) { debugPrint('ℹ️ Aucune session active - affichage normal de la splash'); return false; } debugPrint('🔄 Session active détectée - mode: $displayMode'); debugPrint('🔄 Rechargement des données depuis l\'API...'); if (mounted) { setState(() { _statusMessage = "Restauration de votre session..."; _progress = 0.85; }); } // Configurer ApiService avec le sessionId existant ApiService.instance.setSessionId(sessionId); // Appeler le nouvel endpoint API pour restaurer la session // IMPORTANT: Utiliser getWithoutQueue() pour ne JAMAIS mettre cette requête en file d'attente // Les refresh de session sont liés à une session spécifique et ne doivent pas être rejoués final response = await ApiService.instance.getWithoutQueue( '/api/user/session', queryParameters: {'mode': displayMode}, ); // Gestion des codes de retour HTTP final statusCode = response.statusCode ?? 0; final data = response.data as Map?; switch (statusCode) { case 200: // Succès - traiter les données if (data == null || data['success'] != true) { debugPrint('❌ Format de réponse invalide (200 mais pas success=true)'); await CurrentUserService.instance.clearUser(); return false; } debugPrint('✅ Données reçues de l\'API, traitement...'); if (mounted) { setState(() { _statusMessage = "Chargement de vos données..."; _progress = 0.90; }); } // Traiter les données avec DataLoadingService final apiData = data['data'] as Map?; if (apiData == null) { debugPrint('❌ Données manquantes dans la réponse'); await CurrentUserService.instance.clearUser(); return false; } await DataLoadingService.instance.processLoginData(apiData); debugPrint('✅ Session restaurée avec succès'); break; case 400: // Paramètre mode invalide - erreur technique debugPrint('❌ Paramètre mode invalide: $displayMode'); await CurrentUserService.instance.clearUser(); if (mounted) { setState(() { _statusMessage = "Erreur technique - veuillez vous reconnecter"; }); } return false; case 401: // Session invalide ou expirée debugPrint('⚠️ Session invalide ou expirée'); await CurrentUserService.instance.clearUser(); if (mounted) { setState(() { _statusMessage = "Session expirée - veuillez vous reconnecter"; }); } return false; case 403: // Accès interdit (membre → admin) ou entité inactive final message = data?['message'] ?? 'Accès interdit'; debugPrint('🚫 Accès interdit: $message'); await CurrentUserService.instance.clearUser(); if (mounted) { setState(() { _statusMessage = "Accès interdit - veuillez vous reconnecter"; }); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(message), backgroundColor: Colors.orange, duration: const Duration(seconds: 5), ), ); } return false; case 500: // Erreur serveur final message = data?['message'] ?? 'Erreur serveur'; debugPrint('❌ Erreur serveur: $message'); if (mounted) { setState(() { _statusMessage = "Erreur serveur - veuillez réessayer"; }); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Erreur serveur: $message'), backgroundColor: Colors.red, duration: const Duration(seconds: 5), ), ); } // Ne pas effacer la session en cas d'erreur serveur return false; default: // Code de retour inattendu debugPrint('❌ Code HTTP inattendu: $statusCode'); await CurrentUserService.instance.clearUser(); return false; } if (mounted) { setState(() { _statusMessage = "Session restaurée !"; _progress = 0.95; }); } // Petit délai pour voir le message await Future.delayed(const Duration(milliseconds: 500)); // Rediriger vers la bonne interface selon le mode if (!mounted) return true; if (displayMode == 'admin') { debugPrint('🔀 Redirection vers interface admin'); context.go('/admin/home'); } else { debugPrint('🔀 Redirection vers interface user'); context.go('/user/field-mode'); } return true; } catch (e) { debugPrint('❌ Erreur lors de la restauration de session: $e'); // En cas d'erreur, effacer la session invalide await CurrentUserService.instance.clearUser(); if (mounted) { setState(() { _statusMessage = "Erreur de restauration - veuillez vous reconnecter"; _progress = 0.0; }); } return false; } } /// Vérifie si une nouvelle version est disponible et nettoie si nécessaire Future _checkVersionAndCleanIfNeeded() async { if (!kIsWeb) { debugPrint('📱 Plateforme mobile - pas de nettoyage automatique'); return; } try { String lastVersion = ''; // Lire la version depuis Hive settings if (Hive.isBoxOpen(AppKeys.settingsBoxName)) { final settingsBox = Hive.box(AppKeys.settingsBoxName); lastVersion = settingsBox.get('app_version', defaultValue: '') as String; } debugPrint('🔍 Vérification de version:'); debugPrint(' Version stockée: $lastVersion'); debugPrint(' Version actuelle: $_appVersion'); // Si changement de version détecté if (lastVersion.isNotEmpty && lastVersion != _appVersion) { debugPrint('🆕 NOUVELLE VERSION DÉTECTÉE !'); debugPrint(' Migration de $lastVersion vers $_appVersion'); if (mounted) { setState(() { _statusMessage = "Nouvelle version détectée, mise à jour..."; }); } // Effectuer le nettoyage automatique await _performSelectiveCleanup(manual: false); // Reset du cache des repositories après nettoyage _resetAllRepositoriesCache(); } else if (lastVersion.isEmpty) { // Première installation debugPrint('🎉 Première installation détectée'); if (Hive.isBoxOpen(AppKeys.settingsBoxName)) { final settingsBox = Hive.box(AppKeys.settingsBoxName); await settingsBox.put('app_version', _appVersion); debugPrint('💾 Version initiale sauvegardée dans Hive: $_appVersion'); } } else { debugPrint('✅ Même version - pas de nettoyage nécessaire'); } } catch (e) { debugPrint('⚠️ Erreur lors de la vérification de version: $e'); } } @override void initState() { super.initState(); // Animation controller _animationController = AnimationController( vsync: this, duration: const Duration(seconds: 5), ); _scaleAnimation = Tween( begin: 4.0, end: 1.0, ).animate( CurvedAnimation( parent: _animationController, curve: Curves.easeOutBack, ), ); _animationController.forward(); _getAppVersion(); _startInitialization(); } @override void dispose() { _animationController.dispose(); super.dispose(); } void _startInitialization() async { try { debugPrint('🚀 Début de l\'initialisation complète de l\'application...'); // Étape 1: Vérification des permissions GPS (obligatoire) - 0 à 10% if (!kIsWeb) { if (mounted) { setState(() { _statusMessage = "Vérification des autorisations GPS..."; _progress = 0.05; }); } await Future.delayed(const Duration(milliseconds: 200)); final hasPermission = await LocationService.checkAndRequestPermission(); final errorMessage = await LocationService.getLocationErrorMessage(); if (!hasPermission) { // Si les permissions ne sont pas accordées, on arrête tout debugPrint('❌ Permissions GPS refusées'); if (mounted) { setState(() { _showLocationError = true; _locationErrorMessage = errorMessage ?? "L'application nécessite l'accès à votre position pour fonctionner correctement."; _isInitializing = false; _progress = 0.0; }); } return; // On arrête l'initialisation ici } if (mounted) { setState(() { _statusMessage = "Autorisations GPS accordées..."; _progress = 0.10; }); } } // Étape 1: Préparation - 10 à 15% if (mounted) { setState(() { _statusMessage = "Démarrage de l'application..."; _progress = 0.12; }); } await Future.delayed(const Duration(milliseconds: 200)); // Petit délai pour voir le début if (mounted) { setState(() { _statusMessage = "Chargement des composants..."; _progress = 0.15; }); } // Étape 2: Initialisation Hive - 15 à 60% (étape la plus longue) await HiveService.instance.initializeAndResetHive(); if (mounted) { setState(() { _statusMessage = "Configuration du stockage..."; _progress = 0.45; }); } await Future.delayed(const Duration(milliseconds: 300)); // Simulation du temps de traitement if (mounted) { setState(() { _statusMessage = "Préparation des données..."; _progress = 0.60; }); } // Étape 3: Ouverture des Box - 60 à 80% await HiveService.instance.ensureBoxesAreOpen(); // NOUVEAU : Vérifier et nettoyer si nouvelle version (Web uniquement) // Maintenant que les boxes sont ouvertes, on peut vérifier la version dans Hive await _checkVersionAndCleanIfNeeded(); // NOUVEAU : Détecter et gérer le F5 (refresh de page web avec session existante) final sessionRestored = await _handleSessionRefreshIfNeeded(); if (sessionRestored) { // Session restaurée avec succès, on arrête ici // L'utilisateur a été redirigé vers son interface debugPrint('✅ Session restaurée via F5 - fin de l\'initialisation'); return; } // Gérer la box pending_requests séparément pour préserver les données try { debugPrint('📦 Gestion de la box pending_requests...'); if (!Hive.isBoxOpen(AppKeys.pendingRequestsBoxName)) { // Importer PendingRequest si nécessaire final pendingRequestBox = await Hive.openBox(AppKeys.pendingRequestsBoxName); final pendingCount = pendingRequestBox.length; if (pendingCount > 0) { debugPrint('⏳ $pendingCount requêtes en attente trouvées dans la box'); } else { debugPrint('✅ Box pending_requests ouverte (vide)'); } } else { debugPrint('✅ Box pending_requests déjà ouverte'); } } catch (e) { debugPrint('⚠️ Erreur lors de l\'ouverture de la box pending_requests: $e'); // On continue quand même, ce n'est pas critique pour le démarrage } if (mounted) { setState(() { _statusMessage = "Vérification du système..."; _progress = 0.80; }); } // Étape 4: Vérification finale - 80 à 95% final allBoxesOpen = HiveService.instance.areAllBoxesOpen(); if (!allBoxesOpen) { final diagnostic = HiveService.instance.getDiagnostic(); debugPrint('❌ Diagnostic des Box: $diagnostic'); throw Exception('Une erreur est survenue lors de l\'initialisation'); } if (mounted) { setState(() { _statusMessage = "Finalisation du chargement..."; _progress = 0.95; }); } await Future.delayed(const Duration(milliseconds: 300)); // Petit délai pour finaliser // Étape 5: Finalisation - 95 à 100% if (mounted) { setState(() { _statusMessage = "Application prête !"; _progress = 1.0; }); // Marquer dans settings que l'initialisation complète de Hive a été effectuée try { if (Hive.isBoxOpen(AppKeys.settingsBoxName)) { final settingsBox = Hive.box(AppKeys.settingsBoxName); await settingsBox.put('hive_initialized', true); await settingsBox.put('hive_initialized_at', DateTime.now().toIso8601String()); debugPrint('✅ Clé hive_initialized définie à true dans settings'); } } catch (e) { debugPrint('⚠️ Impossible de définir la clé hive_initialized: $e'); } // Attendre un court instant pour que l'utilisateur voie "Application prête !" await Future.delayed(const Duration(milliseconds: 400)); setState(() { _isInitializing = false; }); // Redirection automatique si des paramètres sont fournis if (widget.action != null) { await _handleAutoRedirect(); } else { setState(() { _showButtons = true; }); } } debugPrint('✅ Initialisation complète de l\'application terminée avec succès'); } catch (e) { debugPrint('❌ Erreur lors de l\'initialisation: $e'); if (mounted) { setState(() { _statusMessage = "Erreur de chargement - Veuillez redémarrer l'application"; _progress = 1.0; _isInitializing = false; _showButtons = true; }); } } } /// Gère la redirection automatique après l'initialisation Future _handleAutoRedirect() async { // Petit délai pour voir le message "Application prête !" await Future.delayed(const Duration(milliseconds: 300)); if (!mounted) return; final action = widget.action?.toLowerCase(); final type = widget.type?.toLowerCase(); debugPrint('🔄 Redirection automatique: action=$action, type=$type'); // Afficher un message de redirection avant de naviguer setState(() { _statusMessage = action == 'login' ? "Redirection vers la connexion..." : action == 'register' ? "Redirection vers l'inscription..." : "Redirection..."; }); await Future.delayed(const Duration(milliseconds: 200)); if (!context.mounted) return; switch (action) { case 'login': if (type == 'admin') { context.go('/login/admin'); } else { // Par défaut, rediriger vers user si type non spécifié ou invalid context.go('/login/user'); } break; case 'register': // Pour register, le type n'est pas pris en compte context.go('/register'); break; default: // Si action non reconnue, afficher les boutons normalement setState(() { _showButtons = true; }); break; } } @override Widget build(BuildContext context) { final theme = Theme.of(context); 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: [Colors.white, Colors.blue.shade300], ), ), child: CustomPaint( painter: DotsPainter(), child: const SizedBox(width: double.infinity, height: double.infinity), ), ), // Contenu principal SafeArea( child: Center( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 40), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Spacer(flex: 2), // Logo avec animation AnimatedBuilder( animation: _scaleAnimation, builder: (context, child) { return Transform.scale( scale: _scaleAnimation.value, child: child, ); }, child: Image.asset( 'assets/images/logo-geosector-1024.png', height: 180, ), ), const SizedBox(height: 24), // Titre AnimatedOpacity( opacity: _isInitializing ? 0.9 : 1.0, duration: const Duration(milliseconds: 500), child: Text( 'Geosector', style: theme.textTheme.headlineLarge?.copyWith( color: theme.colorScheme.primary, fontWeight: FontWeight.bold, letterSpacing: 1.2, ), ), ), const SizedBox(height: 16), // Sous-titre AnimatedOpacity( opacity: _isInitializing ? 0.8 : 1.0, duration: const Duration(milliseconds: 500), child: Text( 'Une application puissante et intuitive de gestion de vos distributions de calendriers', textAlign: TextAlign.center, style: theme.textTheme.bodyLarge?.copyWith( color: theme.colorScheme.onSurface.withOpacity(0.7), fontWeight: FontWeight.w500, ), ), ), const Spacer(flex: 1), // Indicateur de chargement if ((_isInitializing || _isCleaningCache) && !_showLocationError) ...[ Padding( padding: const EdgeInsets.symmetric(horizontal: 40), child: Column( children: [ // Barre de progression avec animation Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(10), boxShadow: [ BoxShadow( color: theme.colorScheme.primary.withOpacity(0.2), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: ClipRRect( borderRadius: BorderRadius.circular(10), child: TweenAnimationBuilder( duration: const Duration(milliseconds: 800), curve: Curves.easeInOut, tween: Tween(begin: 0.0, end: _progress), builder: (context, value, child) { return LinearProgressIndicator( value: value, backgroundColor: Colors.grey.withOpacity(0.15), valueColor: AlwaysStoppedAnimation( theme.colorScheme.primary, ), minHeight: 12, ); }, ), ), ), const SizedBox(height: 8), // Pourcentage Text( '${(_progress * 100).round()}%', style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.primary, fontWeight: FontWeight.w600, ), ), ], ), ), const SizedBox(height: 16), // Message de statut avec animation AnimatedSwitcher( duration: const Duration(milliseconds: 300), child: Text( _statusMessage, key: ValueKey(_statusMessage), style: theme.textTheme.bodyMedium?.copyWith( color: theme.colorScheme.onSurface.withOpacity(0.7), fontWeight: FontWeight.w500, ), textAlign: TextAlign.center, ), ), ], // Erreur de localisation if (_showLocationError) ...[ Container( padding: const EdgeInsets.all(16), margin: const EdgeInsets.symmetric(horizontal: 20), decoration: BoxDecoration( color: Colors.red.shade50, borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.red.shade200), ), child: Column( children: [ Icon( Icons.location_off, size: 48, color: Colors.red.shade700, ), const SizedBox(height: 16), Text( 'Autorisations GPS requises', style: theme.textTheme.titleLarge?.copyWith( color: Colors.red.shade700, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 8), Text( _locationErrorMessage ?? "L'application nécessite l'accès à votre position pour fonctionner correctement.", textAlign: TextAlign.center, style: theme.textTheme.bodyMedium?.copyWith( color: Colors.red.shade700, ), ), const SizedBox(height: 24), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ // Bouton Réessayer ElevatedButton.icon( onPressed: () { setState(() { _showLocationError = false; _isInitializing = true; }); _startInitialization(); }, icon: const Icon(Icons.refresh), label: const Text('Réessayer'), style: ElevatedButton.styleFrom( backgroundColor: Colors.green, foregroundColor: Colors.white, ), ), const SizedBox(width: 16), // Bouton Paramètres OutlinedButton.icon( onPressed: () async { if (_locationErrorMessage?.contains('définitivement') ?? false) { await LocationService.openAppSettings(); } else { await LocationService.openLocationSettings(); } }, icon: const Icon(Icons.settings), label: const Text('Paramètres'), style: OutlinedButton.styleFrom( foregroundColor: Colors.red.shade700, side: BorderSide(color: Colors.red.shade700), ), ), ], ), ], ), ), ], // Boutons (reste identique) if (_showButtons) ...[ // Bouton Connexion Utilisateur AnimatedOpacity( opacity: _showButtons ? 1.0 : 0.0, duration: const Duration(milliseconds: 500), child: ElevatedButton( onPressed: () { context.go('/login/user'); }, style: ElevatedButton.styleFrom( backgroundColor: Colors.green, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric( horizontal: 40, vertical: 16, ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(30), ), elevation: 2, ), child: const Text( 'Connexion Utilisateur', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), ), ), const SizedBox(height: 16), // Bouton Connexion Administrateur AnimatedOpacity( opacity: _showButtons ? 1.0 : 0.0, duration: const Duration(milliseconds: 500), child: ElevatedButton( onPressed: () { context.go('/login/admin'); }, style: ElevatedButton.styleFrom( backgroundColor: Colors.red, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric( horizontal: 40, vertical: 16, ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(30), ), elevation: 2, ), child: const Text( 'Connexion Administrateur', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), ), ), const SizedBox(height: 32), // Bouton d'inscription AnimatedOpacity( opacity: _showButtons ? 1.0 : 0.0, duration: const Duration(milliseconds: 500), child: ElevatedButton( onPressed: () { context.go('/register'); }, style: ElevatedButton.styleFrom( backgroundColor: Colors.blue, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric( horizontal: 40, vertical: 16, ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(30), ), elevation: 2, ), child: const Text( 'Pas encore inscrit ?', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ), ), ), ), const SizedBox(height: 16), // Lien vers le site web AnimatedOpacity( opacity: _showButtons ? 1.0 : 0.0, duration: const Duration(milliseconds: 500), child: TextButton.icon( onPressed: () { String webUrl = 'https://geosector.fr'; if (kIsWeb) { final host = Uri.base.host; if (host.startsWith('dapp.')) { webUrl = 'https://dev.geosector.fr'; } else if (host.startsWith('rapp.')) { webUrl = 'https://rec.geosector.fr'; } else if (host.startsWith('app.')) { webUrl = 'https://geosector.fr'; } } launchUrl( Uri.parse(webUrl), mode: LaunchMode.externalApplication, ); }, icon: Icon( Icons.language, size: 18, color: theme.colorScheme.primary, ), label: Text( 'Site web Geosector', style: TextStyle( color: theme.colorScheme.primary, fontWeight: FontWeight.w500, ), ), ), ), const SizedBox(height: 8), // Bouton de nettoyage du cache (Web uniquement) if (kIsWeb) AnimatedOpacity( opacity: _showButtons ? 1.0 : 0.0, duration: const Duration(milliseconds: 500), child: TextButton.icon( onPressed: _isCleaningCache ? null : () async { // Confirmation avant nettoyage final confirm = await showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Nettoyer le cache ?'), content: const Text( 'Cette action va :\n' '• Supprimer toutes les données locales\n' '• Préserver les requêtes en attente\n' '• Forcer le rechargement de l\'application\n\n' 'Continuer ?' ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(false), child: const Text('Annuler'), ), ElevatedButton( onPressed: () => Navigator.of(context).pop(true), style: ElevatedButton.styleFrom( backgroundColor: Colors.orange, ), child: const Text('Nettoyer'), ), ], ), ); if (confirm == true) { debugPrint('👤 Utilisateur a demandé un nettoyage manuel'); await _performSelectiveCleanup(manual: true); // Reset du cache des repositories après nettoyage _resetAllRepositoriesCache(); // Forcer le rechargement complet de la page if (kIsWeb) { html.window.location.reload(); } else { // Sur mobile, relancer l'initialisation normalement _startInitialization(); } } }, icon: Icon( Icons.cleaning_services, size: 18, color: _isCleaningCache ? Colors.grey : Colors.black87, ), label: Text( _isCleaningCache ? 'Nettoyage...' : 'Nettoyer le cache', style: TextStyle( color: _isCleaningCache ? Colors.grey : Colors.black87, fontWeight: FontWeight.w500, ), ), ), ), ], const Spacer(flex: 1), ], ), ), ), ), // Badge de version if (_appVersion.isNotEmpty) Positioned( bottom: 16, right: 16, child: AnimatedOpacity( opacity: _showButtons ? 0.7 : 0.5, duration: const Duration(milliseconds: 500), 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, width: 1, ), ), child: Text( 'v$_appVersion', style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.primary, fontSize: 10, fontWeight: FontWeight.w500, ), ), ), ), ), ], ), ); } }