import 'package:flutter/material.dart'; import 'package:geosector_app/core/data/models/sector_model.dart'; import 'package:geosector_app/core/data/models/membre_model.dart'; import 'package:geosector_app/core/data/models/user_sector_model.dart'; import 'package:geosector_app/core/repositories/membre_repository.dart'; import 'package:geosector_app/core/services/current_amicale_service.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:geosector_app/core/constants/app_keys.dart'; class SectorDialog extends StatefulWidget { final SectorModel? existingSector; final List> coordinates; final Future Function(String name, String color, List memberIds) onSave; const SectorDialog({ super.key, this.existingSector, required this.coordinates, required this.onSave, }); @override State createState() => _SectorDialogState(); } class _SectorDialogState extends State { final _formKey = GlobalKey(); final _nameController = TextEditingController(); final _nameFocusNode = FocusNode(); final _searchController = TextEditingController(); Color _selectedColor = Colors.blue; final List _selectedMemberIds = []; bool _isLoading = false; bool _membersLoaded = false; String _searchQuery = ''; @override void initState() { super.initState(); if (widget.existingSector != null) { _nameController.text = widget.existingSector!.libelle; _selectedColor = _hexToColor(widget.existingSector!.color); // Charger les membres affectés au secteur _loadSectorMembers(); } // Donner le focus au champ nom après que le dialog soit construit WidgetsBinding.instance.addPostFrameCallback((_) { _nameFocusNode.requestFocus(); }); } // Charger les membres actuellement affectés au secteur void _loadSectorMembers() { if (widget.existingSector == null) return; debugPrint('=== Début chargement membres pour secteur ${widget.existingSector!.id} - ${widget.existingSector!.libelle} ==='); try { // Vérifier si la box UserSector est ouverte if (!Hive.isBoxOpen(AppKeys.userSectorBoxName)) { debugPrint('Box UserSector non ouverte'); return; } final userSectorBox = Hive.box(AppKeys.userSectorBoxName); debugPrint('Box UserSector contient ${userSectorBox.length} entrées au total'); // Afficher toutes les entrées pour debug for (var i = 0; i < userSectorBox.length; i++) { final us = userSectorBox.getAt(i); if (us != null) { debugPrint(' - UserSector[$i]: membreId=${us.id}, fkSector=${us.fkSector}, name="${us.firstName} ${us.name}"'); } } // Récupérer tous les UserSectorModel pour ce secteur final userSectors = userSectorBox.values .where((us) => us.fkSector == widget.existingSector!.id) .toList(); debugPrint('Trouvé ${userSectors.length} UserSectorModel pour le secteur ${widget.existingSector!.id}'); // Pré-sélectionner les IDs des membres affectés setState(() { _selectedMemberIds.clear(); for (final userSector in userSectors) { // userSector.id est l'ID du membre (pas de l'utilisateur) _selectedMemberIds.add(userSector.id); debugPrint('Membre présélectionné: ${userSector.firstName} ${userSector.name} (membreId: ${userSector.id}, fkSector: ${userSector.fkSector})'); } }); debugPrint('=== Fin chargement: ${_selectedMemberIds.length} membres présélectionnés ==='); debugPrint('IDs présélectionnés: $_selectedMemberIds'); // Marquer le chargement comme terminé setState(() { _membersLoaded = true; }); } catch (e) { debugPrint('Erreur lors du chargement des membres du secteur: $e'); setState(() { _membersLoaded = true; // Même en cas d'erreur }); } } @override void dispose() { _nameController.dispose(); _nameFocusNode.dispose(); _searchController.dispose(); super.dispose(); } Color _hexToColor(String hexColor) { final String colorStr = hexColor.startsWith('#') ? hexColor.substring(1) : hexColor; final String fullColorStr = colorStr.length == 6 ? 'FF$colorStr' : colorStr; return Color(int.parse(fullColorStr, radix: 16)); } String _colorToHex(Color color) { return '#${color.value.toRadixString(16).substring(2).toUpperCase()}'; } void _handleSave() async { if (_formKey.currentState!.validate()) { // Vérifier qu'au moins un membre est sélectionné if (_selectedMemberIds.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Veuillez sélectionner au moins un membre'), backgroundColor: Colors.orange, duration: Duration(seconds: 3), ), ); return; } // Indiquer que nous sommes en train de sauvegarder setState(() => _isLoading = true); try { // Appeler le callback onSave et attendre sa résolution await widget.onSave( _nameController.text.trim(), _colorToHex(_selectedColor), _selectedMemberIds, ); // Si tout s'est bien passé, fermer le dialog if (mounted) { Navigator.of(context).pop(); } } catch (e) { // En cas d'erreur, réactiver le bouton if (mounted) { setState(() => _isLoading = false); } // L'erreur sera gérée par le callback onSave rethrow; } } } void _showColorPicker() { // Grille 6x6 de couleurs suivant le spectre // 6 colonnes: Rouge, Orange, Jaune, Vert, Bleu, Violet // 6 lignes: variations de luminosité/saturation final List colors = _generateSpectralColorGrid(); showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Choisir une couleur'), contentPadding: const EdgeInsets.fromLTRB(20, 12, 20, 20), content: Container( width: 280, // Largeur fixe pour contrôler la taille child: Column( mainAxisSize: MainAxisSize.min, children: [ const SizedBox(height: 8), Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( border: Border.all(color: Colors.grey.shade300), borderRadius: BorderRadius.circular(8), color: Colors.grey.shade50, ), child: GridView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 6, mainAxisSpacing: 4, crossAxisSpacing: 4, childAspectRatio: 1.0, ), itemCount: colors.length, itemBuilder: (context, index) { final color = colors[index]; final isSelected = _selectedColor.value == color.value; return InkWell( onTap: () { setState(() { _selectedColor = color; }); Navigator.of(context).pop(); }, child: Container( width: 35, height: 35, decoration: BoxDecoration( color: color, borderRadius: BorderRadius.circular(4), border: Border.all( color: isSelected ? Colors.black87 : Colors.grey.shade400, width: isSelected ? 2.5 : 0.5, ), boxShadow: isSelected ? [ BoxShadow( color: Colors.black.withOpacity(0.3), blurRadius: 4, offset: const Offset(0, 2), ), ] : null, ), child: isSelected ? const Icon( Icons.check, color: Colors.white, size: 18, shadows: [ Shadow( color: Colors.black, blurRadius: 2, ), ], ) : null, ), ); }, ), ), const SizedBox(height: 12), // Affichage de la couleur sélectionnée Container( height: 40, decoration: BoxDecoration( color: _selectedColor, borderRadius: BorderRadius.circular(6), border: Border.all(color: Colors.grey.shade400), ), child: Center( child: Text( 'Couleur sélectionnée', style: TextStyle( color: _selectedColor.computeLuminance() > 0.5 ? Colors.black87 : Colors.white, fontWeight: FontWeight.w500, fontSize: 13, ), ), ), ), ], ), ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('Annuler'), ), ], ), ); } // Mettre en surbrillance les termes recherchés dans le texte List _highlightSearchTerms(String text) { if (_searchQuery.isEmpty) { return [TextSpan(text: text)]; } final List spans = []; final lowerText = text.toLowerCase(); int start = 0; int index = lowerText.indexOf(_searchQuery, start); while (index != -1) { // Ajouter le texte avant le terme trouvé if (index > start) { spans.add(TextSpan( text: text.substring(start, index), )); } // Ajouter le terme trouvé en surbrillance spans.add(TextSpan( text: text.substring(index, index + _searchQuery.length), style: const TextStyle( backgroundColor: Colors.yellow, fontWeight: FontWeight.bold, ), )); start = index + _searchQuery.length; index = lowerText.indexOf(_searchQuery, start); } // Ajouter le reste du texte if (start < text.length) { spans.add(TextSpan( text: text.substring(start), )); } return spans; } // Générer une grille 6x6 de couleurs spectrales List _generateSpectralColorGrid() { final List colors = []; // 6 teintes de base (colonnes) final List hues = [ 0, // Rouge 30, // Orange 60, // Jaune 120, // Vert 210, // Bleu 270, // Violet ]; // 6 variations de luminosité/saturation (lignes) // Du plus clair au plus foncé final List> variations = [ {'saturation': 0.3, 'lightness': 0.85}, // Très clair {'saturation': 0.5, 'lightness': 0.70}, // Clair {'saturation': 0.7, 'lightness': 0.55}, // Moyen clair {'saturation': 0.85, 'lightness': 0.45}, // Moyen foncé {'saturation': 0.95, 'lightness': 0.35}, // Foncé {'saturation': 1.0, 'lightness': 0.25}, // Très foncé ]; // Générer la grille ligne par ligne for (final variation in variations) { for (final hue in hues) { colors.add( HSLColor.fromAHSL( 1.0, hue, variation['saturation']!, variation['lightness']!, ).toColor(), ); } } return colors; } @override Widget build(BuildContext context) { final currentAmicale = CurrentAmicaleService.instance.currentAmicale; final screenHeight = MediaQuery.of(context).size.height; final dialogHeight = (screenHeight * 0.8).clamp(0.0, 800.0); // 80% de l'écran avec max 800px return AlertDialog( title: Text(widget.existingSector == null ? 'Nouveau secteur' : 'Modifier le secteur'), content: Container( width: 450, // Largeur fixe pour la dialog height: dialogHeight, // Hauteur avec maximum de 800px child: Form( key: _formKey, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Section scrollable pour nom et couleur // Nom du secteur TextFormField( controller: _nameController, focusNode: _nameFocusNode, decoration: InputDecoration( labelText: 'Nom du secteur', labelStyle: TextStyle(color: Colors.black), label: Row( mainAxisSize: MainAxisSize.min, children: [ Text('Nom du secteur'), Text( ' *', style: TextStyle(color: Colors.red), ), ], ), hintText: 'Ex: Centre-ville', prefixIcon: Icon(Icons.location_on), ), validator: (value) { if (value == null || value.trim().isEmpty) { return 'Veuillez entrer un nom'; } return null; }, ), const SizedBox(height: 20), // Couleur du secteur const Text( 'Couleur du secteur', style: TextStyle(fontWeight: FontWeight.bold), ), const SizedBox(height: 10), InkWell( onTap: () { _showColorPicker(); }, child: Container( height: 50, decoration: BoxDecoration( color: _selectedColor, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.grey), ), child: Center( child: Text( 'Toucher pour changer', style: TextStyle( color: _selectedColor.computeLuminance() > 0.5 ? Colors.black : Colors.white, fontWeight: FontWeight.bold, ), ), ), ), ), const SizedBox(height: 20), // Sélection des membres Row( children: [ const Text( 'Membres affectés', style: TextStyle(fontWeight: FontWeight.bold), ), const SizedBox(width: 4), Text( '*', style: TextStyle( color: Colors.red, fontWeight: FontWeight.bold, ), ), ], ), const SizedBox(height: 10), // Champ de recherche pour filtrer les membres TextField( controller: _searchController, decoration: InputDecoration( hintText: 'Rechercher par prénom, nom ou nom de tournée...', prefixIcon: const Icon(Icons.search), suffixIcon: _searchQuery.isNotEmpty ? IconButton( icon: const Icon(Icons.clear), onPressed: () { setState(() { _searchController.clear(); _searchQuery = ''; }); }, ) : null, border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), ), contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), ), onChanged: (value) { setState(() { _searchQuery = value.toLowerCase(); }); }, ), const SizedBox(height: 10), if (_selectedMemberIds.isEmpty) Padding( padding: const EdgeInsets.only(bottom: 8.0), child: Text( 'Sélectionnez au moins un membre', style: TextStyle( fontSize: 12, color: Colors.grey[600], fontStyle: FontStyle.italic, ), ), ), // Liste des membres avec scrolling et filtre if (currentAmicale != null) Expanded( child: ValueListenableBuilder>( valueListenable: Hive.box(AppKeys.membresBoxName).listenable(), builder: (context, box, _) { debugPrint('=== Build liste membres - IDs présélectionnés: $_selectedMemberIds ==='); // Filtrer les membres de l'amicale var membres = box.values .where((m) => m.fkEntite == currentAmicale.id) .toList(); // Appliquer le filtre de recherche if (_searchQuery.isNotEmpty) { membres = membres.where((membre) { final firstName = membre.firstName?.toLowerCase() ?? ''; final lastName = membre.name?.toLowerCase() ?? ''; final sectName = membre.sectName?.toLowerCase() ?? ''; return firstName.contains(_searchQuery) || lastName.contains(_searchQuery) || sectName.contains(_searchQuery); }).toList(); } if (membres.isEmpty) { return Center( child: Padding( padding: const EdgeInsets.all(20.0), child: Text( _searchQuery.isNotEmpty ? 'Aucun membre trouvé pour "$_searchQuery"' : 'Aucun membre disponible', style: TextStyle( color: Colors.grey[600], fontSize: 14, ), ), ), ); } // Afficher le nombre de résultats return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.symmetric(vertical: 4.0), child: Text( '${membres.length} membre${membres.length > 1 ? 's' : ''} ${_searchQuery.isNotEmpty ? 'trouvé${membres.length > 1 ? 's' : ''}' : 'disponible${membres.length > 1 ? 's' : ''}'}', style: TextStyle( fontSize: 12, color: Colors.grey[700], ), ), ), Expanded( child: Container( decoration: BoxDecoration( border: Border.all(color: Colors.grey), borderRadius: BorderRadius.circular(8), ), child: ListView.builder( itemCount: membres.length, itemBuilder: (context, index) { final membre = membres[index]; final isSelected = _selectedMemberIds.contains(membre.id); // Log pour debug if (index < 3) { // Limiter les logs aux 3 premiers membres debugPrint('Membre ${index}: ${membre.firstName} ${membre.name} (ID: ${membre.id}) - isSelected: $isSelected'); } return CheckboxListTile( dense: true, contentPadding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 0.0), title: RichText( text: TextSpan( style: const TextStyle(fontSize: 14, color: Colors.black87), children: _highlightSearchTerms( '${membre.firstName} ${membre.name}${membre.sectName != null && membre.sectName!.isNotEmpty ? ' (${membre.sectName})' : ''}', ), ), ), value: isSelected, onChanged: (bool? value) { setState(() { if (value == true) { _selectedMemberIds.add(membre.id); } else { _selectedMemberIds.remove(membre.id); } }); }, ); }, ), ), ), ], ); }, ), ), ], ), ), ), actions: [ TextButton( onPressed: _isLoading ? null : () => Navigator.of(context).pop(), child: const Text('Annuler'), ), ElevatedButton( onPressed: _isLoading ? null : _handleSave, child: _isLoading ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2), ) : Text(widget.existingSector == null ? 'Créer' : 'Modifier'), ), ], ); } }