import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'dart:convert'; import 'package:flutter_map/flutter_map.dart'; import 'package:geosector_app/core/data/models/amicale_model.dart'; import 'package:geosector_app/core/repositories/user_repository.dart'; import 'package:geosector_app/core/services/api_service.dart'; import 'package:geosector_app/core/services/stripe_connect_service.dart'; import 'package:geosector_app/presentation/widgets/mapbox_map.dart'; import 'package:latlong2/latlong.dart'; import 'package:image_picker/image_picker.dart'; import 'package:flutter/foundation.dart' show kIsWeb; import 'dart:io'; import 'package:geosector_app/presentation/widgets/loading_spin_overlay.dart'; import 'package:geosector_app/presentation/widgets/result_dialog.dart'; import 'custom_text_field.dart'; class AmicaleForm extends StatefulWidget { final AmicaleModel? amicale; final Function(AmicaleModel)? onSubmit; final bool readOnly; final UserRepository userRepository; // Nouveau paramètre final ApiService? apiService; // Nouveau paramètre optionnel const AmicaleForm({ super.key, this.amicale, this.onSubmit, this.readOnly = false, required this.userRepository, // Requis this.apiService, // Optionnel }); @override State createState() => _AmicaleFormState(); } class _AmicaleFormState extends State { final _formKey = GlobalKey(); // Controllers late final TextEditingController _nameController; late final TextEditingController _adresse1Controller; late final TextEditingController _adresse2Controller; late final TextEditingController _codePostalController; late final TextEditingController _villeController; late final TextEditingController _phoneController; late final TextEditingController _mobileController; late final TextEditingController _emailController; late final TextEditingController _gpsLatController; late final TextEditingController _gpsLngController; late final TextEditingController _stripeIdController; // Form values int? _fkRegion; String? _libRegion; bool _chkDemo = false; bool _chkCopieMailRecu = false; bool _chkAcceptSms = false; bool _chkActive = true; bool _chkStripe = false; bool _chkMdpManuel = false; bool _chkUsernameManuel = false; bool _chkUserDeletePass = false; bool _chkLotActif = false; // Pour l'upload du logo final ImagePicker _picker = ImagePicker(); XFile? _selectedImage; // Pour Stripe Connect StripeConnectService? _stripeService; bool _isCheckingStripeStatus = false; StripeAccountStatus? _stripeStatus; @override void initState() { super.initState(); // Initialize controllers with amicale data if available final amicale = widget.amicale; _nameController = TextEditingController(text: amicale?.name ?? ''); _adresse1Controller = TextEditingController(text: amicale?.adresse1 ?? ''); _adresse2Controller = TextEditingController(text: amicale?.adresse2 ?? ''); _codePostalController = TextEditingController(text: amicale?.codePostal ?? ''); _villeController = TextEditingController(text: amicale?.ville ?? ''); _phoneController = TextEditingController(text: amicale?.phone ?? ''); _mobileController = TextEditingController(text: amicale?.mobile ?? ''); _emailController = TextEditingController(text: amicale?.email ?? ''); _gpsLatController = TextEditingController(text: amicale?.gpsLat ?? ''); _gpsLngController = TextEditingController(text: amicale?.gpsLng ?? ''); _stripeIdController = TextEditingController(text: amicale?.stripeId ?? ''); _fkRegion = amicale?.fkRegion; _libRegion = amicale?.libRegion; _chkDemo = amicale?.chkDemo ?? false; _chkCopieMailRecu = amicale?.chkCopieMailRecu ?? false; _chkAcceptSms = amicale?.chkAcceptSms ?? false; _chkActive = amicale?.chkActive ?? true; _chkStripe = amicale?.chkStripe ?? false; _chkMdpManuel = amicale?.chkMdpManuel ?? false; _chkUsernameManuel = amicale?.chkUsernameManuel ?? false; _chkUserDeletePass = amicale?.chkUserDeletePass ?? false; _chkLotActif = amicale?.chkLotActif ?? false; // Note : Le logo sera chargé dynamiquement depuis l'API // Initialiser le service Stripe si API disponible if (widget.apiService != null) { _stripeService = StripeConnectService(apiService: widget.apiService!); // Vérifier le statut Stripe si l'amicale a déjà un compte if (_chkStripe && widget.amicale != null) { _checkStripeStatus(); } } } // Vérifier le statut du compte Stripe Future _checkStripeStatus() async { if (_stripeService == null || widget.amicale == null) return; setState(() => _isCheckingStripeStatus = true); try { final status = await _stripeService!.checkAccountStatus(widget.amicale!.id); setState(() { _stripeStatus = status; if (status.accountId != null) { _stripeIdController.text = status.accountId!; } }); } catch (e) { debugPrint('Erreur vérification statut Stripe: $e'); } finally { setState(() => _isCheckingStripeStatus = false); } } // Configurer Stripe Connect Future _configureStripe() async { if (_stripeService == null || widget.amicale == null) return; // Vérifier que nous sommes sur Web if (!kIsWeb) { showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Configuration Web requise'), content: const Text( 'La configuration du compte Stripe doit être effectuée depuis un navigateur web.\n\n' 'Veuillez vous connecter depuis un ordinateur pour configurer les paiements par carte bancaire.', ), actions: [ ElevatedButton( onPressed: () => Navigator.of(context).pop(), child: const Text('Compris'), ), ], ), ); return; } // Afficher un dialog de confirmation final bool? confirm = await showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Configuration Stripe'), content: const Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Vous allez être redirigé vers Stripe pour :'), SizedBox(height: 8), Text('• Créer votre compte marchand'), Text('• Configurer vos informations bancaires'), Text('• Activer les paiements par carte'), SizedBox(height: 16), Text('Ce processus prend environ 5-10 minutes.'), ], ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(false), child: const Text('Annuler'), ), ElevatedButton( onPressed: () => Navigator.of(context).pop(true), child: const Text('Continuer'), ), ], ), ); if (confirm != true) return; // Afficher le loading if (!context.mounted) return; showDialog( // ignore: use_build_context_synchronously context: context, barrierDismissible: false, builder: (context) => const AlertDialog( content: Column( mainAxisSize: MainAxisSize.min, children: [ CircularProgressIndicator(), SizedBox(height: 16), Text('Préparation de votre compte Stripe...'), ], ), ), ); try { // Créer ou récupérer le lien d'onboarding final url = await _stripeService!.createStripeAccount(widget.amicale!); // Fermer le loading if (mounted) Navigator.of(context).pop(); if (url != null) { // Lancer l'onboarding final success = await _stripeService!.launchOnboarding(url); if (success) { // Activer la checkbox setState(() => _chkStripe = true); // Afficher un message de succès if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Configuration Stripe lancée. Revenez ici après avoir terminé.'), duration: Duration(seconds: 5), ), ); } // Vérifier le statut après un délai Future.delayed(const Duration(seconds: 5), _checkStripeStatus); } } else { throw Exception('Impossible de créer le lien de configuration'); } } catch (e) { // Fermer le loading si encore ouvert if (mounted && Navigator.of(context).canPop()) { Navigator.of(context).pop(); } // Afficher l'erreur if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Erreur: ${e.toString()}'), backgroundColor: Colors.red, ), ); } } } @override void dispose() { _nameController.dispose(); _adresse1Controller.dispose(); _adresse2Controller.dispose(); _codePostalController.dispose(); _villeController.dispose(); _phoneController.dispose(); _mobileController.dispose(); _emailController.dispose(); _gpsLatController.dispose(); _gpsLngController.dispose(); _stripeIdController.dispose(); super.dispose(); } // Appeler l'API pour mettre à jour l'entité Future _updateAmicale(AmicaleModel amicale) async { if (!mounted) return; // Afficher l'overlay de chargement final overlay = LoadingSpinOverlayUtils.show( context: context, message: 'Mise à jour en cours...', ); try { // Préparer les données pour l'API final Map data = { 'id': amicale.id, 'name': amicale.name, 'adresse1': amicale.adresse1, 'adresse2': amicale.adresse2, 'code_postal': amicale.codePostal, 'ville': amicale.ville, 'phone': amicale.phone, 'mobile': amicale.mobile, 'email': amicale.email, 'chk_copie_mail_recu': amicale.chkCopieMailRecu ? 1 : 0, 'chk_accept_sms': amicale.chkAcceptSms ? 1 : 0, 'chk_stripe': amicale.chkStripe ? 1 : 0, 'chk_mdp_manuel': amicale.chkMdpManuel ? 1 : 0, 'chk_username_manuel': amicale.chkUsernameManuel ? 1 : 0, 'chk_user_delete_pass': amicale.chkUserDeletePass ? 1 : 0, 'chk_lot_actif': amicale.chkLotActif ? 1 : 0, }; // Ajouter les champs réservés aux administrateurs si l'utilisateur est admin final userRole = widget.userRepository.getUserRole(); if (userRole > 2) { data['gps_lat'] = amicale.gpsLat; data['gps_lng'] = amicale.gpsLng; data['stripe_id'] = amicale.stripeId; data['chk_demo'] = amicale.chkDemo ? 1 : 0; data['chk_active'] = amicale.chkActive ? 1 : 0; } debugPrint('🔧 Données à envoyer à l\'API: $data'); bool apiSuccess = false; String? errorMessage; // Appeler l'API si le service est disponible if (widget.apiService != null) { try { debugPrint('📡 Appel API pour mise à jour amicale...'); // Version RESTful correcte avec PUT final response = await widget.apiService!.put('/entites/${amicale.id}', data: data); // Alternative avec PATCH si votre API le supporte // final response = await widget.apiService!.patch('/entites/${amicale.id}', data: data); debugPrint('📡 Réponse API: ${response.statusCode}'); if (response.statusCode == 200 || response.statusCode == 201) { apiSuccess = true; } else { errorMessage = 'Erreur serveur: ${response.statusCode}'; } } catch (error) { debugPrint('❌ Erreur API: $error'); errorMessage = 'Erreur lors de la communication avec le serveur: $error'; } } // Masquer le loading LoadingSpinOverlayUtils.hideSpecific(overlay); if (!mounted) return; if (apiSuccess) { // Appeler la fonction onSubmit si elle existe if (widget.onSubmit != null) { widget.onSubmit!(amicale); } // Afficher le résultat de succès await ResultDialog.show( context: context, success: true, message: widget.apiService != null ? 'Amicale mise à jour avec succès' : 'Modifications enregistrées localement', ); // Fermer le formulaire if (mounted && Navigator.of(context).canPop()) { Navigator.of(context).pop(); } } else { // Afficher le résultat d'erreur await ResultDialog.show( context: context, success: false, message: errorMessage ?? 'Erreur lors de la mise à jour', ); } } catch (e) { debugPrint('❌ Erreur générale dans _updateAmicale: $e'); // Masquer le loading LoadingSpinOverlayUtils.hideSpecific(overlay); // Afficher l'erreur if (mounted) { await ResultDialog.show( context: context, success: false, message: 'Erreur inattendue: ${e.toString()}', ); } } } // Méthode pour sélectionner une image Future _selectImage() async { try { final XFile? image = await _picker.pickImage( source: ImageSource.gallery, maxWidth: 1024, maxHeight: 1024, imageQuality: 85, ); if (image != null) { // Vérifier la taille du fichier (limite 5 Mo) final int fileSize = await image.length(); const int maxSize = 5 * 1024 * 1024; // 5 Mo en octets if (fileSize > maxSize) { // Fichier trop volumineux final double sizeMB = fileSize / (1024 * 1024); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( 'Le fichier est trop volumineux (${sizeMB.toStringAsFixed(2)} Mo). ' 'La taille maximale autorisée est de 5 Mo.', ), backgroundColor: Colors.orange, duration: const Duration(seconds: 5), ), ); } return; } setState(() { _selectedImage = image; }); // Upload immédiatement après sélection if (widget.amicale?.id != null) { await _uploadLogo(); } } } catch (e) { debugPrint('Erreur lors de la sélection de l\'image: $e'); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Erreur lors de la sélection de l\'image: $e'), backgroundColor: Colors.red, ), ); } } } // Méthode pour uploader le logo Future _uploadLogo() async { if (_selectedImage == null || widget.amicale?.id == null) return; OverlayEntry? spinOverlay; try { // Afficher le spinner spinOverlay = LoadingSpinOverlayUtils.show( context: context, message: 'Upload du logo en cours...', blurAmount: 10.0, showCard: true, ); // Appeler l'API pour uploader le logo final response = await widget.apiService?.uploadLogo( widget.amicale!.id, _selectedImage!, ); if (response != null && response['status'] == 'success') { // Succès if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Logo uploadé avec succès'), backgroundColor: Colors.green, ), ); } // Mettre à jour l'amicale avec le nouveau logo en base64 // Note : Le serveur devrait aussi mettre à jour le logo dans la session // Pour l'instant on garde l'image sélectionnée en preview setState(() { // L'image reste en preview jusqu'au prochain login }); } } catch (e) { debugPrint('Erreur lors de l\'upload du logo: $e'); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Erreur lors de l\'upload: ${e.toString()}'), backgroundColor: Colors.red, ), ); } } finally { // Fermer le spinner LoadingSpinOverlayUtils.hideSpecific(spinOverlay); } } void _submitForm() { debugPrint('🔧 _submitForm appelée'); if (!_formKey.currentState!.validate()) { // Afficher une dialog si la validation échoue showDialog( context: context, builder: (context) => AlertDialog( title: const Row( children: [ Icon(Icons.warning_amber_rounded, color: Colors.orange), SizedBox(width: 8), Text('Formulaire incomplet'), ], ), content: const Text('Veuillez vérifier tous les champs marqués en rouge avant d\'enregistrer'), actions: [ ElevatedButton( onPressed: () => Navigator.of(context).pop(), child: const Text('OK'), ), ], ), ); return; } debugPrint('🔧 Formulaire valide'); // Vérifier qu'au moins un numéro de téléphone est renseigné if (_phoneController.text.isEmpty && _mobileController.text.isEmpty) { debugPrint('⚠️ Aucun numéro de téléphone renseigné'); showDialog( context: context, builder: (context) => AlertDialog( title: const Row( children: [ Icon(Icons.warning_amber_rounded, color: Colors.orange), SizedBox(width: 8), Text('Formulaire incomplet'), ], ), content: const Text('Veuillez renseigner au moins un numéro de téléphone'), actions: [ ElevatedButton( onPressed: () => Navigator.of(context).pop(), child: const Text('OK'), ), ], ), ); return; } debugPrint('🔧 Création de l\'objet AmicaleModel...'); final amicale = widget.amicale?.copyWith( name: _nameController.text, adresse1: _adresse1Controller.text, adresse2: _adresse2Controller.text, codePostal: _codePostalController.text, ville: _villeController.text, fkRegion: _fkRegion, libRegion: _libRegion, phone: _phoneController.text, mobile: _mobileController.text, email: _emailController.text, gpsLat: _gpsLatController.text, gpsLng: _gpsLngController.text, stripeId: _stripeIdController.text, chkDemo: _chkDemo, chkCopieMailRecu: _chkCopieMailRecu, chkAcceptSms: _chkAcceptSms, chkActive: _chkActive, chkStripe: _chkStripe, chkMdpManuel: _chkMdpManuel, chkUsernameManuel: _chkUsernameManuel, chkUserDeletePass: _chkUserDeletePass, chkLotActif: _chkLotActif, ) ?? AmicaleModel( id: 0, // Sera remplacé par l'API name: _nameController.text, adresse1: _adresse1Controller.text, adresse2: _adresse2Controller.text, codePostal: _codePostalController.text, ville: _villeController.text, fkRegion: _fkRegion, libRegion: _libRegion, phone: _phoneController.text, mobile: _mobileController.text, email: _emailController.text, gpsLat: _gpsLatController.text, gpsLng: _gpsLngController.text, stripeId: _stripeIdController.text, chkDemo: _chkDemo, chkCopieMailRecu: _chkCopieMailRecu, chkAcceptSms: _chkAcceptSms, chkActive: _chkActive, chkStripe: _chkStripe, chkMdpManuel: _chkMdpManuel, chkUsernameManuel: _chkUsernameManuel, chkUserDeletePass: _chkUserDeletePass, chkLotActif: _chkLotActif, ); debugPrint('🔧 AmicaleModel créé: ${amicale.name}'); debugPrint('🔧 Appel de _updateAmicale...'); // Appeler l'API pour mettre à jour l'amicale _updateAmicale(amicale); } // Construire la section logo Widget _buildLogoSection() { // Vérifier si on est admin d'amicale (role 2) final userRole = widget.userRepository.getUserRole(); final canUploadLogo = userRole == 2 && !widget.readOnly; return Container( width: 150, height: 150, decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(8), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 4, offset: const Offset(0, 2), ), ], ), child: ClipRRect( borderRadius: BorderRadius.circular(8), child: Stack( children: [ // Afficher l'image sélectionnée, ou le logo depuis l'API, ou l'image par défaut Center( child: _buildLogoImage(), ), // Overlay pour indiquer que l'image est modifiable (si admin d'amicale) if (canUploadLogo) Positioned.fill( child: Material( color: Colors.transparent, child: InkWell( onTap: _selectImage, child: Container( decoration: BoxDecoration( color: Colors.black.withOpacity(0.3), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: const [ Icon( Icons.camera_alt, color: Colors.white, size: 32, ), SizedBox(height: 4), Text( 'Modifier', style: TextStyle( color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold, ), ), ], ), ), ), ), ), ], ), ), ); } // Méthode pour construire l'image du logo Widget _buildLogoImage() { // 1. Si une image a été sélectionnée localement (preview) if (_selectedImage != null) { if (kIsWeb) { return FutureBuilder( future: _selectedImage!.readAsBytes(), builder: (context, snapshot) { if (snapshot.hasData) { return Image.memory( snapshot.data!, width: 150, height: 150, fit: BoxFit.contain, ); } return const CircularProgressIndicator(); }, ); } else { return Image.file( File(_selectedImage!.path), width: 150, height: 150, fit: BoxFit.contain, ); } } // 2. Si l'amicale a un logo en base64 stocké dans Hive if (widget.amicale?.logoBase64 != null && widget.amicale!.logoBase64!.isNotEmpty) { try { // Le logoBase64 contient déjà le data URL complet (data:image/png;base64,...) final dataUrl = widget.amicale!.logoBase64!; // Extraire le base64 du data URL final base64Data = dataUrl.split(',').last; final bytes = base64Decode(base64Data); return Image.memory( bytes, width: 150, height: 150, fit: BoxFit.contain, errorBuilder: (context, error, stackTrace) { debugPrint('Erreur affichage logo base64: $error'); // En cas d'erreur, essayer l'API return _buildLogoFromApi(); }, ); } catch (e) { debugPrint('Erreur décodage base64: $e'); // En cas d'erreur, essayer l'API return _buildLogoFromApi(); } } // 3. Sinon, essayer de charger depuis l'API return _buildLogoFromApi(); } // Méthode pour charger le logo depuis l'API Widget _buildLogoFromApi() { if (widget.amicale?.id != null && widget.apiService != null) { // Construire l'URL complète du logo final logoUrl = '${widget.apiService!.baseUrl}/entites/${widget.amicale!.id}/logo'; return Image.network( logoUrl, width: 150, height: 150, fit: BoxFit.contain, headers: { 'Authorization': 'Bearer ${widget.apiService!.sessionId ?? ""}', }, errorBuilder: (context, error, stackTrace) { // En cas d'erreur, afficher l'image par défaut return Image.asset( 'assets/images/logo_recu.png', width: 150, height: 150, fit: BoxFit.contain, ); }, ); } // Par défaut, afficher l'image locale return Image.asset( 'assets/images/logo_recu.png', width: 150, height: 150, fit: BoxFit.contain, ); } // Construire la minimap Widget _buildMiniMap() { // Vérifier si les coordonnées GPS sont valides double? lat = double.tryParse(_gpsLatController.text); double? lng = double.tryParse(_gpsLngController.text); // Si les coordonnées ne sont pas valides, afficher un message if (lat == null || lng == null) { return Container( width: 150, height: 150, decoration: BoxDecoration( color: Colors.grey.shade200, borderRadius: BorderRadius.circular(8), ), child: const Center( child: Text( 'Aucune coordonnée GPS', textAlign: TextAlign.center, style: TextStyle( color: Colors.grey, fontSize: 12, ), ), ), ); } // Créer la position pour la carte final position = LatLng(lat, lng); // Créer un marqueur pour la position de l'amicale final markers = [ Marker( point: position, width: 20, height: 20, child: const Icon( Icons.fireplace_rounded, color: Color.fromARGB(255, 212, 34, 31), size: 20, ), ), ]; // Retourner la minimap return Container( width: 150, height: 150, decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 4, offset: const Offset(0, 2), ), ], ), child: ClipRRect( borderRadius: BorderRadius.circular(8), child: MapboxMap( initialPosition: position, initialZoom: 15.0, markers: markers, showControls: false, ), ), ); } // Construire le dropdown pour la région Widget _buildRegionDropdown(bool restrictedFieldsReadOnly) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Afficher le libellé de la région en lecture seule if (_libRegion != null && _libRegion!.isNotEmpty) Container( width: double.infinity, padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), decoration: BoxDecoration( color: Theme.of(context).inputDecorationTheme.fillColor, borderRadius: BorderRadius.circular(4), ), child: Text( _libRegion!, style: Theme.of(context).textTheme.bodyLarge?.copyWith( color: Theme.of(context).colorScheme.onSurface, ), ), ) else Container( width: double.infinity, padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), decoration: BoxDecoration( color: Theme.of(context).inputDecorationTheme.fillColor, borderRadius: BorderRadius.circular(4), ), child: Text( 'Aucune région définie', style: Theme.of(context).textTheme.bodyLarge?.copyWith( color: Theme.of(context).hintColor, ), ), ), ], ); } // Construire une option checkbox Widget _buildCheckboxOption({ required String label, required bool value, required void Function(bool?)? onChanged, }) { return Row( children: [ Checkbox( value: value, onChanged: onChanged, activeColor: Theme.of(context).colorScheme.primary, ), Expanded( child: Text( label, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.w500, ), ), ), ], ); } // Construire le formulaire principal Widget _buildMainForm(ThemeData theme, bool restrictedFieldsReadOnly, bool stripeReadOnly) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Nom CustomTextField( controller: _nameController, label: "Nom", readOnly: widget.readOnly, isRequired: true, validator: (value) { if (value == null || value.isEmpty) { return "Veuillez entrer un nom"; } return null; }, ), const SizedBox(height: 16), // Bloc Adresse Text( "Adresse", style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, color: theme.colorScheme.onSurface, ), ), const SizedBox(height: 8), // Adresse 1 CustomTextField( controller: _adresse1Controller, label: "Adresse ligne 1", readOnly: widget.readOnly, isRequired: true, validator: (value) { if (value == null || value.isEmpty) { return "Veuillez entrer une adresse"; } return null; }, ), const SizedBox(height: 16), // Adresse 2 CustomTextField( controller: _adresse2Controller, label: "Adresse ligne 2", readOnly: widget.readOnly, ), const SizedBox(height: 16), // Code Postal et Ville Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Code Postal Expanded( flex: 1, child: CustomTextField( controller: _codePostalController, label: "Code Postal", keyboardType: TextInputType.number, inputFormatters: [ FilteringTextInputFormatter.digitsOnly, LengthLimitingTextInputFormatter(5), ], readOnly: widget.readOnly, isRequired: true, validator: (value) { if (value == null || value.isEmpty) { return "Veuillez entrer un code postal"; } if (value.length < 5) { return "Le code postal doit contenir 5 chiffres"; } return null; }, ), ), const SizedBox(width: 16), // Ville Expanded( flex: 2, child: CustomTextField( controller: _villeController, label: "Ville", readOnly: widget.readOnly, isRequired: true, validator: (value) { if (value == null || value.isEmpty) { return "Veuillez entrer une ville"; } return null; }, ), ), ], ), const SizedBox(height: 16), // Région Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "Région", style: theme.textTheme.titleSmall?.copyWith( fontWeight: FontWeight.w500, color: theme.colorScheme.onSurface, ), ), const SizedBox(height: 8), _buildRegionDropdown(restrictedFieldsReadOnly), ], ), const SizedBox(height: 16), // Contact Text( "Contact", style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, color: theme.colorScheme.onSurface, ), ), const SizedBox(height: 8), // Téléphone fixe et mobile sur la même ligne Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Téléphone fixe Expanded( child: CustomTextField( controller: _phoneController, label: "Téléphone fixe", keyboardType: TextInputType.phone, readOnly: widget.readOnly, inputFormatters: [ FilteringTextInputFormatter.digitsOnly, LengthLimitingTextInputFormatter(10), ], validator: (value) { if (value != null && value.isNotEmpty && value.length < 10) { return "Le numéro de téléphone doit contenir 10 chiffres"; } return null; }, ), ), const SizedBox(width: 16), // Téléphone mobile Expanded( child: CustomTextField( controller: _mobileController, label: "Téléphone mobile", keyboardType: TextInputType.phone, readOnly: widget.readOnly, inputFormatters: [ FilteringTextInputFormatter.digitsOnly, LengthLimitingTextInputFormatter(10), ], validator: (value) { if (value != null && value.isNotEmpty && value.length < 10) { return "Le numéro de mobile doit contenir 10 chiffres"; } return null; }, ), ), ], ), const SizedBox(height: 16), // Email CustomTextField( controller: _emailController, label: "Email", keyboardType: TextInputType.emailAddress, readOnly: widget.readOnly, isRequired: true, validator: (value) { if (value == null || value.isEmpty) { return "Veuillez entrer l'adresse email"; } if (!value.contains('@') || !value.contains('.')) { return "Veuillez entrer une adresse email valide"; } return null; }, ), const SizedBox(height: 16), // Informations avancées (visibles uniquement pour les administrateurs) if (_shouldShowAdvancedInfo()) ...[ Text( "Informations avancées", style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, color: theme.colorScheme.onSurface, ), ), const SizedBox(height: 8), // GPS Latitude et Longitude Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // GPS Latitude Expanded( child: CustomTextField( controller: _gpsLatController, label: "GPS Latitude", keyboardType: const TextInputType.numberWithOptions(decimal: true), readOnly: restrictedFieldsReadOnly, ), ), const SizedBox(width: 16), // GPS Longitude Expanded( child: CustomTextField( controller: _gpsLngController, label: "GPS Longitude", keyboardType: const TextInputType.numberWithOptions(decimal: true), readOnly: restrictedFieldsReadOnly, ), ), ], ), const SizedBox(height: 16), // Stripe Checkbox et configuration Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ // Checkbox Stripe Checkbox( value: _chkStripe, onChanged: stripeReadOnly ? null : (value) { setState(() { _chkStripe = value ?? false; }); // Si on active la checkbox et qu'on a une amicale, vérifier le statut if (value == true && widget.amicale != null) { _checkStripeStatus(); } }, activeColor: const Color(0xFF20335E), ), Text( "Accepte les règlements en CB", style: theme.textTheme.bodyMedium?.copyWith( color: theme.colorScheme.onSurface, fontWeight: FontWeight.w500, ), ), const SizedBox(width: 16), // Bouton Configuration Stripe si checkbox cochée if (_chkStripe && !stripeReadOnly && widget.amicale != null) ...[ ElevatedButton.icon( onPressed: _isCheckingStripeStatus ? null : _configureStripe, icon: _isCheckingStripeStatus ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation(Colors.white), ), ) : Icon(kIsWeb ? Icons.settings : Icons.computer, size: 18), label: Text( !kIsWeb ? 'Config. Web requise' : (_stripeStatus?.canAcceptPayments == true ? 'Compte actif' : 'Configurer Stripe') ), style: ElevatedButton.styleFrom( backgroundColor: !kIsWeb ? Colors.grey : (_stripeStatus?.statusColor ?? Colors.orange), foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), ), ), const SizedBox(width: 8), // Indicateur de statut if (_stripeStatus != null) Tooltip( message: _stripeStatus!.statusMessage, child: Icon( _stripeStatus!.canAcceptPayments ? Icons.check_circle : Icons.warning_amber_rounded, color: _stripeStatus!.statusColor, size: 20, ), ), ], ], ), // Stripe ID (sur une ligne séparée) const SizedBox(height: 8), CustomTextField( controller: _stripeIdController, label: "ID Compte Stripe", readOnly: true, helperText: _chkStripe ? (_stripeStatus?.accountId != null ? "Compte Stripe Connect: ${_stripeStatus!.accountId}" : "L'ID sera généré automatiquement lors de la configuration") : "Activez les paiements CB pour configurer Stripe", ), // Message d'information sur les commissions if (_chkStripe) Padding( padding: const EdgeInsets.only(top: 8), child: Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: _stripeStatus?.statusColor.withOpacity(0.1) ?? Colors.orange.withOpacity(0.1), borderRadius: BorderRadius.circular(8), border: Border.all( color: _stripeStatus?.statusColor.withOpacity(0.3) ?? Colors.orange.withOpacity(0.3), ), ), child: Row( children: [ Icon( _stripeStatus?.canAcceptPayments == true ? Icons.check_circle_outline : Icons.info_outline, color: _stripeStatus?.statusColor ?? Colors.orange, size: 20, ), const SizedBox(width: 8), Expanded( child: Text( _stripeStatus?.canAcceptPayments == true ? "✅ Compte Stripe configuré - 100% des paiements pour votre amicale" : _stripeStatus?.onboardingCompleted == false ? "⏳ Configuration Stripe en cours. Veuillez compléter le processus d'onboarding." : "💳 Activez les paiements par carte bancaire pour vos membres", style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurface, ), ), ), ], ), ), ), ], ), const SizedBox(height: 16), ], // Options Text( "Options", style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, color: theme.colorScheme.onSurface, ), ), const SizedBox(height: 8), // Options organisées sur 2 colonnes Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Colonne de gauche Expanded( child: Column( children: [ // Checkbox Demo _buildCheckboxOption( label: "Mode démo", value: _chkDemo, onChanged: restrictedFieldsReadOnly ? null : (value) { setState(() { _chkDemo = value!; }); }, ), const SizedBox(height: 8), // Checkbox Copie Mail Reçu _buildCheckboxOption( label: "Copie des mails reçus", value: _chkCopieMailRecu, onChanged: widget.readOnly ? null : (value) { setState(() { _chkCopieMailRecu = value!; }); }, ), const SizedBox(height: 8), // Checkbox Accept SMS _buildCheckboxOption( label: "Accepte les SMS", value: _chkAcceptSms, onChanged: widget.readOnly ? null : (value) { setState(() { _chkAcceptSms = value!; }); }, ), ], ), ), const SizedBox(width: 32), // Colonne de droite Expanded( child: Column( children: [ // Checkbox Active _buildCheckboxOption( label: "Actif", value: _chkActive, onChanged: restrictedFieldsReadOnly ? null : (value) { setState(() { _chkActive = value!; }); }, ), const SizedBox(height: 8), // Checkbox Mot de passe manuel _buildCheckboxOption( label: "Saisie manuelle des mots de passe", value: _chkMdpManuel, onChanged: widget.readOnly ? null : (value) { setState(() { _chkMdpManuel = value!; }); }, ), const SizedBox(height: 8), // Checkbox Username manuel _buildCheckboxOption( label: "Saisie manuelle des identifiants", value: _chkUsernameManuel, onChanged: widget.readOnly ? null : (value) { setState(() { _chkUsernameManuel = value!; }); }, ), ], ), ), ], ), const SizedBox(height: 8), // Checkbox pour autoriser les membres à supprimer des passages _buildCheckboxOption( label: "Autoriser les membres à supprimer des passages", value: _chkUserDeletePass, onChanged: widget.readOnly ? null : (value) { setState(() { _chkUserDeletePass = value!; }); }, ), const SizedBox(height: 8), // Checkbox pour activer le mode Lot _buildCheckboxOption( label: "Activer le mode Lot (distributions groupées)", value: _chkLotActif, onChanged: widget.readOnly ? null : (value) { setState(() { _chkLotActif = value!; }); }, ), const SizedBox(height: 25), // Boutons Fermer et Enregistrer if (!widget.readOnly) Center( child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ // Bouton Fermer OutlinedButton( onPressed: () { Navigator.of(context).pop(); }, style: OutlinedButton.styleFrom( foregroundColor: const Color(0xFF20335E), side: const BorderSide(color: Color(0xFF20335E)), padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(50), ), minimumSize: const Size(150, 50), ), child: const Text( 'Fermer', style: TextStyle( fontSize: 18, fontWeight: FontWeight.w500, ), ), ), const SizedBox(width: 20), // Bouton Enregistrer ElevatedButton( onPressed: _submitForm, style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF20335E), foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(50), ), minimumSize: const Size(150, 50), ), child: const Text( 'Enregistrer', style: TextStyle( fontSize: 18, fontWeight: FontWeight.w500, ), ), ), ], ), ), ], ); } // Vérifier si les informations avancées doivent être affichées bool _shouldShowAdvancedInfo() { final userRole = widget.userRepository.getUserRole(); final bool canEditRestrictedFields = userRole > 2; return canEditRestrictedFields || _gpsLatController.text.isNotEmpty || _gpsLngController.text.isNotEmpty || _stripeIdController.text.isNotEmpty; } @override Widget build(BuildContext context) { final theme = Theme.of(context); // Note : Utilise le rôle RÉEL pour les permissions d'édition (pas le mode d'affichage) final userRole = widget.userRepository.getUserRole(); // Déterminer si l'utilisateur peut modifier les champs restreints (super admin uniquement) final bool canEditRestrictedFields = userRole > 2; // Pour Stripe, les admins d'amicale (rôle 2) et super admins peuvent configurer final bool canEditStripe = userRole >= 2; // Lecture seule pour les champs restreints si l'utilisateur n'a pas les droits final bool restrictedFieldsReadOnly = widget.readOnly || !canEditRestrictedFields; // Lecture seule spécifique pour Stripe final bool stripeReadOnly = widget.readOnly || !canEditStripe; // Calculer la largeur maximale du formulaire pour les écrans larges final screenWidth = MediaQuery.of(context).size.width; final maxFormWidth = screenWidth > 800 ? 800.0 : screenWidth; final formContent = Container( width: maxFormWidth, padding: const EdgeInsets.all(16), child: Form( key: _formKey, child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Header avec logo et minimap Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ // Section Logo _buildLogoSection(), // Section MiniMap _buildMiniMap(), ], ), const SizedBox(height: 24), // Formulaire principal _buildMainForm(theme, restrictedFieldsReadOnly, stripeReadOnly), ], ), ), ), ); // Vérifier si on est dans une Dialog en regardant le type du widget parent final route = ModalRoute.of(context); final isInDialog = route?.settings.name == null; // Si on est dans une Dialog, ne pas utiliser Scaffold if (isInDialog) { return Center(child: formContent); } // Sinon, utiliser Scaffold pour les pages complètes return Scaffold( appBar: AppBar( title: Text(widget.readOnly ? 'Détails de l\'amicale' : 'Modifier l\'amicale'), backgroundColor: theme.appBarTheme.backgroundColor, foregroundColor: theme.appBarTheme.foregroundColor, ), body: Center(child: formContent), ); } }