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/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, bool updatePassages) 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; String _searchQuery = ''; bool _updatePassages = true; // Par défaut activé bool _initialUpdatePassages = true; // Valeur initiale pour détecter les changements @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]: userId=${us.userId}, opeUserId=${us.opeUserId}, 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 (ope_users.id) setState(() { _selectedMemberIds.clear(); for (final userSector in userSectors) { // Utiliser opeUserId (ope_users.id) pour la sélection _selectedMemberIds.add(userSector.opeUserId); debugPrint('Membre présélectionné: ${userSector.firstName} ${userSector.name} (userId: ${userSector.userId}, opeUserId: ${userSector.opeUserId}, 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(() { }); } catch (e) { debugPrint('Erreur lors du chargement des membres du secteur: $e'); setState(() { // Marquer comme terminé 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()}'; } // Dialogue de confirmation pour le changement du switch Future _showUpdatePassagesConfirmation(bool newValue) async { final result = await showDialog( context: context, builder: (context) => AlertDialog( title: Row( children: [ Icon( Icons.warning_amber_rounded, color: Colors.orange, size: 28, ), const SizedBox(width: 8), const Text('Confirmation'), ], ), content: Text( newValue ? 'Êtes-vous sûr de vouloir recalculer les passages ?\n\n' 'Tous les passages du secteur seront réaffectés selon les nouvelles limites.' : 'Êtes-vous sûr de vouloir conserver les passages existants ?\n\n' 'Les passages actuels ne seront pas modifiés même si les limites du secteur changent.', style: const TextStyle(fontSize: 15), ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(false), child: const Text('Annuler'), ), ElevatedButton( onPressed: () => Navigator.of(context).pop(true), style: ElevatedButton.styleFrom( backgroundColor: Colors.blue, foregroundColor: Colors.white, ), child: const Text('Confirmer'), ), ], ), ); return result ?? false; } 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, _updatePassages, // Passer le paramètre updatePassages ); // 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), // Switch pour la mise à jour des passages (uniquement en mode édition) if (widget.existingSector != null) ...[ Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), decoration: BoxDecoration( color: Colors.blue.shade50, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.blue.shade200), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Mise à jour des passages', style: TextStyle( fontWeight: FontWeight.bold, fontSize: 15, color: Colors.blue.shade900, ), ), const SizedBox(height: 4), Text( _updatePassages ? 'Les passages seront recalculés et réaffectés' : 'Les passages existants ne seront pas modifiés', style: TextStyle( fontSize: 12, color: Colors.grey.shade700, fontStyle: FontStyle.italic, ), ), ], ), ), Switch( value: _updatePassages, onChanged: (value) async { // Afficher confirmation uniquement si la valeur change par rapport à l'initiale if (value != _initialUpdatePassages) { final confirmed = await _showUpdatePassagesConfirmation(value); if (confirmed) { setState(() { _updatePassages = value; }); } } else { setState(() { _updatePassages = value; }); } }, activeColor: Colors.blue, ), ], ), ], ), ), 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 ==='); // Récupérer tous les membres (déjà uniques dans la box) // Filtrer uniquement ceux qui ont un opeUserId (dans l'opération courante) var membres = box.values .whereType() .where((membre) => membre.opeUserId != null) .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(); } // Trier : membres affectés en premier, puis les autres membres.sort((a, b) { final aSelected = _selectedMemberIds.contains(a.opeUserId); final bSelected = _selectedMemberIds.contains(b.opeUserId); // Si l'un est sélectionné et pas l'autre, le mettre en premier if (aSelected && !bSelected) return -1; if (!aSelected && bSelected) return 1; // Sinon, trier alphabétiquement par prénom puis nom final firstNameCompare = (a.firstName ?? '').compareTo(b.firstName ?? ''); if (firstNameCompare != 0) return firstNameCompare; return (a.name ?? '').compareTo(b.name ?? ''); }); 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 pour cette opération', 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.opeUserId); // Log pour debug if (index < 3) { // Limiter les logs aux 3 premiers membres debugPrint('Membre ${index}: ${membre.firstName} ${membre.name} (userId: ${membre.id}, opeUserId: ${membre.opeUserId}) - 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) { // opeUserId ne peut pas être null grâce au filtre ligne 517 _selectedMemberIds.add(membre.opeUserId!); } else { _selectedMemberIds.remove(membre.opeUserId!); } }); }, ); }, ), ), ), ], ); }, ), ), ], ), ), ), 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'), ), ], ); } }