import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:hive_flutter/hive_flutter.dart'; import '../models/room.dart'; import '../services/chat_service.dart'; import '../services/chat_config_loader.dart'; import '../widgets/recipient_selector.dart'; import 'chat_page.dart'; import 'package:geosector_app/presentation/widgets/loading_overlay.dart'; /// Version embarquée de RoomsPage sans AppBar pour intégration class RoomsPageEmbedded extends StatefulWidget { final VoidCallback? onAddPressed; final VoidCallback? onRefreshPressed; const RoomsPageEmbedded({ super.key, this.onAddPressed, this.onRefreshPressed, }); @override State createState() => RoomsPageEmbeddedState(); } class RoomsPageEmbeddedState extends State { final _service = ChatService.instance; bool _isLoading = false; // Pas de loading initial car déjà chargé au login bool _isProcessingAction = false; // Pour empêcher les doubles clics String? _selectedRoomId; // ID de la room sélectionnée (pour le web split-view) @override void initState() { super.initState(); // Les rooms sont déjà chargées au login et synchronisées toutes les 15 secondes } Future _loadRooms() async { setState(() => _isLoading = true); // Faire une sync incrémentale manuelle (pas complète) await _service.getRooms(forceFullSync: false); setState(() => _isLoading = false); widget.onRefreshPressed?.call(); } @override Widget build(BuildContext context) { if (_isLoading) { return const Center(child: CircularProgressIndicator()); } // Utiliser la vue split responsive pour toutes les plateformes return _buildResponsiveSplitView(context); } // Méthode publique pour rafraîchir void refresh() { _loadRooms(); } Future createNewConversation() async { final currentRole = _service.currentUserRole; final config = ChatConfigLoader.instance.getPossibleRecipientsConfig(currentRole); // Déterminer si on permet la sélection multiple // Pour role 1 (membre), permettre la sélection multiple pour contacter plusieurs membres/admins // Pour role 2 (admin amicale), permettre la sélection multiple pour GEOSECTOR ou Amicale // Pour role 9 (super admin), permettre la sélection multiple selon config final allowMultiple = (currentRole == 1) || (currentRole == 2) || (currentRole == 9 && config.any((c) => c['allow_selection'] == true)); // Ouvrir le dialog de sélection final result = await RecipientSelectorDialog.show( context, allowMultiple: allowMultiple, ); if (result != null) { final recipients = result['recipients'] as List>?; final initialMessage = result['initial_message'] as String?; final isBroadcast = result['is_broadcast'] as bool? ?? false; if (recipients != null && recipients.isNotEmpty) { try { Room? newRoom; if (recipients.length == 1) { // Conversation privée final recipient = recipients.first; // Construire le nom complet avec prénom + nom final firstName = recipient['first_name'] ?? ''; final lastName = recipient['name'] ?? ''; final fullName = '$firstName $lastName'.trim(); newRoom = await _service.createPrivateRoom( recipientId: recipient['id'], recipientName: fullName.isNotEmpty ? fullName : 'Sans nom', recipientRole: recipient['role'], recipientEntite: recipient['entite_id'], initialMessage: initialMessage, ); } else { // Conversation de groupe final participantIds = recipients.map((r) => r['id'] as int).toList(); // Déterminer le titre en fonction du type de groupe String title; if (currentRole == 1) { // Pour un membre final hasAdmins = recipients.any((r) => r['role'] == 2); final hasMembers = recipients.any((r) => r['role'] == 1); if (hasAdmins && !hasMembers) { title = 'Administrateurs Amicale'; } else if (recipients.length > 3) { title = '${recipients.take(3).map((r) { final firstName = r['first_name'] ?? ''; final lastName = r['name'] ?? ''; return '$firstName $lastName'.trim(); }).join(', ')} et ${recipients.length - 3} autres'; } else { title = recipients.map((r) { final firstName = r['first_name'] ?? ''; final lastName = r['name'] ?? ''; return '$firstName $lastName'.trim(); }).join(', '); } } else if (currentRole == 2) { // Pour un admin d'amicale final hasSuperAdmins = recipients.any((r) => r['role'] == 9); final hasMembers = recipients.any((r) => r['role'] == 1); if (hasSuperAdmins && !hasMembers) { title = 'Support GEOSECTOR'; } else if (!hasSuperAdmins && hasMembers && recipients.length > 5) { title = 'Toute l\'Amicale'; } else if (recipients.length > 3) { title = '${recipients.take(3).map((r) { final firstName = r['first_name'] ?? ''; final lastName = r['name'] ?? ''; return '$firstName $lastName'.trim(); }).join(', ')} et ${recipients.length - 3} autres'; } else { title = recipients.map((r) { final firstName = r['first_name'] ?? ''; final lastName = r['name'] ?? ''; return '$firstName $lastName'.trim(); }).join(', '); } } else { // Pour un super admin if (recipients.length > 3) { title = '${recipients.take(3).map((r) { final firstName = r['first_name'] ?? ''; final lastName = r['name'] ?? ''; return '$firstName $lastName'.trim(); }).join(', ')} et ${recipients.length - 3} autres'; } else { title = recipients.map((r) { final firstName = r['first_name'] ?? ''; final lastName = r['name'] ?? ''; return '$firstName $lastName'.trim(); }).join(', '); } } // Créer la room avec le bon type (broadcast si coché, sinon group) newRoom = await _service.createRoom( title: title, participantIds: participantIds, type: isBroadcast ? 'broadcast' : 'group', initialMessage: initialMessage, ); } if (newRoom != null && mounted) { // Sur le web, sélectionner la room, sur mobile naviguer if (kIsWeb) { setState(() { _selectedRoomId = newRoom?.id; }); } else { Navigator.push( context, MaterialPageRoute( builder: (_) => ChatPage( roomId: newRoom!.id, roomTitle: newRoom.title, roomType: newRoom.type, roomCreatorId: newRoom.createdBy, ), ), ); } } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(e.toString()), backgroundColor: Colors.red, ), ); } } } } } /// Méthode pour créer la vue split responsive Widget _buildResponsiveSplitView(BuildContext context) { return ValueListenableBuilder>( valueListenable: _service.roomsBox.listenable(), builder: (context, box, _) { // Récupérer la room sélectionnée depuis Hive avec l'ID stocké final selectedRoom = _selectedRoomId != null ? box.values.firstWhere( (r) => r.id == _selectedRoomId, orElse: () { // Si la room n'existe plus, réinitialiser la sélection WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { setState(() { _selectedRoomId = null; }); } }); return Room( id: '', title: '', type: 'private', createdAt: DateTime.now(), ); }, ) : null; // Déterminer si on est sur un petit écran final screenWidth = MediaQuery.of(context).size.width; final screenHeight = MediaQuery.of(context).size.height; final isSmallScreen = screenWidth < 900; // Si petit écran ou mobile, disposition verticale if (isSmallScreen) { // Calculer la hauteur appropriée pour la liste des rooms // Sur mobile, utiliser 30% de la hauteur, sur web small screen 250px final roomsHeight = kIsWeb ? 250.0 : screenHeight * 0.3; return Column( children: [ // Liste des rooms en haut avec hauteur adaptative Container( height: roomsHeight.clamp(200.0, 350.0), // Entre 200 et 350 pixels decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, border: Border( bottom: BorderSide( color: Theme.of(context).dividerColor, width: 1, ), ), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 4, offset: const Offset(0, 2), ), ], ), child: _buildRoomsList(context), ), // Conversation sélectionnée en dessous (reste de l'espace) Expanded( child: selectedRoom != null && selectedRoom.id.isNotEmpty ? ChatPage( key: ValueKey(selectedRoom.id), // Clé unique par room roomId: selectedRoom.id, roomTitle: selectedRoom.title, roomType: selectedRoom.type, roomCreatorId: selectedRoom.createdBy, isEmbedded: true, // Pour indiquer qu'on est en mode embedded ) : _buildEmptyConversation(context), ), ], ); } // Si grand écran, disposition horizontale (comme avant) return Row( children: [ // Colonne de gauche : Liste des rooms (30%) Container( width: MediaQuery.of(context).size.width * 0.3, constraints: const BoxConstraints( minWidth: 280, maxWidth: 400, ), decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, border: Border( right: BorderSide( color: Theme.of(context).dividerColor, width: 1, ), ), ), child: _buildRoomsList(context), ), // Colonne de droite : Conversation sélectionnée (70%) Expanded( child: selectedRoom != null && selectedRoom.id.isNotEmpty ? ChatPage( key: ValueKey(selectedRoom.id), // Clé unique par room roomId: selectedRoom.id, roomTitle: selectedRoom.title, roomType: selectedRoom.type, roomCreatorId: selectedRoom.createdBy, isEmbedded: true, // Pour indiquer qu'on est en mode embedded ) : _buildEmptyConversation(context), ), ], ); }, ); } /// Liste des rooms pour le web Widget _buildRoomsList(BuildContext context) { final helpText = ChatConfigLoader.instance.getHelpText(_service.currentUserRole); final userRole = _service.currentUserRole; return Column( children: [ // En-tête avec boutons d'action Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.white, border: Border( bottom: BorderSide(color: Colors.grey[200]!), ), ), child: Column( children: [ // Titre de la section Row( children: [ Icon(Icons.chat, size: 20, color: Colors.grey[700]), const SizedBox(width: 8), Text( 'Conversations', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: Colors.grey[800], ), ), const Spacer(), // Bouton nouvelle conversation IconButton( icon: const Icon(Icons.add_circle_outline), onPressed: createNewConversation, tooltip: 'Nouvelle conversation', color: Colors.blue[600], ), ], ), // Boutons d'action rapide selon le rôle if (userRole == 9) ...[ // Super Admin - Bouton pour contacter tous les admins d'amicale const SizedBox(height: 8), SizedBox( width: double.infinity, child: ElevatedButton.icon( icon: const Icon(Icons.groups, size: 16), label: const Text('Tous les admins d\'amicale'), style: ElevatedButton.styleFrom( backgroundColor: Colors.amber[600], foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 10), ), onPressed: _isProcessingAction ? null : () => _handleQuickActionAllAdmins(), ), ), ] else if (userRole == 2) ...[ const SizedBox(height: 8), ValueListenableBuilder>( valueListenable: _service.roomsBox.listenable(), builder: (context, box, _) { // Vérifier l'existence des rooms spéciales final hasAmicaleRoom = box.values.any((room) => room.title == 'Toute l\'Amicale' ); final hasGeosectorRoom = box.values.any((room) => room.title == 'Support GEOSECTOR' ); return Row( children: [ Expanded( child: ElevatedButton.icon( icon: Icon( hasAmicaleRoom ? Icons.check : Icons.group, size: 16 ), label: Text( hasAmicaleRoom ? 'Amicale (actif)' : 'Toute l\'Amicale', style: const TextStyle(fontSize: 12) ), style: ElevatedButton.styleFrom( backgroundColor: hasAmicaleRoom ? Colors.grey[400] : Colors.green[600], foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 8), ), onPressed: (hasAmicaleRoom || _isProcessingAction) ? null : () => _handleQuickActionAmicale(), ), ), const SizedBox(width: 8), Expanded( child: ElevatedButton.icon( icon: Icon( hasGeosectorRoom ? Icons.check : Icons.support_agent, size: 16 ), label: Text( hasGeosectorRoom ? 'Support (actif)' : 'Support GEOSECTOR', style: const TextStyle(fontSize: 12) ), style: ElevatedButton.styleFrom( backgroundColor: hasGeosectorRoom ? Colors.grey[400] : Colors.blue[600], foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 8), ), onPressed: (hasGeosectorRoom || _isProcessingAction) ? null : () => _handleQuickActionGeosector(), ), ), ], ); }, ), ] else if (userRole == 1) ...[ const SizedBox(height: 8), SizedBox( width: double.infinity, child: ElevatedButton.icon( icon: const Icon(Icons.admin_panel_settings, size: 16), label: const Text('Contacter les admins'), style: ElevatedButton.styleFrom( backgroundColor: Colors.orange[600], foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 10), ), onPressed: _isProcessingAction ? null : () => _handleQuickActionAdmins(), ), ), ], ], ), ), // Liste des conversations Expanded( child: ValueListenableBuilder>( valueListenable: _service.roomsBox.listenable(), builder: (context, box, _) { final rooms = box.values.toList() ..sort((a, b) => (b.lastMessageAt ?? b.createdAt) .compareTo(a.lastMessageAt ?? a.createdAt)); // Vérifier si la room sélectionnée existe encore if (_selectedRoomId != null && !rooms.any((r) => r.id == _selectedRoomId)) { // La room sélectionnée a été supprimée, la désélectionner WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { setState(() { _selectedRoomId = null; }); } }); } if (rooms.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.chat_bubble_outline, size: 48, color: Colors.grey[400], ), const SizedBox(height: 16), Text( 'Aucune conversation', style: TextStyle( fontSize: 14, color: Colors.grey[600], ), ), if (helpText.isNotEmpty) Padding( padding: const EdgeInsets.all(8), child: Text( helpText, style: TextStyle( fontSize: 12, color: Colors.grey[500], ), textAlign: TextAlign.center, ), ), ], ), ); } return ListView.builder( itemCount: rooms.length, itemBuilder: (context, index) { final room = rooms[index]; final isSelected = _selectedRoomId == room.id; return Container( decoration: BoxDecoration( color: isSelected ? Colors.blue.shade50 : Colors.white, border: Border( bottom: BorderSide(color: Colors.grey[200]!), ), ), child: _WebRoomTile( room: room, isSelected: isSelected, currentUserId: _service.currentUserId, onTap: () { setState(() { _selectedRoomId = room.id; }); }, onDelete: () { debugPrint('🗑️ Clic suppression: room.createdBy=${room.createdBy}, currentUserId=${_service.currentUserId}'); _handleDeleteRoom(room); }, ), ); }, ); }, ), ), ], ); } /// Vue vide quand aucune conversation n'est sélectionnée Widget _buildEmptyConversation(BuildContext context) { return Container( color: Colors.grey[50], child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.chat, size: 64, color: Colors.grey[300], ), const SizedBox(height: 16), Text( 'Sélectionnez une conversation', style: TextStyle( fontSize: 18, color: Colors.grey[500], ), ), const SizedBox(height: 8), Text( 'ou créez-en une nouvelle', style: TextStyle( fontSize: 14, color: Colors.grey[400], ), ), ], ), ), ); } /// Action rapide : Contacter toute l'amicale (pour admin) Future _handleQuickActionAmicale() async { // Protection contre les doubles clics if (_isProcessingAction) return; setState(() { _isProcessingAction = true; }); try { // Utiliser LoadingOverlay pour l'opération longue await LoadingOverlay.show( context: context, message: 'Création de la conversation avec toute l\'amicale...', future: _createAmicaleRoom(), ); } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Erreur: ${e.toString()}'), backgroundColor: Colors.red, ), ); } } finally { if (mounted) { setState(() { _isProcessingAction = false; }); } } } Future _createAmicaleRoom() async { try { // Récupérer tous les membres de l'amicale via l'API final recipients = await _service.getPossibleRecipients(); // Filtrer pour ne garder que les membres de l'amicale (role 1 et 2) final amicaleMembers = recipients.where((r) => r['role'] == 1 || r['role'] == 2 ).toList(); if (amicaleMembers.isEmpty) { throw Exception('Aucun membre trouvé dans l\'amicale'); } // Créer une conversation de groupe avec tous les membres final participantIds = amicaleMembers.map((r) => r['id'] as int).toList(); final newRoom = await _service.createRoom( title: 'Toute l\'Amicale', participantIds: participantIds, type: 'group', initialMessage: null, ); if (newRoom != null && mounted) { setState(() { _selectedRoomId = newRoom.id; }); } } catch (e) { rethrow; } } /// Action rapide : Contacter GEOSECTOR (pour admin) Future _handleQuickActionGeosector() async { // Protection contre les doubles clics if (_isProcessingAction) return; setState(() { _isProcessingAction = true; }); try { // Utiliser LoadingOverlay pour l'opération longue await LoadingOverlay.show( context: context, message: 'Création de la conversation avec le support GEOSECTOR...', future: _createGeosectorRoom(), ); } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Erreur: ${e.toString()}'), backgroundColor: Colors.red, ), ); } } finally { if (mounted) { setState(() { _isProcessingAction = false; }); } } } Future _createGeosectorRoom() async { try { // Récupérer tous les destinataires via l'API final recipients = await _service.getPossibleRecipients(); // Filtrer pour ne garder que les super admins (role 9) final superAdmins = recipients.where((r) => r['role'] == 9).toList(); if (superAdmins.isEmpty) { throw Exception('Aucun super admin disponible'); } // Créer une conversation avec les super admins if (superAdmins.length == 1) { // Conversation privée si un seul super admin final admin = superAdmins.first; final firstName = admin['first_name'] ?? ''; final lastName = admin['name'] ?? ''; final fullName = '$firstName $lastName'.trim(); final newRoom = await _service.createPrivateRoom( recipientId: admin['id'], recipientName: fullName.isNotEmpty ? fullName : 'Super Admin', recipientRole: admin['role'], recipientEntite: admin['entite_id'], initialMessage: null, ); if (newRoom != null && mounted) { setState(() { _selectedRoomId = newRoom.id; }); } } else { // Conversation de groupe si plusieurs super admins final participantIds = superAdmins.map((r) => r['id'] as int).toList(); final newRoom = await _service.createRoom( title: 'Support GEOSECTOR', participantIds: participantIds, type: 'group', initialMessage: null, ); if (newRoom != null && mounted) { setState(() { _selectedRoomId = newRoom.id; }); } } } catch (e) { rethrow; } } /// Supprimer une room Future _handleDeleteRoom(Room room) async { debugPrint('🚀 _handleDeleteRoom appelée: room.createdBy=${room.createdBy}, currentUserId=${_service.currentUserId}'); // Vérifier que l'utilisateur est bien le créateur if (room.createdBy != _service.currentUserId) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Vous ne pouvez supprimer que les conversations que vous avez créées'), backgroundColor: Colors.orange, ), ); } return; } // Demander confirmation final confirm = await showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Supprimer la conversation'), content: Text('Voulez-vous vraiment supprimer la conversation "${room.title}" ?'), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), child: const Text('Annuler'), ), TextButton( onPressed: () => Navigator.pop(context, true), style: TextButton.styleFrom(foregroundColor: Colors.red), child: const Text('Supprimer'), ), ], ), ); if (confirm == true) { try { await _service.deleteRoom(room.id); // Si la room supprimée était sélectionnée, désélectionner if (_selectedRoomId == room.id) { setState(() { _selectedRoomId = null; }); } if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Conversation supprimée'), backgroundColor: Colors.green, ), ); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Erreur: ${e.toString()}'), backgroundColor: Colors.red, ), ); } } } } /// Action rapide : Contacter les admins (pour membre) Future _handleQuickActionAdmins() async { // Protection contre les doubles clics if (_isProcessingAction) return; setState(() { _isProcessingAction = true; }); try { // Utiliser LoadingOverlay pour l'opération longue await LoadingOverlay.show( context: context, message: 'Création de la conversation avec les administrateurs...', future: _createAdminsRoom(), ); } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Erreur: ${e.toString()}'), backgroundColor: Colors.red, ), ); } } finally { if (mounted) { setState(() { _isProcessingAction = false; }); } } } Future _createAdminsRoom() async { try { // Récupérer tous les destinataires via l'API final recipients = await _service.getPossibleRecipients(); // Filtrer pour ne garder que les admins de l'amicale (role 2) final admins = recipients.where((r) => r['role'] == 2).toList(); if (admins.isEmpty) { throw Exception('Aucun administrateur disponible'); } // Créer une conversation avec les admins if (admins.length == 1) { // Conversation privée si un seul admin final admin = admins.first; final firstName = admin['first_name'] ?? ''; final lastName = admin['name'] ?? ''; final fullName = '$firstName $lastName'.trim(); final newRoom = await _service.createPrivateRoom( recipientId: admin['id'], recipientName: fullName.isNotEmpty ? fullName : 'Admin Amicale', recipientRole: admin['role'], recipientEntite: admin['entite_id'], initialMessage: null, ); if (newRoom != null && mounted) { setState(() { _selectedRoomId = newRoom.id; }); } } else { // Conversation de groupe si plusieurs admins final participantIds = admins.map((r) => r['id'] as int).toList(); final newRoom = await _service.createRoom( title: 'Administrateurs Amicale', participantIds: participantIds, type: 'group', initialMessage: null, ); if (newRoom != null && mounted) { setState(() { _selectedRoomId = newRoom.id; }); } } } catch (e) { rethrow; } } /// Action rapide pour Super Admin - Contacter tous les admins d'amicale Future _handleQuickActionAllAdmins() async { // Protection contre les doubles clics if (_isProcessingAction) return; setState(() { _isProcessingAction = true; }); try { // Utiliser LoadingOverlay pour l'opération longue await LoadingOverlay.show( context: context, message: 'Création de l\'annonce pour tous les administrateurs...', future: _createAllAdminsRoom(), ); } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Erreur: ${e.toString()}'), backgroundColor: Colors.red, ), ); } } finally { if (mounted) { setState(() { _isProcessingAction = false; }); } } } Future _createAllAdminsRoom() async { try { // Récupérer tous les destinataires via l'API final recipients = await _service.getPossibleRecipients(); // Filtrer pour ne garder que les admins d'amicale (role 2) final allAdmins = recipients.where((r) => r['role'] == 2).toList(); if (allAdmins.isEmpty) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Aucun administrateur d\'amicale trouvé'), backgroundColor: Colors.orange, ), ); } return; } // Ouvrir le dialog de création avec les admins pré-sélectionnés // et l'option broadcast activée par défaut pour les annonces if (!mounted) return; final result = await showDialog>( context: context, builder: (dialogContext) => Dialog( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), child: ConstrainedBox( constraints: BoxConstraints( maxHeight: MediaQuery.of(context).size.height * 0.8, maxWidth: 500, ), child: _QuickBroadcastDialog( recipients: allAdmins, title: 'Annonce à tous les administrateurs', ), ), ), ); if (result != null) { final initialMessage = result['message'] as String?; final isBroadcast = result['broadcast'] as bool? ?? true; // Créer les IDs des participants final participantIds = allAdmins.map((r) => r['id'] as int).toList(); // Créer la room broadcast final newRoom = await _service.createRoom( title: 'Annonce GEOSECTOR', participantIds: participantIds, type: isBroadcast ? 'broadcast' : 'group', initialMessage: initialMessage, ); if (newRoom != null && mounted) { setState(() { _selectedRoomId = newRoom.id; }); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( isBroadcast ? '📢 Annonce envoyée à ${allAdmins.length} administrateur(s)' : 'Conversation créée avec ${allAdmins.length} administrateur(s)' ), backgroundColor: isBroadcast ? Colors.amber[600] : Colors.green, ), ); } } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Erreur: ${e.toString()}'), backgroundColor: Colors.red, ), ); } } } } /// Dialog rapide pour envoyer un broadcast class _QuickBroadcastDialog extends StatefulWidget { final List> recipients; final String title; const _QuickBroadcastDialog({ required this.recipients, required this.title, }); @override State<_QuickBroadcastDialog> createState() => _QuickBroadcastDialogState(); } class _QuickBroadcastDialogState extends State<_QuickBroadcastDialog> { final _messageController = TextEditingController(); bool _isBroadcast = true; // Par défaut en mode broadcast pour super admin @override Widget build(BuildContext context) { return Column( mainAxisSize: MainAxisSize.min, children: [ // En-tête Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.amber.shade50, borderRadius: const BorderRadius.only( topLeft: Radius.circular(12), topRight: Radius.circular(12), ), ), child: Row( children: [ Icon(Icons.campaign, color: Colors.amber.shade700), const SizedBox(width: 12), Expanded( child: Text( widget.title, style: TextStyle( fontSize: 18, fontWeight: FontWeight.w600, color: Colors.amber.shade900, ), ), ), IconButton( icon: const Icon(Icons.close), onPressed: () => Navigator.of(context).pop(), ), ], ), ), // Corps Expanded( child: SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Info destinataires Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.blue.shade50, borderRadius: BorderRadius.circular(8), ), child: Row( children: [ Icon(Icons.group, size: 20, color: Colors.blue.shade700), const SizedBox(width: 8), Expanded( child: Text( '${widget.recipients.length} administrateur(s) d\'amicale', style: TextStyle( fontSize: 14, color: Colors.blue.shade900, ), ), ), ], ), ), const SizedBox(height: 16), // Switch broadcast Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: _isBroadcast ? Colors.amber.shade50 : Colors.grey.shade50, borderRadius: BorderRadius.circular(8), border: Border.all( color: _isBroadcast ? Colors.amber.shade300 : Colors.grey.shade300, ), ), child: Row( children: [ Icon( _isBroadcast ? Icons.campaign : Icons.chat, color: _isBroadcast ? Colors.amber.shade700 : Colors.grey.shade700, ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( _isBroadcast ? 'Mode Annonce (Broadcast)' : 'Mode Discussion', style: TextStyle( fontWeight: FontWeight.w600, color: _isBroadcast ? Colors.amber.shade900 : Colors.grey.shade900, ), ), Text( _isBroadcast ? 'Les destinataires ne pourront pas répondre' : 'Tous les participants peuvent discuter', style: TextStyle( fontSize: 12, color: _isBroadcast ? Colors.amber.shade700 : Colors.grey.shade600, ), ), ], ), ), Switch( value: _isBroadcast, onChanged: (value) { setState(() { _isBroadcast = value; }); }, thumbColor: WidgetStateProperty.resolveWith((states) { if (states.contains(WidgetState.selected)) { return Colors.amber.shade600; } return null; }), ), ], ), ), const SizedBox(height: 16), // Champ message Text( _isBroadcast ? 'Votre annonce' : 'Message initial (optionnel)', style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 8), TextField( controller: _messageController, decoration: InputDecoration( hintText: _isBroadcast ? 'Écrivez votre annonce officielle...' : 'Écrivez votre message...', border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), ), filled: true, fillColor: Colors.white, ), maxLines: 5, minLines: 3, ), ], ), ), ), // Boutons Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.grey.shade50, border: Border( top: BorderSide(color: Colors.grey.shade200), ), ), child: Row( children: [ Expanded( child: TextButton( onPressed: () => Navigator.of(context).pop(), child: const Text('Annuler'), ), ), const SizedBox(width: 8), Expanded( child: ElevatedButton.icon( onPressed: () { Navigator.of(context).pop({ 'message': _messageController.text.trim(), 'broadcast': _isBroadcast, }); }, icon: Icon(_isBroadcast ? Icons.campaign : Icons.send), label: Text(_isBroadcast ? 'Envoyer l\'annonce' : 'Créer la discussion'), style: ElevatedButton.styleFrom( backgroundColor: _isBroadcast ? Colors.amber.shade600 : Colors.blue, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(vertical: 12), ), ), ), ], ), ), ], ); } @override void dispose() { _messageController.dispose(); super.dispose(); } } /// Widget spécifique pour les tuiles de room sur le web class _WebRoomTile extends StatelessWidget { final Room room; final bool isSelected; final int currentUserId; final VoidCallback onTap; final VoidCallback onDelete; const _WebRoomTile({ required this.room, required this.isSelected, required this.currentUserId, required this.onTap, required this.onDelete, }); @override Widget build(BuildContext context) { debugPrint('🔍 _WebRoomTile pour ${room.title}: createdBy=${room.createdBy}, currentUserId=$currentUserId, showDelete=${room.createdBy == currentUserId}'); return ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), leading: CircleAvatar( radius: 18, backgroundColor: room.type == 'broadcast' ? (isSelected ? Colors.amber.shade600 : Colors.amber.shade400) : (isSelected ? const Color(0xFF2563EB) : Colors.grey[400]), child: room.type == 'broadcast' ? const Icon(Icons.campaign, color: Colors.white, size: 16) : Text( _getInitials(room.title), style: const TextStyle(color: Colors.white, fontSize: 12), ), ), title: Row( children: [ Expanded( child: Text( room.title, style: TextStyle( fontWeight: isSelected ? FontWeight.w700 : FontWeight.w600, fontSize: 14, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), if (room.type == 'broadcast') Container( margin: const EdgeInsets.only(left: 4), padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1), decoration: BoxDecoration( color: Colors.amber.shade100, borderRadius: BorderRadius.circular(6), ), child: Text( 'ANN.', style: TextStyle( fontSize: 8, fontWeight: FontWeight.bold, color: Colors.amber.shade800, ), ), ), ], ), subtitle: room.lastMessage != null ? Text( room.lastMessage!, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( color: Colors.grey[600], fontSize: 12, ), ) : null, trailing: Row( mainAxisSize: MainAxisSize.min, children: [ Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.end, children: [ if (room.lastMessageAt != null) Text( _formatTime(room.lastMessageAt!), style: TextStyle( fontSize: 11, color: Colors.grey[500], ), ), if (room.unreadCount > 0) Container( margin: const EdgeInsets.only(top: 2), padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1), decoration: BoxDecoration( color: const Color(0xFF2563EB), borderRadius: BorderRadius.circular(8), ), child: Text( room.unreadCount.toString(), style: const TextStyle( color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold, ), ), ), ], ), // Bouton de suppression si l'utilisateur est le créateur if (room.createdBy == currentUserId) ...[ const SizedBox(width: 8), IconButton( icon: Icon( Icons.delete_outline, size: 18, color: Colors.red[400], ), onPressed: onDelete, tooltip: 'Supprimer la conversation', padding: EdgeInsets.zero, constraints: const BoxConstraints( minWidth: 32, minHeight: 32, ), ), ], ], ), onTap: onTap, ); } String _formatTime(DateTime date) { final now = DateTime.now(); final diff = now.difference(date); if (diff.inDays > 0) { return '${diff.inDays}j'; } else if (diff.inHours > 0) { return '${diff.inHours}h'; } else if (diff.inMinutes > 0) { return '${diff.inMinutes}m'; } else { return 'Maintenant'; } } String _getInitials(String title) { // Pour les titres spéciaux, retourner des initiales appropriées if (title == 'Support GEOSECTOR') return 'SG'; if (title == 'Toute l\'Amicale') return 'TA'; if (title == 'Administrateurs Amicale') return 'AA'; // Pour les noms de personnes, extraire les initiales final words = title.split(' ').where((w) => w.isNotEmpty).toList(); if (words.isEmpty) return '?'; // Si c'est un seul mot, prendre les 2 premières lettres if (words.length == 1) { final word = words[0]; return word.length >= 2 ? '${word[0]}${word[1]}'.toUpperCase() : word[0].toUpperCase(); } // Si c'est prénom + nom, prendre la première lettre de chaque if (words.length == 2) { return '${words[0][0]}${words[1][0]}'.toUpperCase(); } // Pour les groupes avec plusieurs noms, prendre les 2 premières initiales return '${words[0][0]}${words[1][0]}'.toUpperCase(); } }