import 'package:flutter/material.dart'; import '../services/chat_service.dart'; import '../services/chat_config_loader.dart'; /// Widget pour sélectionner les destinataires avec autocomplete /// Respecte les règles de permissions définies dans chat_config.yaml class RecipientSelector extends StatefulWidget { final Function(List>) onRecipientsSelected; final bool allowMultiple; const RecipientSelector({ super.key, required this.onRecipientsSelected, this.allowMultiple = false, }); @override State createState() => _RecipientSelectorState(); } class _RecipientSelectorState extends State { final _service = ChatService.instance; final _searchController = TextEditingController(); final _selectedRecipients = >[]; List> _suggestions = []; List> _allRecipients = []; // Liste complète pour la recherche locale bool _isLoading = false; @override void initState() { super.initState(); _loadInitialRecipients(); } Future _loadInitialRecipients() async { setState(() => _isLoading = true); try { final recipients = await _service.getPossibleRecipients(); setState(() { // Pour un admin (rôle 2), on filtre les super admins de la liste de sélection individuelle // et on exclut aussi l'utilisateur lui-même if (_service.currentUserRole == 2) { _allRecipients = recipients.where((r) => r['role'] != 9 && // Pas de super admins r['id'] != _service.currentUserId // Pas l'utilisateur lui-même ).toList(); } else { // Pour les autres rôles, on exclut juste l'utilisateur lui-même _allRecipients = recipients.where((r) => r['id'] != _service.currentUserId ).toList(); } _suggestions = _allRecipients; // Afficher tous les destinataires initialement _isLoading = false; }); } catch (e) { setState(() => _isLoading = false); } } Future _searchRecipients(String query) async { if (query.isEmpty) { setState(() { _suggestions = _allRecipients; }); return; } // Recherche locale sur les trois champs final searchQuery = query.toLowerCase().trim(); setState(() { _suggestions = _allRecipients.where((recipient) { // Récupérer les trois champs à rechercher final firstName = (recipient['first_name'] ?? '').toString().toLowerCase(); final lastName = (recipient['name'] ?? '').toString().toLowerCase(); final sectName = (recipient['sect_name'] ?? '').toString().toLowerCase(); // Rechercher dans les trois champs return firstName.contains(searchQuery) || lastName.contains(searchQuery) || sectName.contains(searchQuery); }).toList(); }); } void _toggleRecipient(Map recipient) { setState(() { if (widget.allowMultiple) { final exists = _selectedRecipients.any((r) => r['id'] == recipient['id']); if (exists) { _selectedRecipients.removeWhere((r) => r['id'] == recipient['id']); } else { _selectedRecipients.add(recipient); } } else { _selectedRecipients.clear(); _selectedRecipients.add(recipient); } }); widget.onRecipientsSelected(_selectedRecipients); } Widget _buildRoleBadge(int role) { final color = ChatConfigLoader.instance.getRoleColor(role); final name = ChatConfigLoader.instance.getRoleName(role); return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( color: _hexToColor(color).withOpacity(0.1), borderRadius: BorderRadius.circular(12), border: Border.all(color: _hexToColor(color).withOpacity(0.3)), ), child: Text( name, style: TextStyle( fontSize: 11, color: _hexToColor(color), fontWeight: FontWeight.w600, ), ), ); } Color _hexToColor(String hex) { final buffer = StringBuffer(); if (hex.length == 6 || hex.length == 7) buffer.write('ff'); buffer.write(hex.replaceFirst('#', '')); return Color(int.parse(buffer.toString(), radix: 16)); } @override Widget build(BuildContext context) { final currentRole = _service.currentUserRole; final config = ChatConfigLoader.instance.getPossibleRecipientsConfig(currentRole); final canSelect = config.any((c) => c['allow_selection'] == true); return Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ // En-tête avec options pour membre role 1 if (currentRole == 1) ...[ Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Sélectionner les destinataires', style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, ), ), const SizedBox(height: 12), Wrap( spacing: 8, runSpacing: 8, children: [ // Bouton Admin pour contacter tous les admins de l'amicale ActionChip( label: const Text('Administrateurs'), avatar: const Icon(Icons.admin_panel_settings, size: 18), backgroundColor: Colors.red.shade50, onPressed: () async { final allRecipients = await _service.getPossibleRecipients(); setState(() { _selectedRecipients.clear(); // Sélectionner tous les admins de l'amicale (role 2) _selectedRecipients.addAll( allRecipients.where((r) => r['role'] == 2) ); }); widget.onRecipientsSelected(_selectedRecipients); }, ), if (_selectedRecipients.isNotEmpty) ActionChip( label: Text('${_selectedRecipients.length} sélectionné(s)'), avatar: const Icon(Icons.check_circle, size: 18), backgroundColor: Colors.orange.shade50, onPressed: () { setState(() => _selectedRecipients.clear()); widget.onRecipientsSelected(_selectedRecipients); }, ), ], ), const SizedBox(height: 8), Text( 'Ou sélectionnez des membres individuellement :', style: TextStyle( fontSize: 13, color: Colors.grey[600], ), ), ], ), ), const Divider(height: 1), ], // En-tête avec options pour admin role 2 if (currentRole == 2) ...[ Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Sélectionner les destinataires', style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, ), ), const SizedBox(height: 12), Wrap( spacing: 8, runSpacing: 8, children: [ // Bouton GEOSECTOR pour contacter tous les super-admins ActionChip( label: const Text('GEOSECTOR'), avatar: const Icon(Icons.business, size: 18), backgroundColor: Colors.blue.shade50, onPressed: () async { final allRecipients = await _service.getPossibleRecipients(); setState(() { _selectedRecipients.clear(); // Sélectionner tous les super-admins (role 9) _selectedRecipients.addAll( allRecipients.where((r) => r['role'] == 9) ); }); widget.onRecipientsSelected(_selectedRecipients); }, ), // Bouton Amicale pour contacter tous les membres de son amicale ActionChip( label: const Text('Toute l\'Amicale'), avatar: const Icon(Icons.group, size: 18), backgroundColor: Colors.green.shade50, onPressed: () async { final allRecipients = await _service.getPossibleRecipients(); setState(() { _selectedRecipients.clear(); // Sélectionner tous les membres actifs de l'amicale (pas les super admins) // On inclut l'utilisateur lui-même pour qu'il reçoive aussi le message // L'API devrait déjà retourner tous les membres actifs de l'amicale (rôle 1 et 2) _selectedRecipients.addAll( allRecipients.where((r) => r['role'] != 9) ); // Si l'utilisateur n'est pas dans la liste, on l'ajoute if (!_selectedRecipients.any((r) => r['id'] == _service.currentUserId)) { // On utilise le nom complet fourni au service // TODO: Il faudrait passer prénom et nom séparément lors de l'init du ChatService _selectedRecipients.add({ 'id': _service.currentUserId, 'name': _service.currentUserName, 'first_name': '', 'role': _service.currentUserRole, }); } }); widget.onRecipientsSelected(_selectedRecipients); }, ), if (_selectedRecipients.isNotEmpty) ActionChip( label: Text('${_selectedRecipients.length} sélectionné(s)'), avatar: const Icon(Icons.check_circle, size: 18), backgroundColor: Colors.orange.shade50, onPressed: () { setState(() => _selectedRecipients.clear()); widget.onRecipientsSelected(_selectedRecipients); }, ), ], ), const SizedBox(height: 8), Text( 'Ou sélectionnez des membres individuellement :', style: TextStyle( fontSize: 13, color: Colors.grey[600], ), ), ], ), ), const Divider(height: 1), ], // En-tête avec options pour super-admin if (currentRole == 9) ...[ Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Sélectionner les destinataires', style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, ), ), const SizedBox(height: 8), Wrap( spacing: 8, children: [ // Bouton "Tous les admins" retiré - maintenant disponible via le bouton d'action rapide dans la liste des rooms if (_selectedRecipients.isNotEmpty) ActionChip( label: Text('${_selectedRecipients.length} sélectionné(s)'), avatar: const Icon(Icons.check_circle, size: 18), backgroundColor: Colors.green.shade50, onPressed: () { setState(() => _selectedRecipients.clear()); widget.onRecipientsSelected(_selectedRecipients); }, ), ], ), ], ), ), const Divider(height: 1), ], // Barre de recherche Padding( padding: const EdgeInsets.all(16), child: TextField( controller: _searchController, decoration: InputDecoration( hintText: 'Rechercher un destinataire...', prefixIcon: const Icon(Icons.search), border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: BorderSide(color: Colors.grey.shade300), ), contentPadding: const EdgeInsets.symmetric(horizontal: 16), ), onChanged: _searchRecipients, ), ), // Liste des suggestions Expanded( child: _isLoading ? const Center(child: CircularProgressIndicator()) : _suggestions.isEmpty ? Center( child: Text( ChatConfigLoader.instance.getUIMessages()['no_recipients'] ?? 'Aucun destinataire disponible', style: TextStyle(color: Colors.grey[600]), ), ) : ListView.builder( itemCount: _suggestions.length, itemBuilder: (context, index) { final recipient = _suggestions[index]; final isSelected = _selectedRecipients.any( (r) => r['id'] == recipient['id'] ); // Construire le nom complet (prénom + nom) final String firstName = recipient['first_name'] ?? ''; final String lastName = recipient['name'] ?? ''; final String fullName = '${firstName.trim()} ${lastName.trim()}'.trim(); final String displayName = fullName.isNotEmpty ? fullName : 'Sans nom'; // Utiliser sect_name s'il existe, sinon rien final String? sectName = recipient['sect_name']; final String? subtitle = sectName?.isNotEmpty == true ? sectName : null; // Initiales pour l'avatar (première lettre prénom + première lettre nom) String avatarLetters = ''; if (firstName.isNotEmpty) { avatarLetters += firstName.substring(0, 1).toUpperCase(); } if (lastName.isNotEmpty) { avatarLetters += lastName.substring(0, 1).toUpperCase(); } if (avatarLetters.isEmpty) { avatarLetters = '?'; } return ListTile( leading: CircleAvatar( backgroundColor: isSelected ? Theme.of(context).primaryColor : Colors.grey.shade200, child: Text( avatarLetters, style: TextStyle( color: isSelected ? Colors.white : Colors.grey[700], fontWeight: FontWeight.w600, fontSize: avatarLetters.length > 1 ? 14 : 16, ), ), ), title: Text( displayName, style: const TextStyle(fontWeight: FontWeight.w500), ), subtitle: subtitle != null ? Text( subtitle, style: TextStyle( fontSize: 13, color: Colors.grey[600], fontStyle: sectName?.isNotEmpty == true ? FontStyle.italic : FontStyle.normal, ), ) : null, trailing: Row( mainAxisSize: MainAxisSize.min, children: [ if (recipient['role'] != null) _buildRoleBadge(recipient['role']), if (widget.allowMultiple || canSelect) Checkbox( value: isSelected, onChanged: (_) => _toggleRecipient(recipient), ), ], ), onTap: () => _toggleRecipient(recipient), ); }, ), ), // Bouton de validation if (_selectedRecipients.isNotEmpty) Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, border: Border( top: BorderSide(color: Colors.grey.shade200), ), ), child: SizedBox( width: double.infinity, child: ElevatedButton( onPressed: () => Navigator.of(context).pop(_selectedRecipients), style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 12), backgroundColor: Theme.of(context).primaryColor, ), child: Text( widget.allowMultiple ? 'Créer conversation avec ${_selectedRecipients.length} personne(s)' : 'Créer conversation', style: const TextStyle( fontWeight: FontWeight.w600, color: Colors.white, ), ), ), ), ), ], ); } @override void dispose() { _searchController.dispose(); super.dispose(); } } /// Dialog pour sélectionner les destinataires class RecipientSelectorDialog extends StatelessWidget { final bool allowMultiple; const RecipientSelectorDialog({ super.key, this.allowMultiple = false, }); static Future?> show( BuildContext context, { bool allowMultiple = false, }) async { return showDialog>( context: context, builder: (context) => RecipientSelectorDialog( allowMultiple: allowMultiple, ), ); } @override Widget build(BuildContext context) { return Dialog( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), child: ConstrainedBox( constraints: BoxConstraints( maxHeight: MediaQuery.of(context).size.height * 0.8, maxWidth: 500, ), child: _RecipientSelectorWithMessage( allowMultiple: allowMultiple, ), ), ); } } /// Widget interne pour gérer la sélection et le message initial class _RecipientSelectorWithMessage extends StatefulWidget { final bool allowMultiple; const _RecipientSelectorWithMessage({ required this.allowMultiple, }); @override State<_RecipientSelectorWithMessage> createState() => _RecipientSelectorWithMessageState(); } class _RecipientSelectorWithMessageState extends State<_RecipientSelectorWithMessage> { List> _selectedRecipients = []; final _messageController = TextEditingController(); bool _isBroadcast = false; // Option broadcast pour super admins @override Widget build(BuildContext context) { return Column( mainAxisSize: MainAxisSize.min, children: [ // Sélecteur de destinataires Expanded( child: RecipientSelector( allowMultiple: widget.allowMultiple, onRecipientsSelected: (recipients) { setState(() { _selectedRecipients = recipients; }); }, ), ), // Champ de message initial si des destinataires sont sélectionnés if (_selectedRecipients.isNotEmpty) ...[ // Option broadcast pour super admins uniquement if (ChatService.instance.currentUserRole == 9) Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( color: Colors.amber.shade50, border: Border( top: BorderSide(color: Colors.grey.shade200), ), ), child: Row( children: [ Expanded( child: Row( children: [ Icon(Icons.campaign, size: 20, color: Colors.amber.shade700), const SizedBox(width: 8), Text( 'Mode Annonce (Broadcast)', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: Colors.amber.shade900, ), ), const SizedBox(width: 8), Expanded( child: Text( 'Les destinataires ne pourront pas répondre', style: TextStyle( fontSize: 12, color: Colors.amber.shade700, fontStyle: FontStyle.italic, ), ), ), ], ), ), Switch( value: _isBroadcast, onChanged: (value) { setState(() { _isBroadcast = value; }); }, activeColor: Colors.amber.shade600, ), ], ), ), Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.grey.shade50, border: Border( top: BorderSide(color: Colors.grey.shade200), ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( _isBroadcast ? 'Message de l\'annonce' : 'Message initial (optionnel)', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: _isBroadcast ? Colors.amber.shade900 : const Color(0xFF1E293B), ), ), const SizedBox(height: 8), TextField( controller: _messageController, decoration: InputDecoration( hintText: _isBroadcast ? 'Écrivez votre annonce officielle...' : 'Écrivez votre premier message...', hintStyle: TextStyle(color: Colors.grey[400]), filled: true, fillColor: Colors.white, border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: BorderSide( color: _isBroadcast ? Colors.amber.shade300 : Colors.grey.shade300, ), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: BorderSide( color: _isBroadcast ? Colors.amber.shade300 : Colors.grey.shade300, ), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: BorderSide( color: _isBroadcast ? Colors.amber.shade600 : Theme.of(context).primaryColor, width: 2, ), ), contentPadding: const EdgeInsets.symmetric( horizontal: 12, vertical: 10, ), ), maxLines: _isBroadcast ? 5 : 3, minLines: _isBroadcast ? 3 : 2, ), ], ), ), // Bouton de validation Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, border: Border( top: BorderSide(color: Colors.grey.shade200), ), ), child: SizedBox( width: double.infinity, child: ElevatedButton( onPressed: () { Navigator.of(context).pop({ 'recipients': _selectedRecipients, 'initial_message': _messageController.text.trim(), 'is_broadcast': _isBroadcast, // Ajouter le flag broadcast }); }, style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 12), backgroundColor: _isBroadcast ? Colors.amber.shade600 : Theme.of(context).primaryColor, ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ if (_isBroadcast) ...[ const Icon(Icons.campaign, color: Colors.white, size: 20), const SizedBox(width: 8), ], Text( _isBroadcast ? 'Envoyer l\'annonce à ${_selectedRecipients.length} admin(s)' : widget.allowMultiple ? 'Créer conversation avec ${_selectedRecipients.length} personne(s)' : 'Créer conversation', style: const TextStyle( fontWeight: FontWeight.w600, color: Colors.white, ), ), ], ), ), ), ), ], ], ); } @override void dispose() { _messageController.dispose(); super.dispose(); } }