- Mise à jour VERSION vers 3.3.4 - Optimisations et révisions architecture API (deploy-api.sh, scripts de migration) - Ajout documentation Stripe Tap to Pay complète - Migration vers polices Inter Variable pour Flutter - Optimisations build Android et nettoyage fichiers temporaires - Amélioration système de déploiement avec gestion backups - Ajout scripts CRON et migrations base de données 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1434 lines
49 KiB
Dart
1434 lines
49 KiB
Dart
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<RoomsPageEmbedded> createState() => RoomsPageEmbeddedState();
|
|
}
|
|
|
|
class RoomsPageEmbeddedState extends State<RoomsPageEmbedded> {
|
|
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<void> _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<void> 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<Map<String, dynamic>>?;
|
|
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<Box<Room>>(
|
|
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.withValues(alpha: 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<Box<Room>>(
|
|
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<Box<Room>>(
|
|
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<void> _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<void> _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<void> _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<void> _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<void> _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<bool>(
|
|
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<void> _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<void> _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<void> _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<void> _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<Map<String, dynamic>>(
|
|
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<Map<String, dynamic>> 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;
|
|
});
|
|
},
|
|
activeThumbColor: Colors.amber.shade600,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
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();
|
|
}
|
|
} |