feat: synchronisation mode deconnecte fin chat et stats
This commit is contained in:
@@ -24,6 +24,7 @@ class _RecipientSelectorState extends State<RecipientSelector> {
|
||||
final _selectedRecipients = <Map<String, dynamic>>[];
|
||||
|
||||
List<Map<String, dynamic>> _suggestions = [];
|
||||
List<Map<String, dynamic>> _allRecipients = []; // Liste complète pour la recherche locale
|
||||
bool _isLoading = false;
|
||||
|
||||
@override
|
||||
@@ -38,7 +39,20 @@ class _RecipientSelectorState extends State<RecipientSelector> {
|
||||
try {
|
||||
final recipients = await _service.getPossibleRecipients();
|
||||
setState(() {
|
||||
_suggestions = recipients;
|
||||
// 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) {
|
||||
@@ -47,22 +61,29 @@ class _RecipientSelectorState extends State<RecipientSelector> {
|
||||
}
|
||||
|
||||
Future<void> _searchRecipients(String query) async {
|
||||
if (query.length < 2) {
|
||||
_loadInitialRecipients();
|
||||
if (query.isEmpty) {
|
||||
setState(() {
|
||||
_suggestions = _allRecipients;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() => _isLoading = true);
|
||||
// Recherche locale sur les trois champs
|
||||
final searchQuery = query.toLowerCase().trim();
|
||||
|
||||
try {
|
||||
final recipients = await _service.getPossibleRecipients(search: query);
|
||||
setState(() {
|
||||
_suggestions = recipients;
|
||||
_isLoading = false;
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() => _isLoading = false);
|
||||
}
|
||||
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<String, dynamic> recipient) {
|
||||
@@ -116,7 +137,6 @@ class _RecipientSelectorState extends State<RecipientSelector> {
|
||||
Widget build(BuildContext context) {
|
||||
final currentRole = _service.currentUserRole;
|
||||
final config = ChatConfigLoader.instance.getPossibleRecipientsConfig(currentRole);
|
||||
final canBroadcast = config.any((c) => c['allow_broadcast'] == true);
|
||||
final canSelect = config.any((c) => c['allow_selection'] == true);
|
||||
|
||||
return Column(
|
||||
@@ -227,10 +247,23 @@ class _RecipientSelectorState extends State<RecipientSelector> {
|
||||
final allRecipients = await _service.getPossibleRecipients();
|
||||
setState(() {
|
||||
_selectedRecipients.clear();
|
||||
// Sélectionner tous les membres de l'amicale (role 1)
|
||||
// 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'] == 1)
|
||||
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);
|
||||
},
|
||||
@@ -277,21 +310,7 @@ class _RecipientSelectorState extends State<RecipientSelector> {
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
children: [
|
||||
if (canBroadcast)
|
||||
ActionChip(
|
||||
label: const Text('Tous les admins'),
|
||||
avatar: const Icon(Icons.groups, size: 18),
|
||||
onPressed: () async {
|
||||
final allAdmins = await _service.getPossibleRecipients();
|
||||
setState(() {
|
||||
_selectedRecipients.clear();
|
||||
_selectedRecipients.addAll(
|
||||
allAdmins.where((r) => r['role'] == 2)
|
||||
);
|
||||
});
|
||||
widget.onRecipientsSelected(_selectedRecipients);
|
||||
},
|
||||
),
|
||||
// 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)'),
|
||||
@@ -316,8 +335,7 @@ class _RecipientSelectorState extends State<RecipientSelector> {
|
||||
child: TextField(
|
||||
controller: _searchController,
|
||||
decoration: InputDecoration(
|
||||
hintText: ChatConfigLoader.instance.getUIMessages()['search_placeholder']
|
||||
?? 'Rechercher...',
|
||||
hintText: 'Rechercher un destinataire...',
|
||||
prefixIcon: const Icon(Icons.search),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
@@ -349,29 +367,57 @@ class _RecipientSelectorState extends State<RecipientSelector> {
|
||||
(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(
|
||||
recipient['name']?.substring(0, 1).toUpperCase() ?? '?',
|
||||
avatarLetters,
|
||||
style: TextStyle(
|
||||
color: isSelected ? Colors.white : Colors.grey[700],
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: avatarLetters.length > 1 ? 14 : 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
recipient['name'] ?? 'Sans nom',
|
||||
displayName,
|
||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||
),
|
||||
subtitle: recipient['entite_name'] != null
|
||||
subtitle: subtitle != null
|
||||
? Text(
|
||||
recipient['entite_name'],
|
||||
subtitle,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Colors.grey[600],
|
||||
fontStyle: sectName?.isNotEmpty == true
|
||||
? FontStyle.italic
|
||||
: FontStyle.normal,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
@@ -489,6 +535,7 @@ class _RecipientSelectorWithMessage extends StatefulWidget {
|
||||
class _RecipientSelectorWithMessageState extends State<_RecipientSelectorWithMessage> {
|
||||
List<Map<String, dynamic>> _selectedRecipients = [];
|
||||
final _messageController = TextEditingController();
|
||||
bool _isBroadcast = false; // Option broadcast pour super admins
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -508,7 +555,59 @@ class _RecipientSelectorWithMessageState extends State<_RecipientSelectorWithMes
|
||||
),
|
||||
|
||||
// Champ de message initial si des destinataires sont sélectionnés
|
||||
if (_selectedRecipients.isNotEmpty) ...[
|
||||
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(
|
||||
@@ -520,33 +619,50 @@ class _RecipientSelectorWithMessageState extends State<_RecipientSelectorWithMes
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Message initial (optionnel)',
|
||||
Text(
|
||||
_isBroadcast ? 'Message de l\'annonce' : 'Message initial (optionnel)',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF1E293B),
|
||||
color: _isBroadcast ? Colors.amber.shade900 : const Color(0xFF1E293B),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
controller: _messageController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Écrivez votre premier message...',
|
||||
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: Colors.grey.shade300),
|
||||
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: 3,
|
||||
minLines: 2,
|
||||
maxLines: _isBroadcast ? 5 : 3,
|
||||
minLines: _isBroadcast ? 3 : 2,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -568,20 +684,34 @@ class _RecipientSelectorWithMessageState extends State<_RecipientSelectorWithMes
|
||||
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: Theme.of(context).primaryColor,
|
||||
backgroundColor: _isBroadcast
|
||||
? Colors.amber.shade600
|
||||
: 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,
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user