feat: synchronisation mode deconnecte fin chat et stats

This commit is contained in:
2025-08-31 18:21:20 +02:00
parent 41a4505b4b
commit 604294af96
149 changed files with 285769 additions and 250633 deletions

View File

@@ -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,
),
),
],
),
),
),