import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter/foundation.dart' show kIsWeb; import 'package:go_router/go_router.dart'; import 'dart:math' as math; import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:hive_flutter/hive_flutter.dart'; import 'package:url_launcher/url_launcher.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:geosector_app/core/services/app_info_service.dart'; import 'package:geosector_app/core/services/hive_service.dart'; import 'package:geosector_app/core/constants/app_keys.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales class RegisterPage extends StatefulWidget { const RegisterPage({super.key}); @override State createState() => _RegisterPageState(); } // 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.withValues(alpha: 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; } // Modèle pour les villes class City { final String name; final String postalCode; City({required this.name, required this.postalCode}); factory City.fromJson(Map json) { return City( name: json['nom'] ?? '', postalCode: json['codePostal'] ?? '', ); } } class _RegisterPageState extends State { final _formKey = GlobalKey(); final _nameController = TextEditingController(); final _amicaleNameController = TextEditingController(); final _postalCodeController = TextEditingController(); final _emailController = TextEditingController(); final _captchaController = TextEditingController(); String _appVersion = ''; // Valeur cachée pour le test anti-robot final String _hiddenToken = DateTime.now().millisecondsSinceEpoch.toString(); // Valeurs pour le captcha simple final int _captchaNum1 = 2 + (DateTime.now().second % 5); // Nombre entre 2 et 6 final int _captchaNum2 = 3 + (DateTime.now().minute % 4); // Nombre entre 3 et 6 // État de la connexion Internet et de la plateforme bool _isConnected = false; bool _isMobile = false; String _connectionType = ''; bool _isLoading = false; // État de chargement local // Liste des villes correspondant au code postal List _cities = []; City? _selectedCity; bool _isLoadingCities = 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 }); } } } @override void initState() { super.initState(); // VÉRIFICATION CRITIQUE : S'assurer que Hive est initialisé correctement // Vérifier la clé 'hive_initialized' dans la box settings try { // D'abord vérifier que les boxes sont disponibles if (!HiveService.instance.areBoxesInitialized()) { debugPrint('⚠️ RegisterPage: Boxes Hive non initialisées, redirection vers SplashPage'); WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { context.go('/?action=register'); } }); return; // IMPORTANT : Arrêter l'exécution du reste de initState } // Ensuite vérifier la clé de réinitialisation if (Hive.isBoxOpen(AppKeys.settingsBoxName)) { final settingsBox = Hive.box(AppKeys.settingsBoxName); final isInitialized = settingsBox.get('hive_initialized', defaultValue: false); if (isInitialized != true) { debugPrint('⚠️ RegisterPage: Réinitialisation Hive requise (hive_initialized=$isInitialized)'); // Forcer une réinitialisation complète via SplashPage WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { context.go('/?action=register'); } }); return; // IMPORTANT : Arrêter l'exécution du reste de initState } debugPrint('✅ RegisterPage: Hive correctement initialisé'); } } catch (e) { debugPrint('❌ RegisterPage: Erreur lors de la vérification de hive_initialized: $e'); // En cas d'erreur, forcer la réinitialisation WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { context.go('/?action=register'); } }); return; } // Déterminer si l'application s'exécute sur mobile _isMobile = !kIsWeb; // Initialiser l'état de la connexion _checkConnectivity(); // Récupérer la version de l'application _getAppVersion(); // Écouter les changements du code postal _postalCodeController.addListener(_onPostalCodeChanged); } // Méthode pour vérifier la connectivité Future _checkConnectivity() async { await connectivityService.checkConnectivity(); if (mounted) { setState(() { _isConnected = connectivityService.isConnected; _connectionType = connectivityService.connectionType; }); } } // Méthode appelée lorsque le code postal change void _onPostalCodeChanged() { final postalCode = _postalCodeController.text; // Réinitialiser la ville sélectionnée si le code postal change setState(() { _selectedCity = null; }); // Si le code postal a au moins 3 chiffres, rechercher les villes correspondantes if (postalCode.length >= 3) { _fetchCities(postalCode); } else { setState(() { _cities = []; }); } } // Méthode pour récupérer les villes correspondant au code postal Future _fetchCities(String postalCode) async { if (!_isConnected) return; setState(() { _isLoadingCities = true; }); try { // Utiliser l'API interne de geosector pour récupérer les villes par code postal final baseUrl = Uri .base.origin; // Récupère l'URL de base (ex: https://app.geosector.fr) final apiUrl = '$baseUrl/api/villes?code_postal=$postalCode'; final response = await http.get( Uri.parse(apiUrl), headers: {'Content-Type': 'application/json'}, ); if (response.statusCode == 200) { final Map responseData = json.decode(response.body); // Vérifier si la réponse contient des données if (responseData['success'] == true && responseData['data'] != null) { final List data = responseData['data']; setState(() { _cities = data .map((city) => City( name: city['nom'] ?? '', postalCode: city['code_postal'] ?? postalCode, )) .toList(); _isLoadingCities = false; // Ne pas présélectionner automatiquement la première ville // Laisser l'utilisateur saisir son code postal complet et choisir une ville _selectedCity = null; }); } else { setState(() { _cities = []; _isLoadingCities = false; }); } } else { setState(() { _cities = []; _isLoadingCities = false; }); } } catch (e) { print('Erreur lors de la récupération des villes: $e'); setState(() { _cities = []; _isLoadingCities = false; }); } } @override void dispose() { _nameController.dispose(); _amicaleNameController.dispose(); _postalCodeController.removeListener(_onPostalCodeChanged); _postalCodeController.dispose(); _emailController.dispose(); _captchaController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { // Utiliser l'instance globale de userRepository définie dans app.dart 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), ), ), SafeArea( child: Center( child: SingleChildScrollView( padding: const EdgeInsets.all(24), child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 500), child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Logo et titre Image.asset( 'assets/images/logo-geosector-1024.png', height: 140, ), const SizedBox(height: 16), Text( 'Inscription Administrateur', style: theme.textTheme.headlineMedium?.copyWith( fontWeight: FontWeight.bold, color: theme.colorScheme.primary, ), textAlign: TextAlign.center, ), const SizedBox(height: 8), Text( 'Enregistrez votre amicale sur GeoSector', style: theme.textTheme.bodyLarge?.copyWith( color: theme.colorScheme.onSurface.withValues(alpha: 0.7), ), textAlign: TextAlign.center, ), const SizedBox(height: 16), // Indicateur de connectivité ConnectivityIndicator( onConnectivityChanged: (isConnected) { if (mounted && _isConnected != isConnected) { setState(() { _isConnected = isConnected; _connectionType = connectivityService.connectionType; }); } }, ), // Message d'avertissement pour les utilisateurs mobiles sans connexion if (_isMobile && !_isConnected) Container( margin: const EdgeInsets.only(top: 16), padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: theme.colorScheme.error.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), border: Border.all( color: theme.colorScheme.error.withValues(alpha: 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, ), textAlign: TextAlign.center, ), const SizedBox(height: 8), Text( 'Veuillez vous connecter à Internet (WiFi ou données mobiles) pour pouvoir vous inscrire.', style: theme.textTheme.bodyMedium, textAlign: TextAlign.center, ), const SizedBox(height: 8), ElevatedButton.icon( onPressed: () async { await _checkConnectivity(); if (_isConnected && context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( 'Connexion Internet $_connectionType détectée.'), backgroundColor: Colors.green, ), ); } }, icon: const Icon(Icons.refresh), label: const Text('Vérifier à nouveau'), style: ElevatedButton.styleFrom( backgroundColor: theme.colorScheme.primary, foregroundColor: theme.colorScheme.onPrimary, ), ), ], ), ), const SizedBox(height: 16), // Formulaire d'inscription Form( key: _formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ CustomTextField( controller: _nameController, label: 'Nom complet', hintText: 'Entrez votre nom complet', prefixIcon: Icons.person_outline, isRequired: true, validator: (value) { if (value == null || value.isEmpty) { return 'Veuillez entrer votre nom complet'; } if (value.length < 5) { return 'Le nom doit contenir au moins 5 caractères'; } return null; }, ), const SizedBox(height: 16), CustomTextField( controller: _emailController, label: 'Email', hintText: 'Entrez votre email', prefixIcon: Icons.email_outlined, keyboardType: TextInputType.emailAddress, isRequired: true, 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; }, ), const SizedBox(height: 16), CustomTextField( controller: _amicaleNameController, label: 'Nom de l\'amicale', hintText: 'Entrez le nom de votre amicale', prefixIcon: Icons.local_fire_department, isRequired: true, validator: (value) { if (value == null || value.isEmpty) { return 'Veuillez entrer le nom de votre amicale'; } if (value.length < 5) { return 'Le nom de l\'amicale doit contenir au moins 5 caractères'; } return null; }, ), const SizedBox(height: 16), CustomTextField( controller: _postalCodeController, label: 'Code postal de l\'amicale', hintText: 'Entrez le code postal de votre amicale', prefixIcon: Icons.location_on_outlined, keyboardType: TextInputType.number, isRequired: true, inputFormatters: [ FilteringTextInputFormatter.digitsOnly, LengthLimitingTextInputFormatter(5), ], validator: (value) { if (value == null || value.isEmpty) { return 'Veuillez entrer votre code postal'; } if (!RegExp(r'^[0-9]{5}$').hasMatch(value)) { return 'Le code postal doit contenir 5 chiffres'; } return null; }, ), const SizedBox(height: 16), // Sélection de la commune Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text( 'Commune de l\'amicale', style: theme.textTheme.titleSmall?.copyWith( fontWeight: FontWeight.w500, color: theme.colorScheme.onSurface, ), ), const Text( ' •', style: TextStyle( color: Colors.red, fontWeight: FontWeight.bold, fontSize: 16, ), ), ], ), const SizedBox(height: 8), Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), color: const Color(0xFFECEFF1), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.05), blurRadius: 4, offset: const Offset(0, 2), ), ], ), child: _isLoadingCities ? const Padding( padding: EdgeInsets.symmetric( vertical: 16), child: Center( child: CircularProgressIndicator(), ), ) : DropdownButtonFormField( initialValue: _selectedCity, decoration: InputDecoration( prefixIcon: Icon( Icons.location_city_outlined, color: theme.colorScheme.primary, ), hintText: _postalCodeController .text.length < 3 ? 'Entrez d\'abord au moins 3 chiffres du code postal' : _cities.isEmpty ? 'Aucune commune trouvée pour ce code postal' : 'Sélectionnez une commune', border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none, ), contentPadding: const EdgeInsets.symmetric( horizontal: 16, vertical: 16, ), ), items: _cities.map((City city) { return DropdownMenuItem( value: city, child: Text(city.name), ); }).toList(), onChanged: (City? newValue) { setState(() { _selectedCity = newValue; // Mettre à jour le code postal avec celui de la ville sélectionnée if (newValue != null) { // Désactiver temporairement le listener pour éviter une boucle infinie _postalCodeController .removeListener( _onPostalCodeChanged); // Mettre à jour le code postal _postalCodeController.text = newValue.postalCode; // Réactiver le listener _postalCodeController .addListener( _onPostalCodeChanged); } }); }, validator: (value) { if (value == null) { return 'Veuillez sélectionner une commune'; } return null; }, isExpanded: true, icon: Icon( Icons.arrow_drop_down, color: theme.colorScheme.primary, ), dropdownColor: Colors.white, ), ), ], ), const SizedBox(height: 16), // Test anti-robot (captcha simple) const SizedBox(height: 24), Text( 'Vérification de sécurité', style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w500, color: theme.colorScheme.primary, ), textAlign: TextAlign.center, ), const SizedBox(height: 8), CustomTextField( controller: _captchaController, label: 'Combien font $_captchaNum1 + $_captchaNum2 ?', hintText: 'Entrez le résultat', prefixIcon: Icons.security, keyboardType: TextInputType.number, isRequired: true, validator: (value) { if (value == null || value.isEmpty) { return 'Veuillez répondre à cette question'; } final int? answer = int.tryParse(value); if (answer == null) { return 'Veuillez entrer un nombre'; } if (answer != _captchaNum1 + _captchaNum2) { return 'La réponse est incorrecte'; } return null; }, ), // Champ caché pour le token anti-robot // Ce champ ne sera pas visible mais sera envoyé avec le formulaire Opacity( opacity: 0, child: SizedBox( height: 0, child: TextFormField( initialValue: _hiddenToken, enabled: false, ), ), ), const SizedBox(height: 32), // Bouton d'inscription CustomButton( onPressed: (_isLoading || (_isMobile && !_isConnected)) ? null : () async { if (_formKey.currentState!.validate()) { // Vérifier la connexion Internet avant de soumettre // Utiliser l'instance globale de connectivityService définie dans app.dart await connectivityService .checkConnectivity(); if (!connectivityService.isConnected) { if (context.mounted) { ScaffoldMessenger.of(context) .showSnackBar( SnackBar( content: const Text( 'Aucune connexion Internet. L\'inscription nécessite une connexion active.'), backgroundColor: theme.colorScheme.error, duration: const Duration(seconds: 3), action: SnackBarAction( label: 'Réessayer', onPressed: () async { await connectivityService .checkConnectivity(); if (connectivityService .isConnected && context.mounted) { ScaffoldMessenger.of( context) .showSnackBar( SnackBar( content: Text( 'Connexion Internet ${connectivityService.connectionType} détectée.'), backgroundColor: Colors.green, ), ); } }, ), ), ); } return; } // Vérifier que le captcha est correct final int? captchaAnswer = int.tryParse( _captchaController.text); if (captchaAnswer != _captchaNum1 + _captchaNum2) { if (!context.mounted) return; ScaffoldMessenger.of(context) .showSnackBar( const SnackBar( content: Text( 'La vérification de sécurité a échoué. Veuillez réessayer.'), backgroundColor: Colors.red, ), ); return; } // Préparer les données du formulaire final Map formData = { 'email': _emailController.text.trim(), 'name': _nameController.text.trim(), 'amicale_name': _amicaleNameController .text .trim(), 'postal_code': _postalCodeController.text, 'city_name': _selectedCity?.name ?? '', 'captcha_answer': captchaAnswer, 'captcha_expected': _captchaNum1 + _captchaNum2, 'token': _hiddenToken, }; // Afficher un indicateur de chargement setState(() { _isLoading = true; }); try { // Envoyer les données à l'API final baseUrl = Uri.base.origin; final apiUrl = '$baseUrl/api/register'; final response = await http.post( Uri.parse(apiUrl), headers: { 'Content-Type': 'application/json', }, body: json.encode(formData), ); // Masquer l'indicateur de chargement setState(() { _isLoading = false; }); // Traiter la réponse if (response.statusCode == 200 || response.statusCode == 201) { final responseData = json.decode(response.body); // Vérifier si la réponse indique un succès final bool isSuccess = responseData['success'] == true || responseData['status'] == 'success'; // Récupérer le message de la réponse final String message = responseData[ 'message'] ?? (isSuccess ? 'Inscription réussie !' : 'Échec de l\'inscription. Veuillez réessayer.'); if (isSuccess) { if (context.mounted) { // Afficher une boîte de dialogue de succès showDialog( context: context, barrierDismissible: false, // L'utilisateur doit cliquer sur OK builder: (BuildContext context) { return AlertDialog( title: const Row( children: [ Icon( Icons.check_circle, color: Colors.green, ), SizedBox(width: 10), Text( 'Inscription réussie'), ], ), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment .start, children: [ Text( 'Votre demande d\'inscription a été enregistrée avec succès.', style: theme .textTheme .bodyLarge, ), const SizedBox( height: 16), Text( 'Vous allez recevoir un email contenant :', style: theme .textTheme .bodyMedium, ), const SizedBox( height: 8), Row( crossAxisAlignment: CrossAxisAlignment .start, children: [ Icon( Icons .arrow_right, size: 20, color: theme .colorScheme .primary), const SizedBox( width: 4), const Expanded( child: Text( 'Votre identifiant de connexion'), ), ], ), const SizedBox( height: 4), Row( crossAxisAlignment: CrossAxisAlignment .start, children: [ Icon( Icons .arrow_right, size: 20, color: theme .colorScheme .primary), const SizedBox( width: 4), const Expanded( child: Text( 'Un lien pour définir votre mot de passe'), ), ], ), const SizedBox( height: 16), Text( 'Vérifiez votre boîte de réception et vos spams.', style: TextStyle( fontStyle: FontStyle .italic, color: theme .colorScheme .onSurface .withValues(alpha: 0.7), ), ), ], ), actions: [ TextButton( onPressed: () { Navigator.of( context) .pop(); // Rediriger vers splash avec redirection automatique vers login admin context .go('/?action=login&type=admin'); }, style: TextButton .styleFrom( foregroundColor: theme .colorScheme .primary, textStyle: const TextStyle( fontWeight: FontWeight .bold), ), child: const Text('OK'), ), ], ); }, ); } } else { // Afficher le message d'erreur retourné par l'API if (context.mounted) { // Afficher un message d'erreur plus visible showDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: const Text( 'Erreur d\'inscription'), content: Text(message), actions: [ TextButton( onPressed: () { Navigator.of( context) .pop(); }, child: const Text('OK'), ), ], ); }, ); // Afficher également un SnackBar if (context.mounted) { ScaffoldMessenger.of(context) .showSnackBar( SnackBar( content: Text(message), backgroundColor: Colors.red, ), ); } } } } else { // Gérer les erreurs HTTP if (context.mounted) { ScaffoldMessenger.of(context) .showSnackBar( SnackBar( content: Text( 'Erreur ${response.statusCode}: ${response.reasonPhrase ?? "Échec de l'inscription"}'), backgroundColor: Colors.red, ), ); } } } catch (e) { // Masquer l'indicateur de chargement setState(() { _isLoading = false; }); // Gérer les exceptions if (context.mounted) { ScaffoldMessenger.of(context) .showSnackBar( SnackBar( content: Text( 'Erreur: ${e.toString()}'), backgroundColor: Colors.red, ), ); } } } }, text: (_isMobile && !_isConnected) ? 'Connexion Internet requise' : 'Enregistrer mon amicale', isLoading: _isLoading, ), const SizedBox(height: 24), // Déjà un compte Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( 'Déjà un compte ?', style: theme.textTheme.bodyMedium, ), TextButton( onPressed: () { context.go('/?action=login&type=admin'); }, child: Text( 'Se connecter', style: TextStyle( color: theme.colorScheme.primary, fontWeight: FontWeight.bold, ), ), ), ], ), // Lien vers le site web TextButton( onPressed: () { // Déterminer l'URL du site web en fonction de l'environnement 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'; } } // Ouvrir l'URL dans une nouvelle fenêtre/onglet launchUrl( Uri.parse(webUrl), mode: LaunchMode.externalApplication, ); }, child: const Row( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.language, size: 16, color: Colors.blue, ), SizedBox(width: 8), Text( 'Revenir sur le site web', style: TextStyle( color: Colors.blue, fontWeight: FontWeight.bold, ), ), ], ), ), ], ), ), ], ), ), ), ), ), // 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.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), border: Border.all( color: theme.colorScheme.primary.withValues(alpha: 0.3), width: 1, ), ), child: Text( 'v$_appVersion', style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.primary.withValues(alpha: 0.8), fontSize: 10, fontWeight: FontWeight.w500, ), ), ), ), ], ), ); } }