feat: Release version 3.1.4 - Mode terrain et génération PDF
✨ Nouvelles fonctionnalités: - Ajout du mode terrain pour utilisation mobile hors connexion - Génération automatique de reçus PDF avec template personnalisé - Révision complète du système de cartes avec amélioration des performances 🔧 Améliorations techniques: - Refactoring du module chat avec architecture simplifiée - Optimisation du système de sécurité NIST SP 800-63B - Amélioration de la gestion des secteurs géographiques - Support UTF-8 étendu pour les noms d'utilisateurs 📱 Application mobile: - Nouveau mode terrain dans user_field_mode_page - Interface utilisateur adaptative pour conditions difficiles - Synchronisation offline améliorée 🗺️ Cartographie: - Optimisation des performances MapBox - Meilleure gestion des tuiles hors ligne - Amélioration de l'affichage des secteurs 📄 Documentation: - Ajout guide Android (ANDROID-GUIDE.md) - Documentation sécurité API (API-SECURITY.md) - Guide module chat (CHAT_MODULE.md) 🐛 Corrections: - Résolution des erreurs 400 lors de la création d'utilisateurs - Correction de la validation des noms d'utilisateurs - Fix des problèmes de synchronisation chat 🤖 Generated with Claude Code (https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:geosector_app/core/theme/app_theme.dart';
|
||||
import 'package:geosector_app/presentation/widgets/chat/chat_sidebar.dart';
|
||||
import 'package:geosector_app/presentation/widgets/chat/chat_messages.dart';
|
||||
import 'package:geosector_app/presentation/widgets/chat/chat_input.dart';
|
||||
import 'package:geosector_app/chat/chat_module.dart';
|
||||
import 'package:geosector_app/chat/pages/rooms_page_embedded.dart';
|
||||
import 'package:geosector_app/core/services/current_user_service.dart';
|
||||
import 'package:geosector_app/core/services/api_service.dart';
|
||||
import 'package:geosector_app/core/services/current_amicale_service.dart';
|
||||
|
||||
class AdminCommunicationPage extends StatefulWidget {
|
||||
const AdminCommunicationPage({super.key});
|
||||
@@ -12,546 +14,238 @@ class AdminCommunicationPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _AdminCommunicationPageState extends State<AdminCommunicationPage> {
|
||||
int selectedContactId = 0;
|
||||
String selectedContactName = '';
|
||||
bool isTeamChat = true;
|
||||
String messageText = '';
|
||||
bool isReplying = false;
|
||||
Map<String, dynamic>? replyingTo;
|
||||
bool _isChatInitialized = false;
|
||||
bool _isInitializing = false;
|
||||
String? _initError;
|
||||
GlobalKey<RoomsPageEmbeddedState>? _roomsPageKey;
|
||||
|
||||
// Données simulées pour les conversations d'équipe
|
||||
final List<Map<String, dynamic>> teamContacts = [
|
||||
{
|
||||
'id': 1,
|
||||
'name': 'Équipe',
|
||||
'isGroup': true,
|
||||
'lastMessage': 'Réunion à 14h aujourd\'hui',
|
||||
'time': DateTime.now().subtract(const Duration(minutes: 30)),
|
||||
'unread': 2,
|
||||
'online': true,
|
||||
'avatar': 'assets/images/team.png',
|
||||
},
|
||||
{
|
||||
'id': 2,
|
||||
'name': 'Jean Dupont',
|
||||
'isGroup': false,
|
||||
'lastMessage': 'Je serai présent demain',
|
||||
'time': DateTime.now().subtract(const Duration(hours: 1)),
|
||||
'unread': 0,
|
||||
'online': true,
|
||||
'avatar': 'assets/images/avatar1.png',
|
||||
},
|
||||
{
|
||||
'id': 3,
|
||||
'name': 'Marie Martin',
|
||||
'isGroup': false,
|
||||
'lastMessage': 'Secteur Sud terminé',
|
||||
'time': DateTime.now().subtract(const Duration(hours: 3)),
|
||||
'unread': 1,
|
||||
'online': false,
|
||||
'avatar': 'assets/images/avatar2.png',
|
||||
},
|
||||
{
|
||||
'id': 4,
|
||||
'name': 'Pierre Legrand',
|
||||
'isGroup': false,
|
||||
'lastMessage': 'J\'ai une question sur mon secteur',
|
||||
'time': DateTime.now().subtract(const Duration(days: 1)),
|
||||
'unread': 0,
|
||||
'online': false,
|
||||
'avatar': 'assets/images/avatar3.png',
|
||||
},
|
||||
];
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initializeChat();
|
||||
}
|
||||
|
||||
// Données simulées pour les conversations clients
|
||||
final List<Map<String, dynamic>> clientContacts = [
|
||||
{
|
||||
'id': 101,
|
||||
'name': 'Martin Durand',
|
||||
'isGroup': false,
|
||||
'lastMessage': 'Merci pour votre passage',
|
||||
'time': DateTime.now().subtract(const Duration(hours: 5)),
|
||||
'unread': 0,
|
||||
'online': false,
|
||||
'avatar': null,
|
||||
'email': 'martin.durand@example.com',
|
||||
},
|
||||
{
|
||||
'id': 102,
|
||||
'name': 'Sophie Lambert',
|
||||
'isGroup': false,
|
||||
'lastMessage': 'Question concernant le reçu',
|
||||
'time': DateTime.now().subtract(const Duration(days: 1)),
|
||||
'unread': 3,
|
||||
'online': false,
|
||||
'avatar': null,
|
||||
'email': 'sophie.lambert@example.com',
|
||||
},
|
||||
{
|
||||
'id': 103,
|
||||
'name': 'Thomas Bernard',
|
||||
'isGroup': false,
|
||||
'lastMessage': 'Rendez-vous manqué',
|
||||
'time': DateTime.now().subtract(const Duration(days: 2)),
|
||||
'unread': 0,
|
||||
'online': false,
|
||||
'avatar': null,
|
||||
'email': 'thomas.bernard@example.com',
|
||||
},
|
||||
];
|
||||
Future<void> _initializeChat() async {
|
||||
if (_isInitializing) return;
|
||||
|
||||
setState(() {
|
||||
_isInitializing = true;
|
||||
_initError = null;
|
||||
});
|
||||
|
||||
// Messages simulés pour la conversation sélectionnée
|
||||
final Map<int, List<Map<String, dynamic>>> chatMessages = {
|
||||
1: [
|
||||
{
|
||||
'id': 1,
|
||||
'senderId': 2,
|
||||
'senderName': 'Jean Dupont',
|
||||
'message':
|
||||
'Bonjour à tous, comment avance la collecte dans vos secteurs ?',
|
||||
'time': DateTime.now().subtract(const Duration(days: 1, hours: 3)),
|
||||
'isRead': true,
|
||||
'avatar': 'assets/images/avatar1.png',
|
||||
},
|
||||
{
|
||||
'id': 2,
|
||||
'senderId': 3,
|
||||
'senderName': 'Marie Martin',
|
||||
'message': 'J\'ai terminé le secteur Sud avec 45 passages réalisés !',
|
||||
'time': DateTime.now()
|
||||
.subtract(const Duration(days: 1, hours: 2, minutes: 30)),
|
||||
'isRead': true,
|
||||
'avatar': 'assets/images/avatar2.png',
|
||||
},
|
||||
{
|
||||
'id': 3,
|
||||
'senderId': 4,
|
||||
'senderName': 'Pierre Legrand',
|
||||
'message':
|
||||
'Secteur Est en cours, j\'ai réalisé 28 passages pour l\'instant.',
|
||||
'time': DateTime.now().subtract(const Duration(days: 1, hours: 2)),
|
||||
'isRead': true,
|
||||
'avatar': 'assets/images/avatar3.png',
|
||||
},
|
||||
{
|
||||
'id': 4,
|
||||
'senderId': 0,
|
||||
'senderName': 'Vous',
|
||||
'message':
|
||||
'Parfait, n\'oubliez pas la réunion de demain à 14h pour faire le point !',
|
||||
'time': DateTime.now().subtract(const Duration(hours: 1)),
|
||||
'isRead': true,
|
||||
},
|
||||
{
|
||||
'id': 5,
|
||||
'senderId': 2,
|
||||
'senderName': 'Jean Dupont',
|
||||
'message': 'Je serai présent 👍',
|
||||
'time': DateTime.now().subtract(const Duration(minutes: 30)),
|
||||
'isRead': false,
|
||||
'avatar': 'assets/images/avatar1.png',
|
||||
},
|
||||
],
|
||||
2: [
|
||||
{
|
||||
'id': 101,
|
||||
'senderId': 2,
|
||||
'senderName': 'Jean Dupont',
|
||||
'message':
|
||||
'Bonjour, est-ce que je peux commencer le secteur Ouest demain ?',
|
||||
'time': DateTime.now().subtract(const Duration(days: 2)),
|
||||
'isRead': true,
|
||||
'avatar': 'assets/images/avatar1.png',
|
||||
},
|
||||
{
|
||||
'id': 102,
|
||||
'senderId': 0,
|
||||
'senderName': 'Vous',
|
||||
'message': 'Bonjour Jean, oui bien sûr. Les documents sont prêts.',
|
||||
'time': DateTime.now()
|
||||
.subtract(const Duration(days: 2))
|
||||
.add(const Duration(minutes: 15)),
|
||||
'isRead': true,
|
||||
},
|
||||
{
|
||||
'id': 103,
|
||||
'senderId': 2,
|
||||
'senderName': 'Jean Dupont',
|
||||
'message': 'Merci ! Je passerai les récupérer ce soir.',
|
||||
'time': DateTime.now()
|
||||
.subtract(const Duration(days: 2))
|
||||
.add(const Duration(minutes: 20)),
|
||||
'isRead': true,
|
||||
'avatar': 'assets/images/avatar1.png',
|
||||
},
|
||||
{
|
||||
'id': 104,
|
||||
'senderId': 2,
|
||||
'senderName': 'Jean Dupont',
|
||||
'message': 'Je serai présent à la réunion de demain.',
|
||||
'time': DateTime.now().subtract(const Duration(hours: 1)),
|
||||
'isRead': true,
|
||||
'avatar': 'assets/images/avatar1.png',
|
||||
},
|
||||
],
|
||||
101: [
|
||||
{
|
||||
'id': 201,
|
||||
'senderId': 101,
|
||||
'senderName': 'Martin Durand',
|
||||
'message':
|
||||
'Bonjour, je voulais vous remercier pour votre passage. J\'ai bien reçu le reçu par email.',
|
||||
'time': DateTime.now().subtract(const Duration(days: 1, hours: 5)),
|
||||
'isRead': true,
|
||||
},
|
||||
{
|
||||
'id': 202,
|
||||
'senderId': 0,
|
||||
'senderName': 'Vous',
|
||||
'message':
|
||||
'Bonjour M. Durand, je vous remercie pour votre contribution. N\'hésitez pas si vous avez des questions.',
|
||||
'time': DateTime.now().subtract(const Duration(days: 1, hours: 4)),
|
||||
'isRead': true,
|
||||
},
|
||||
{
|
||||
'id': 203,
|
||||
'senderId': 101,
|
||||
'senderName': 'Martin Durand',
|
||||
'message': 'Tout est parfait, merci !',
|
||||
'time': DateTime.now().subtract(const Duration(hours: 5)),
|
||||
'isRead': true,
|
||||
},
|
||||
],
|
||||
102: [
|
||||
{
|
||||
'id': 301,
|
||||
'senderId': 102,
|
||||
'senderName': 'Sophie Lambert',
|
||||
'message':
|
||||
'Bonjour, je n\'ai pas reçu le reçu suite à mon paiement d\'hier. Pouvez-vous vérifier ?',
|
||||
'time': DateTime.now().subtract(const Duration(days: 1, hours: 3)),
|
||||
'isRead': true,
|
||||
},
|
||||
{
|
||||
'id': 302,
|
||||
'senderId': 0,
|
||||
'senderName': 'Vous',
|
||||
'message':
|
||||
'Bonjour Mme Lambert, je m\'excuse pour ce désagrément. Je vérifie cela immédiatement.',
|
||||
'time': DateTime.now().subtract(const Duration(days: 1, hours: 2)),
|
||||
'isRead': true,
|
||||
},
|
||||
{
|
||||
'id': 303,
|
||||
'senderId': 0,
|
||||
'senderName': 'Vous',
|
||||
'message':
|
||||
'Il semble qu\'il y ait eu un problème technique. Je viens de renvoyer le reçu à votre adresse email. Pourriez-vous vérifier si vous l\'avez bien reçu ?',
|
||||
'time': DateTime.now().subtract(const Duration(days: 1, hours: 1)),
|
||||
'isRead': true,
|
||||
},
|
||||
{
|
||||
'id': 304,
|
||||
'senderId': 102,
|
||||
'senderName': 'Sophie Lambert',
|
||||
'message':
|
||||
'Je n\'ai toujours rien reçu. Mon email est-il correct ? C\'est sophie.lambert@example.com',
|
||||
'time': DateTime.now().subtract(const Duration(days: 1)),
|
||||
'isRead': true,
|
||||
},
|
||||
{
|
||||
'id': 305,
|
||||
'senderId': 102,
|
||||
'senderName': 'Sophie Lambert',
|
||||
'message': 'Est-ce que vous pouvez réessayer ?',
|
||||
'time': DateTime.now().subtract(const Duration(hours: 5)),
|
||||
'isRead': false,
|
||||
},
|
||||
{
|
||||
'id': 306,
|
||||
'senderId': 102,
|
||||
'senderName': 'Sophie Lambert',
|
||||
'message': 'Toujours pas de nouvelles...',
|
||||
'time': DateTime.now().subtract(const Duration(hours: 3)),
|
||||
'isRead': false,
|
||||
},
|
||||
{
|
||||
'id': 307,
|
||||
'senderId': 102,
|
||||
'senderName': 'Sophie Lambert',
|
||||
'message': 'Pouvez-vous me contacter dès que possible ?',
|
||||
'time': DateTime.now().subtract(const Duration(hours: 1)),
|
||||
'isRead': false,
|
||||
},
|
||||
],
|
||||
};
|
||||
try {
|
||||
// Récupérer les informations utilisateur
|
||||
final currentUser = CurrentUserService.instance;
|
||||
final apiService = ApiService.instance;
|
||||
final currentAmicale = CurrentAmicaleService.instance.currentAmicale;
|
||||
|
||||
if (currentUser.currentUser == null) {
|
||||
throw Exception('Administrateur non connecté');
|
||||
}
|
||||
|
||||
// Initialiser le module chat avec les informations de l'administrateur
|
||||
await ChatModule.init(
|
||||
apiUrl: apiService.baseUrl,
|
||||
userId: currentUser.currentUser!.id,
|
||||
userName: currentUser.userName ?? currentUser.userEmail ?? 'Administrateur',
|
||||
userRole: currentUser.currentUser!.role,
|
||||
userEntite: currentUser.fkEntite ?? currentAmicale?.id,
|
||||
authToken: currentUser.sessionId,
|
||||
);
|
||||
|
||||
setState(() {
|
||||
_isChatInitialized = true;
|
||||
_isInitializing = false;
|
||||
_roomsPageKey = GlobalKey<RoomsPageEmbeddedState>();
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_initError = e.toString();
|
||||
_isInitializing = false;
|
||||
});
|
||||
debugPrint('Erreur initialisation chat admin: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void _refreshRooms() {
|
||||
_roomsPageKey?.currentState?.refresh();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
final isDesktop = screenWidth > 800;
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
// Sidebar des contacts (fixe sur desktop, conditional sur mobile)
|
||||
if (isDesktop || selectedContactId == 0)
|
||||
SizedBox(
|
||||
width: isDesktop ? 320 : screenWidth,
|
||||
child: ChatSidebar(
|
||||
teamContacts: teamContacts,
|
||||
clientContacts: clientContacts,
|
||||
isTeamChat: isTeamChat,
|
||||
selectedContactId: selectedContactId,
|
||||
onContactSelected: (contactId, contactName, isTeam) {
|
||||
setState(() {
|
||||
selectedContactId = contactId;
|
||||
selectedContactName = contactName;
|
||||
isTeamChat = isTeam;
|
||||
replyingTo = null;
|
||||
isReplying = false;
|
||||
});
|
||||
},
|
||||
onToggleGroup: (isTeam) {
|
||||
setState(() {
|
||||
isTeamChat = isTeam;
|
||||
selectedContactId = 0;
|
||||
selectedContactName = '';
|
||||
});
|
||||
},
|
||||
),
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(16.0),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: theme.shadowColor.withOpacity(0.1),
|
||||
blurRadius: 20,
|
||||
spreadRadius: 1,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
|
||||
// Vue des messages (conditionnelle sur mobile)
|
||||
if (isDesktop || selectedContactId != 0)
|
||||
Expanded(
|
||||
child: selectedContactId == 0
|
||||
? const Center(
|
||||
child: Text('Sélectionnez une conversation pour commencer'),
|
||||
)
|
||||
: Column(
|
||||
children: [
|
||||
// En-tête de la conversation
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppTheme.spacingL,
|
||||
vertical: AppTheme.spacingM,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 5,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
if (!isDesktop)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
selectedContactId = 0;
|
||||
selectedContactName = '';
|
||||
});
|
||||
},
|
||||
),
|
||||
CircleAvatar(
|
||||
radius: 20,
|
||||
backgroundColor:
|
||||
AppTheme.primaryColor.withOpacity(0.2),
|
||||
backgroundImage:
|
||||
_getAvatarForContact(selectedContactId),
|
||||
child: _getAvatarForContact(selectedContactId) ==
|
||||
null
|
||||
? Text(
|
||||
selectedContactName.isNotEmpty
|
||||
? selectedContactName[0].toUpperCase()
|
||||
: '',
|
||||
style: const TextStyle(
|
||||
color: AppTheme.primaryColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
const SizedBox(width: AppTheme.spacingM),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
selectedContactName,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
if (!isTeamChat && selectedContactId > 100)
|
||||
Text(
|
||||
_getEmailForContact(selectedContactId),
|
||||
style: TextStyle(
|
||||
color: Colors.grey[600],
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.info_outline),
|
||||
onPressed: () {
|
||||
// Afficher les détails du contact
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Messages
|
||||
Expanded(
|
||||
child: ChatMessages(
|
||||
messages: chatMessages[selectedContactId] ?? [],
|
||||
currentUserId: 0,
|
||||
onReply: (message) {
|
||||
setState(() {
|
||||
isReplying = true;
|
||||
replyingTo = message;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// Zone de réponse
|
||||
if (isReplying)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(AppTheme.spacingM),
|
||||
color: Colors.grey[100],
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 4,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.primaryColor,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppTheme.spacingM),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Réponse à ${replyingTo?['senderName']}',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
color: AppTheme.primaryColor,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
replyingTo?['message'] ?? '',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
isReplying = false;
|
||||
replyingTo = null;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Zone de saisie du message
|
||||
ChatInput(
|
||||
onMessageSent: (text) {
|
||||
setState(() {
|
||||
// Ajouter le message à la conversation
|
||||
if (chatMessages[selectedContactId] != null) {
|
||||
final newMessageId =
|
||||
chatMessages[selectedContactId]!.last['id'] +
|
||||
1;
|
||||
|
||||
chatMessages[selectedContactId]!.add({
|
||||
'id': newMessageId,
|
||||
'senderId': 0,
|
||||
'senderName': 'Vous',
|
||||
'message': text,
|
||||
'time': DateTime.now(),
|
||||
'isRead': false,
|
||||
'replyTo': isReplying ? replyingTo : null,
|
||||
});
|
||||
|
||||
// Mise à jour du dernier message pour le contact
|
||||
final contactsList =
|
||||
isTeamChat ? teamContacts : clientContacts;
|
||||
final contactIndex = contactsList.indexWhere(
|
||||
(c) => c['id'] == selectedContactId);
|
||||
|
||||
if (contactIndex != -1) {
|
||||
contactsList[contactIndex]['lastMessage'] =
|
||||
text;
|
||||
contactsList[contactIndex]['time'] =
|
||||
DateTime.now();
|
||||
contactsList[contactIndex]['unread'] = 0;
|
||||
}
|
||||
|
||||
isReplying = false;
|
||||
replyingTo = null;
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
child: _buildContent(theme),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
ImageProvider? _getAvatarForContact(int contactId) {
|
||||
String? avatarPath;
|
||||
|
||||
if (isTeamChat) {
|
||||
final contact = teamContacts.firstWhere(
|
||||
(c) => c['id'] == contactId,
|
||||
orElse: () => {'avatar': null},
|
||||
Widget _buildContent(ThemeData theme) {
|
||||
if (_isInitializing) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const CircularProgressIndicator(),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Initialisation du chat administrateur...',
|
||||
style: theme.textTheme.bodyLarge,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
avatarPath = contact['avatar'];
|
||||
} else {
|
||||
final contact = clientContacts.firstWhere(
|
||||
(c) => c['id'] == contactId,
|
||||
orElse: () => {'avatar': null},
|
||||
);
|
||||
avatarPath = contact['avatar'];
|
||||
}
|
||||
|
||||
return avatarPath != null ? AssetImage(avatarPath) : null;
|
||||
}
|
||||
|
||||
String _getEmailForContact(int contactId) {
|
||||
if (!isTeamChat) {
|
||||
final contact = clientContacts.firstWhere(
|
||||
(c) => c['id'] == contactId,
|
||||
orElse: () => {'email': ''},
|
||||
if (_initError != null) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
size: 64,
|
||||
color: theme.colorScheme.error,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Erreur d\'initialisation chat',
|
||||
style: theme.textTheme.headlineSmall?.copyWith(
|
||||
color: theme.colorScheme.error,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
_initError!,
|
||||
style: theme.textTheme.bodyMedium,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton.icon(
|
||||
onPressed: _initializeChat,
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: const Text('Réessayer'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: theme.colorScheme.error,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
return contact['email'] ?? '';
|
||||
}
|
||||
return '';
|
||||
|
||||
if (_isChatInitialized) {
|
||||
// Afficher le module chat avec un header simplifié
|
||||
return Column(
|
||||
children: [
|
||||
// En-tête simplifié avec boutons intégrés
|
||||
Container(
|
||||
height: 60,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.shade50,
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: theme.dividerColor.withOpacity(0.1),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.admin_panel_settings,
|
||||
color: Colors.red.shade600,
|
||||
size: 24,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
'Messages Administration',
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.red.shade600,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
// Boutons d'action
|
||||
IconButton(
|
||||
icon: Icon(Icons.add, color: Colors.red.shade600),
|
||||
onPressed: () {
|
||||
// Déclencher la création d'une nouvelle conversation
|
||||
// Cela sera géré par RoomsPageEmbedded
|
||||
_roomsPageKey?.currentState?.createNewConversation();
|
||||
},
|
||||
tooltip: 'Nouvelle conversation',
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.refresh, color: Colors.red.shade600),
|
||||
onPressed: _refreshRooms,
|
||||
tooltip: 'Actualiser',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Module chat sans AppBar
|
||||
Expanded(
|
||||
child: RoomsPageEmbedded(
|
||||
key: _roomsPageKey,
|
||||
onRefreshPressed: () {
|
||||
// Callback optionnel après refresh
|
||||
debugPrint('Conversations actualisées');
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// État initial
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.chat_bubble_outline,
|
||||
size: 80,
|
||||
color: theme.colorScheme.primary.withOpacity(0.3),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'Chat administrateur non initialisé',
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
color: theme.colorScheme.onSurface.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton.icon(
|
||||
onPressed: _initializeChat,
|
||||
icon: const Icon(Icons.power_settings_new),
|
||||
label: const Text('Initialiser le chat'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:geosector_app/presentation/widgets/dashboard_layout.dart';
|
||||
import 'package:geosector_app/presentation/widgets/badged_navigation_destination.dart';
|
||||
import 'package:geosector_app/core/constants/app_keys.dart';
|
||||
import 'dart:math' as math;
|
||||
|
||||
@@ -146,13 +147,25 @@ class _AdminDashboardPageState extends State<AdminDashboardPage> with WidgetsBin
|
||||
|
||||
// Ajouter les éléments de base
|
||||
for (final item in _baseNavigationItems) {
|
||||
destinations.add(
|
||||
NavigationDestination(
|
||||
icon: Icon(item.icon),
|
||||
selectedIcon: Icon(item.selectedIcon),
|
||||
label: item.label,
|
||||
),
|
||||
);
|
||||
// Utiliser createBadgedNavigationDestination pour les messages
|
||||
if (item.label == 'Messages') {
|
||||
destinations.add(
|
||||
createBadgedNavigationDestination(
|
||||
icon: Icon(item.icon),
|
||||
selectedIcon: Icon(item.selectedIcon),
|
||||
label: item.label,
|
||||
showBadge: true,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
destinations.add(
|
||||
NavigationDestination(
|
||||
icon: Icon(item.icon),
|
||||
selectedIcon: Icon(item.selectedIcon),
|
||||
label: item.label,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Ajouter les éléments admin si l'utilisateur a le rôle requis
|
||||
@@ -165,13 +178,25 @@ class _AdminDashboardPageState extends State<AdminDashboardPage> with WidgetsBin
|
||||
}
|
||||
|
||||
if (item.requiredRole == null || item.requiredRole == 2) {
|
||||
destinations.add(
|
||||
NavigationDestination(
|
||||
icon: Icon(item.icon),
|
||||
selectedIcon: Icon(item.selectedIcon),
|
||||
label: item.label,
|
||||
),
|
||||
);
|
||||
// Utiliser createBadgedNavigationDestination pour les messages
|
||||
if (item.label == 'Messages') {
|
||||
destinations.add(
|
||||
createBadgedNavigationDestination(
|
||||
icon: Icon(item.icon),
|
||||
selectedIcon: Icon(item.selectedIcon),
|
||||
label: item.label,
|
||||
showBadge: true,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
destinations.add(
|
||||
NavigationDestination(
|
||||
icon: Icon(item.icon),
|
||||
selectedIcon: Icon(item.selectedIcon),
|
||||
label: item.label,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3981,6 +3981,8 @@ class _AdminMapPageState extends State<AdminMapPage> {
|
||||
initialZoom: _currentZoom,
|
||||
mapController: _mapController,
|
||||
disableDrag: _isDraggingPoint,
|
||||
// Utiliser OpenStreetMap temporairement sur mobile si Mapbox échoue
|
||||
useOpenStreetMap: !kIsWeb, // true sur mobile, false sur web
|
||||
labelMarkers: _buildSectorLabels(),
|
||||
markers: [
|
||||
..._buildMarkers(),
|
||||
|
||||
@@ -12,6 +12,7 @@ import 'package:geosector_app/presentation/widgets/custom_text_field.dart';
|
||||
import 'package:geosector_app/presentation/widgets/connectivity_indicator.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:geosector_app/app.dart'; // Pour accéder aux instances globales
|
||||
import 'package:geosector_app/core/services/hive_service.dart'; // Pour vérifier l'initialisation
|
||||
|
||||
class LoginPage extends StatefulWidget {
|
||||
final String? loginType;
|
||||
@@ -58,7 +59,7 @@ class _LoginPageState extends State<LoginPage> {
|
||||
|
||||
|
||||
// État de la connexion Internet
|
||||
bool _isConnected = false;
|
||||
bool _isConnected = true; // Par défaut, on suppose qu'il y a une connexion
|
||||
|
||||
Future<void> _getAppVersion() async {
|
||||
try {
|
||||
@@ -95,7 +96,28 @@ class _LoginPageState extends State<LoginPage> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// Vérification du type de connexion
|
||||
// VÉRIFICATION CRITIQUE : S'assurer que Hive est initialisé
|
||||
// Cette vérification DOIT se faire avant tout accès aux repositories
|
||||
if (!HiveService.instance.areBoxesInitialized()) {
|
||||
debugPrint('⚠️ LoginPage: Boxes Hive non initialisées, redirection vers SplashPage');
|
||||
|
||||
// Construire les paramètres pour la redirection après initialisation
|
||||
final loginType = widget.loginType ?? 'admin';
|
||||
|
||||
// Rediriger immédiatement vers SplashPage avec les bons paramètres
|
||||
// pour que SplashPage puisse rediriger automatiquement après l'initialisation
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) {
|
||||
context.go('/?action=login&type=$loginType');
|
||||
}
|
||||
});
|
||||
|
||||
// Initialiser avec des valeurs par défaut pour éviter les erreurs
|
||||
_loginType = '';
|
||||
return; // IMPORTANT : Arrêter l'exécution du reste de initState
|
||||
}
|
||||
|
||||
// Vérification du type de connexion (seulement si Hive est initialisé)
|
||||
if (widget.loginType == null) {
|
||||
// Si aucun type n'est spécifié, naviguer vers la splash page
|
||||
print(
|
||||
@@ -191,7 +213,14 @@ class _LoginPageState extends State<LoginPage> {
|
||||
// Pré-remplir le champ username avec l'identifiant du dernier utilisateur connecté
|
||||
// seulement si le rôle correspond au type de login
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final users = userRepository.getAllUsers();
|
||||
// Vérifier à nouveau que les boxes sont disponibles
|
||||
if (!HiveService.instance.areBoxesInitialized()) {
|
||||
debugPrint('⚠️ Boxes non disponibles pour pré-remplir le username');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final users = userRepository.getAllUsers();
|
||||
|
||||
if (users.isNotEmpty) {
|
||||
// Trouver l'utilisateur le plus récent (celui avec la date de dernière connexion la plus récente)
|
||||
@@ -236,6 +265,10 @@ class _LoginPageState extends State<LoginPage> {
|
||||
'Le rôle ($roleValue) ne correspond pas au type de login ($_loginType), champ username non pré-rempli');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Erreur lors du pré-remplissage: $e');
|
||||
// Continuer sans pré-remplir en cas d'erreur
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -27,10 +27,12 @@ class _SectorDialogState extends State<SectorDialog> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _nameController = TextEditingController();
|
||||
final _nameFocusNode = FocusNode();
|
||||
final _searchController = TextEditingController();
|
||||
Color _selectedColor = Colors.blue;
|
||||
final List<int> _selectedMemberIds = [];
|
||||
bool _isLoading = false;
|
||||
bool _membersLoaded = false;
|
||||
String _searchQuery = '';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -108,6 +110,7 @@ class _SectorDialogState extends State<SectorDialog> {
|
||||
void dispose() {
|
||||
_nameController.dispose();
|
||||
_nameFocusNode.dispose();
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -162,69 +165,111 @@ class _SectorDialogState extends State<SectorDialog> {
|
||||
}
|
||||
|
||||
void _showColorPicker() {
|
||||
// Liste de couleurs prédéfinies
|
||||
final List<Color> colors = [
|
||||
Colors.red,
|
||||
Colors.pink,
|
||||
Colors.purple,
|
||||
Colors.deepPurple,
|
||||
Colors.indigo,
|
||||
Colors.blue,
|
||||
Colors.lightBlue,
|
||||
Colors.cyan,
|
||||
Colors.teal,
|
||||
Colors.green,
|
||||
Colors.lightGreen,
|
||||
Colors.lime,
|
||||
Colors.yellow,
|
||||
Colors.amber,
|
||||
Colors.orange,
|
||||
Colors.deepOrange,
|
||||
Colors.brown,
|
||||
Colors.grey,
|
||||
Colors.blueGrey,
|
||||
const Color(0xFF1E88E5), // Bleu personnalisé
|
||||
const Color(0xFF43A047), // Vert personnalisé
|
||||
const Color(0xFFE53935), // Rouge personnalisé
|
||||
const Color(0xFFFFB300), // Ambre personnalisé
|
||||
const Color(0xFF8E24AA), // Violet personnalisé
|
||||
];
|
||||
// Grille 6x6 de couleurs suivant le spectre
|
||||
// 6 colonnes: Rouge, Orange, Jaune, Vert, Bleu, Violet
|
||||
// 6 lignes: variations de luminosité/saturation
|
||||
final List<Color> colors = _generateSpectralColorGrid();
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Choisir une couleur'),
|
||||
contentPadding: const EdgeInsets.fromLTRB(20, 12, 20, 20),
|
||||
content: Container(
|
||||
width: double.maxFinite,
|
||||
child: GridView.builder(
|
||||
shrinkWrap: true,
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 4,
|
||||
mainAxisSpacing: 8,
|
||||
crossAxisSpacing: 8,
|
||||
),
|
||||
itemCount: colors.length,
|
||||
itemBuilder: (context, index) {
|
||||
final color = colors[index];
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_selectedColor = color;
|
||||
});
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
border: Border.all(
|
||||
color: _selectedColor == color ? Colors.black : Colors.grey,
|
||||
width: _selectedColor == color ? 3 : 1,
|
||||
width: 280, // Largeur fixe pour contrôler la taille
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: Colors.grey.shade50,
|
||||
),
|
||||
child: GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 6,
|
||||
mainAxisSpacing: 4,
|
||||
crossAxisSpacing: 4,
|
||||
childAspectRatio: 1.0,
|
||||
),
|
||||
itemCount: colors.length,
|
||||
itemBuilder: (context, index) {
|
||||
final color = colors[index];
|
||||
final isSelected = _selectedColor.value == color.value;
|
||||
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_selectedColor = color;
|
||||
});
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Container(
|
||||
width: 35,
|
||||
height: 35,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(
|
||||
color: isSelected ? Colors.black87 : Colors.grey.shade400,
|
||||
width: isSelected ? 2.5 : 0.5,
|
||||
),
|
||||
boxShadow: isSelected
|
||||
? [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.3),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
]
|
||||
: null,
|
||||
),
|
||||
child: isSelected
|
||||
? const Icon(
|
||||
Icons.check,
|
||||
color: Colors.white,
|
||||
size: 18,
|
||||
shadows: [
|
||||
Shadow(
|
||||
color: Colors.black,
|
||||
blurRadius: 2,
|
||||
),
|
||||
],
|
||||
)
|
||||
: null,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// Affichage de la couleur sélectionnée
|
||||
Container(
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: _selectedColor,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(color: Colors.grey.shade400),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'Couleur sélectionnée',
|
||||
style: TextStyle(
|
||||
color: _selectedColor.computeLuminance() > 0.5
|
||||
? Colors.black87
|
||||
: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 13,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
@@ -237,19 +282,107 @@ class _SectorDialogState extends State<SectorDialog> {
|
||||
);
|
||||
}
|
||||
|
||||
// Mettre en surbrillance les termes recherchés dans le texte
|
||||
List<TextSpan> _highlightSearchTerms(String text) {
|
||||
if (_searchQuery.isEmpty) {
|
||||
return [TextSpan(text: text)];
|
||||
}
|
||||
|
||||
final List<TextSpan> spans = [];
|
||||
final lowerText = text.toLowerCase();
|
||||
int start = 0;
|
||||
int index = lowerText.indexOf(_searchQuery, start);
|
||||
|
||||
while (index != -1) {
|
||||
// Ajouter le texte avant le terme trouvé
|
||||
if (index > start) {
|
||||
spans.add(TextSpan(
|
||||
text: text.substring(start, index),
|
||||
));
|
||||
}
|
||||
|
||||
// Ajouter le terme trouvé en surbrillance
|
||||
spans.add(TextSpan(
|
||||
text: text.substring(index, index + _searchQuery.length),
|
||||
style: const TextStyle(
|
||||
backgroundColor: Colors.yellow,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
));
|
||||
|
||||
start = index + _searchQuery.length;
|
||||
index = lowerText.indexOf(_searchQuery, start);
|
||||
}
|
||||
|
||||
// Ajouter le reste du texte
|
||||
if (start < text.length) {
|
||||
spans.add(TextSpan(
|
||||
text: text.substring(start),
|
||||
));
|
||||
}
|
||||
|
||||
return spans;
|
||||
}
|
||||
|
||||
// Générer une grille 6x6 de couleurs spectrales
|
||||
List<Color> _generateSpectralColorGrid() {
|
||||
final List<Color> colors = [];
|
||||
|
||||
// 6 teintes de base (colonnes)
|
||||
final List<double> hues = [
|
||||
0, // Rouge
|
||||
30, // Orange
|
||||
60, // Jaune
|
||||
120, // Vert
|
||||
210, // Bleu
|
||||
270, // Violet
|
||||
];
|
||||
|
||||
// 6 variations de luminosité/saturation (lignes)
|
||||
// Du plus clair au plus foncé
|
||||
final List<Map<String, double>> variations = [
|
||||
{'saturation': 0.3, 'lightness': 0.85}, // Très clair
|
||||
{'saturation': 0.5, 'lightness': 0.70}, // Clair
|
||||
{'saturation': 0.7, 'lightness': 0.55}, // Moyen clair
|
||||
{'saturation': 0.85, 'lightness': 0.45}, // Moyen foncé
|
||||
{'saturation': 0.95, 'lightness': 0.35}, // Foncé
|
||||
{'saturation': 1.0, 'lightness': 0.25}, // Très foncé
|
||||
];
|
||||
|
||||
// Générer la grille ligne par ligne
|
||||
for (final variation in variations) {
|
||||
for (final hue in hues) {
|
||||
colors.add(
|
||||
HSLColor.fromAHSL(
|
||||
1.0,
|
||||
hue,
|
||||
variation['saturation']!,
|
||||
variation['lightness']!,
|
||||
).toColor(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return colors;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final currentAmicale = CurrentAmicaleService.instance.currentAmicale;
|
||||
final screenHeight = MediaQuery.of(context).size.height;
|
||||
final dialogHeight = (screenHeight * 0.8).clamp(0.0, 800.0); // 80% de l'écran avec max 800px
|
||||
|
||||
return AlertDialog(
|
||||
title: Text(widget.existingSector == null ? 'Nouveau secteur' : 'Modifier le secteur'),
|
||||
content: SingleChildScrollView(
|
||||
content: Container(
|
||||
width: 450, // Largeur fixe pour la dialog
|
||||
height: dialogHeight, // Hauteur avec maximum de 800px
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Section scrollable pour nom et couleur
|
||||
// Nom du secteur
|
||||
TextFormField(
|
||||
controller: _nameController,
|
||||
@@ -329,6 +462,37 @@ class _SectorDialogState extends State<SectorDialog> {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// Champ de recherche pour filtrer les membres
|
||||
TextField(
|
||||
controller: _searchController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Rechercher par prénom, nom ou nom de tournée...',
|
||||
prefixIcon: const Icon(Icons.search),
|
||||
suffixIcon: _searchQuery.isNotEmpty
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_searchController.clear();
|
||||
_searchQuery = '';
|
||||
});
|
||||
},
|
||||
)
|
||||
: null,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_searchQuery = value.toLowerCase();
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
if (_selectedMemberIds.isEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
@@ -341,62 +505,111 @@ class _SectorDialogState extends State<SectorDialog> {
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Liste des membres avec scrolling et filtre
|
||||
if (currentAmicale != null)
|
||||
ValueListenableBuilder<Box<MembreModel>>(
|
||||
valueListenable: Hive.box<MembreModel>(AppKeys.membresBoxName).listenable(),
|
||||
builder: (context, box, _) {
|
||||
debugPrint('=== Build liste membres - IDs présélectionnés: $_selectedMemberIds ===');
|
||||
|
||||
final membres = box.values
|
||||
.where((m) => m.fkEntite == currentAmicale.id)
|
||||
.toList();
|
||||
|
||||
if (membres.isEmpty) {
|
||||
return const Center(
|
||||
child: Text('Aucun membre disponible'),
|
||||
);
|
||||
}
|
||||
|
||||
return Container(
|
||||
constraints: const BoxConstraints(maxHeight: 200),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: membres.length,
|
||||
itemBuilder: (context, index) {
|
||||
final membre = membres[index];
|
||||
final isSelected = _selectedMemberIds.contains(membre.id);
|
||||
Expanded(
|
||||
child: ValueListenableBuilder<Box<MembreModel>>(
|
||||
valueListenable: Hive.box<MembreModel>(AppKeys.membresBoxName).listenable(),
|
||||
builder: (context, box, _) {
|
||||
debugPrint('=== Build liste membres - IDs présélectionnés: $_selectedMemberIds ===');
|
||||
|
||||
// Filtrer les membres de l'amicale
|
||||
var membres = box.values
|
||||
.where((m) => m.fkEntite == currentAmicale.id)
|
||||
.toList();
|
||||
|
||||
// Appliquer le filtre de recherche
|
||||
if (_searchQuery.isNotEmpty) {
|
||||
membres = membres.where((membre) {
|
||||
final firstName = membre.firstName?.toLowerCase() ?? '';
|
||||
final lastName = membre.name?.toLowerCase() ?? '';
|
||||
final sectName = membre.sectName?.toLowerCase() ?? '';
|
||||
|
||||
// Log pour debug
|
||||
if (index < 3) { // Limiter les logs aux 3 premiers membres
|
||||
debugPrint('Membre ${index}: ${membre.firstName} ${membre.name} (ID: ${membre.id}) - isSelected: $isSelected');
|
||||
}
|
||||
|
||||
return CheckboxListTile(
|
||||
dense: true,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 0.0),
|
||||
title: Text(
|
||||
'${membre.firstName} ${membre.name}${membre.sectName != null && membre.sectName!.isNotEmpty ? ' (${membre.sectName})' : ''}',
|
||||
style: const TextStyle(fontSize: 14),
|
||||
return firstName.contains(_searchQuery) ||
|
||||
lastName.contains(_searchQuery) ||
|
||||
sectName.contains(_searchQuery);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
if (membres.isEmpty) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Text(
|
||||
_searchQuery.isNotEmpty
|
||||
? 'Aucun membre trouvé pour "$_searchQuery"'
|
||||
: 'Aucun membre disponible',
|
||||
style: TextStyle(
|
||||
color: Colors.grey[600],
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
value: isSelected,
|
||||
onChanged: (bool? value) {
|
||||
setState(() {
|
||||
if (value == true) {
|
||||
_selectedMemberIds.add(membre.id);
|
||||
} else {
|
||||
_selectedMemberIds.remove(membre.id);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Afficher le nombre de résultats
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||
child: Text(
|
||||
'${membres.length} membre${membres.length > 1 ? 's' : ''} ${_searchQuery.isNotEmpty ? 'trouvé${membres.length > 1 ? 's' : ''}' : 'disponible${membres.length > 1 ? 's' : ''}'}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: ListView.builder(
|
||||
itemCount: membres.length,
|
||||
itemBuilder: (context, index) {
|
||||
final membre = membres[index];
|
||||
final isSelected = _selectedMemberIds.contains(membre.id);
|
||||
|
||||
// Log pour debug
|
||||
if (index < 3) { // Limiter les logs aux 3 premiers membres
|
||||
debugPrint('Membre ${index}: ${membre.firstName} ${membre.name} (ID: ${membre.id}) - isSelected: $isSelected');
|
||||
}
|
||||
|
||||
return CheckboxListTile(
|
||||
dense: true,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 0.0),
|
||||
title: RichText(
|
||||
text: TextSpan(
|
||||
style: const TextStyle(fontSize: 14, color: Colors.black87),
|
||||
children: _highlightSearchTerms(
|
||||
'${membre.firstName} ${membre.name}${membre.sectName != null && membre.sectName!.isNotEmpty ? ' (${membre.sectName})' : ''}',
|
||||
),
|
||||
),
|
||||
),
|
||||
value: isSelected,
|
||||
onChanged: (bool? value) {
|
||||
setState(() {
|
||||
if (value == true) {
|
||||
_selectedMemberIds.add(membre.id);
|
||||
} else {
|
||||
_selectedMemberIds.remove(membre.id);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:geosector_app/core/theme/app_theme.dart';
|
||||
import 'package:geosector_app/chat/widgets/conversations_list.dart';
|
||||
import 'package:geosector_app/chat/widgets/chat_screen.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:geosector_app/core/constants/app_keys.dart';
|
||||
import 'package:geosector_app/chat/models/conversation_model.dart';
|
||||
import 'package:geosector_app/chat/chat_module.dart';
|
||||
import 'package:geosector_app/core/services/current_user_service.dart';
|
||||
import 'package:geosector_app/core/services/api_service.dart';
|
||||
import 'package:geosector_app/core/services/current_amicale_service.dart';
|
||||
|
||||
class UserCommunicationPage extends StatefulWidget {
|
||||
const UserCommunicationPage({super.key});
|
||||
@@ -14,24 +13,54 @@ class UserCommunicationPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _UserCommunicationPageState extends State<UserCommunicationPage> {
|
||||
String? _selectedConversationId;
|
||||
late Box<ConversationModel> _conversationsBox;
|
||||
bool _hasConversations = false;
|
||||
bool _isChatInitialized = false;
|
||||
bool _isInitializing = false;
|
||||
String? _initError;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_checkConversations();
|
||||
_initializeChat();
|
||||
}
|
||||
|
||||
Future<void> _checkConversations() async {
|
||||
Future<void> _initializeChat() async {
|
||||
if (_isInitializing) return;
|
||||
|
||||
setState(() {
|
||||
_isInitializing = true;
|
||||
_initError = null;
|
||||
});
|
||||
|
||||
try {
|
||||
_conversationsBox = Hive.box<ConversationModel>(AppKeys.chatConversationsBoxName);
|
||||
// Récupérer les informations utilisateur
|
||||
final currentUser = CurrentUserService.instance;
|
||||
final apiService = ApiService.instance;
|
||||
final currentAmicale = CurrentAmicaleService.instance.currentAmicale;
|
||||
|
||||
if (currentUser.currentUser == null) {
|
||||
throw Exception('Utilisateur non connecté');
|
||||
}
|
||||
|
||||
// Initialiser le module chat avec les informations de l'utilisateur
|
||||
await ChatModule.init(
|
||||
apiUrl: apiService.baseUrl,
|
||||
userId: currentUser.currentUser!.id,
|
||||
userName: currentUser.userName ?? currentUser.userEmail ?? 'Utilisateur',
|
||||
userRole: currentUser.currentUser!.role,
|
||||
userEntite: currentUser.fkEntite ?? currentAmicale?.id,
|
||||
authToken: currentUser.sessionId,
|
||||
);
|
||||
|
||||
setState(() {
|
||||
_hasConversations = _conversationsBox.values.isNotEmpty;
|
||||
_isChatInitialized = true;
|
||||
_isInitializing = false;
|
||||
});
|
||||
} catch (e) {
|
||||
debugPrint('Erreur lors de la vérification des conversations: $e');
|
||||
setState(() {
|
||||
_initError = e.toString();
|
||||
_isInitializing = false;
|
||||
});
|
||||
debugPrint('Erreur initialisation chat: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,131 +86,69 @@ class _UserCommunicationPageState extends State<UserCommunicationPage> {
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
child: Column(
|
||||
children: [
|
||||
// En-tête du chat
|
||||
Container(
|
||||
height: 70,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primary.withOpacity(0.05),
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: theme.dividerColor.withOpacity(0.1),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.chat_bubble_outline,
|
||||
color: theme.colorScheme.primary,
|
||||
size: 26,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
'Messages d\'équipe',
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
if (_hasConversations) ...[
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppTheme.secondaryColor.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.green,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'5 en ligne',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: AppTheme.secondaryColor,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add_circle_outline),
|
||||
iconSize: 28,
|
||||
color: theme.colorScheme.primary,
|
||||
onPressed: () {
|
||||
// TODO: Créer une nouvelle conversation
|
||||
},
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Contenu principal
|
||||
Expanded(
|
||||
child: _hasConversations
|
||||
? Row(
|
||||
children: [
|
||||
// Liste des conversations (gauche)
|
||||
Container(
|
||||
width: 320,
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surface,
|
||||
border: Border(
|
||||
right: BorderSide(
|
||||
color: theme.dividerColor.withOpacity(0.1),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: ConversationsList(
|
||||
onConversationSelected: (conversation) {
|
||||
setState(() {
|
||||
// TODO: obtenir l'ID de la conversation à partir de l'objet conversation
|
||||
_selectedConversationId = 'test-conversation-id';
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// Zone de conversation (droite)
|
||||
Expanded(
|
||||
child: Container(
|
||||
color: theme.colorScheme.surface,
|
||||
child: _selectedConversationId != null
|
||||
? ChatScreen(conversationId: _selectedConversationId!)
|
||||
: _buildEmptyState(theme),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: _buildNoConversationsMessage(theme),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: _buildContent(theme),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyState(ThemeData theme) {
|
||||
Widget _buildContent(ThemeData theme) {
|
||||
if (_isInitializing) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const CircularProgressIndicator(),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Initialisation du chat...',
|
||||
style: theme.textTheme.bodyLarge,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (_initError != null) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
size: 64,
|
||||
color: theme.colorScheme.error,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Erreur d\'initialisation',
|
||||
style: theme.textTheme.headlineSmall?.copyWith(
|
||||
color: theme.colorScheme.error,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
_initError!,
|
||||
style: theme.textTheme.bodyMedium,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton.icon(
|
||||
onPressed: _initializeChat,
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: const Text('Réessayer'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (_isChatInitialized) {
|
||||
// Afficher directement le module chat
|
||||
return ChatModule.getRoomsPage();
|
||||
}
|
||||
|
||||
// État initial
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
@@ -193,70 +160,24 @@ class _UserCommunicationPageState extends State<UserCommunicationPage> {
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'Sélectionnez une conversation',
|
||||
'Chat non initialisé',
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
color: theme.colorScheme.onSurface.withOpacity(0.5),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Choisissez une conversation dans la liste\npour commencer à discuter',
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurface.withOpacity(0.3),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: _initializeChat,
|
||||
child: const Text('Initialiser le chat'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNoConversationsMessage(ThemeData theme) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.forum_outlined,
|
||||
size: 100,
|
||||
color: theme.colorScheme.primary.withOpacity(0.3),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'Aucune conversation',
|
||||
style: theme.textTheme.headlineSmall?.copyWith(
|
||||
color: theme.colorScheme.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Vous n\'avez pas encore de conversations.\nCommencez une discussion avec votre équipe !',
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
color: theme.colorScheme.onSurface.withOpacity(0.6),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
// TODO: Créer une nouvelle conversation
|
||||
},
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Démarrer une conversation'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: theme.colorScheme.primary,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
vertical: 16,
|
||||
),
|
||||
textStyle: const TextStyle(fontSize: 16),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@override
|
||||
void dispose() {
|
||||
// Ne pas disposer le chat ici car il est partagé
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:geosector_app/core/constants/app_keys.dart';
|
||||
import 'package:geosector_app/presentation/widgets/dashboard_layout.dart';
|
||||
import 'package:geosector_app/presentation/widgets/passages/passage_form.dart';
|
||||
import 'package:geosector_app/presentation/widgets/badged_navigation_destination.dart';
|
||||
|
||||
// Import des pages utilisateur
|
||||
import 'user_dashboard_home_page.dart';
|
||||
@@ -11,6 +12,7 @@ import 'user_statistics_page.dart';
|
||||
import 'user_history_page.dart';
|
||||
import 'user_communication_page.dart';
|
||||
import 'user_map_page.dart';
|
||||
import 'user_field_mode_page.dart';
|
||||
|
||||
class UserDashboardPage extends StatefulWidget {
|
||||
const UserDashboardPage({super.key});
|
||||
@@ -37,6 +39,7 @@ class _UserDashboardPageState extends State<UserDashboardPage> {
|
||||
const UserHistoryPage(),
|
||||
const UserCommunicationPage(),
|
||||
const UserMapPage(),
|
||||
const UserFieldModePage(),
|
||||
];
|
||||
|
||||
// Initialiser et charger les paramètres
|
||||
@@ -140,32 +143,38 @@ class _UserDashboardPageState extends State<UserDashboardPage> {
|
||||
_saveSettings(); // Sauvegarder l'index de page sélectionné
|
||||
});
|
||||
},
|
||||
destinations: const [
|
||||
NavigationDestination(
|
||||
destinations: [
|
||||
const NavigationDestination(
|
||||
icon: Icon(Icons.dashboard_outlined),
|
||||
selectedIcon: Icon(Icons.dashboard),
|
||||
label: 'Tableau de bord',
|
||||
),
|
||||
NavigationDestination(
|
||||
const NavigationDestination(
|
||||
icon: Icon(Icons.bar_chart_outlined),
|
||||
selectedIcon: Icon(Icons.bar_chart),
|
||||
label: 'Stats',
|
||||
),
|
||||
NavigationDestination(
|
||||
const NavigationDestination(
|
||||
icon: Icon(Icons.history_outlined),
|
||||
selectedIcon: Icon(Icons.history),
|
||||
label: 'Historique',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.chat_outlined),
|
||||
selectedIcon: Icon(Icons.chat),
|
||||
createBadgedNavigationDestination(
|
||||
icon: const Icon(Icons.chat_outlined),
|
||||
selectedIcon: const Icon(Icons.chat),
|
||||
label: 'Messages',
|
||||
showBadge: true,
|
||||
),
|
||||
NavigationDestination(
|
||||
const NavigationDestination(
|
||||
icon: Icon(Icons.map_outlined),
|
||||
selectedIcon: Icon(Icons.map),
|
||||
label: 'Carte',
|
||||
),
|
||||
const NavigationDestination(
|
||||
icon: Icon(Icons.explore_outlined),
|
||||
selectedIcon: Icon(Icons.explore),
|
||||
label: 'Terrain',
|
||||
),
|
||||
],
|
||||
onNewPassagePressed: () => _showPassageForm(context),
|
||||
body: _pages[_selectedIndex],
|
||||
|
||||
1003
app/lib/presentation/user/user_field_mode_page.dart
Normal file
1003
app/lib/presentation/user/user_field_mode_page.dart
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
import 'dart:math' as math;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
@@ -600,6 +601,8 @@ class _UserMapPageState extends State<UserMapPage> {
|
||||
initialPosition: _currentPosition,
|
||||
initialZoom: _currentZoom,
|
||||
mapController: _mapController,
|
||||
// Utiliser OpenStreetMap sur mobile, Mapbox sur web
|
||||
useOpenStreetMap: !kIsWeb,
|
||||
markers: _buildPassageMarkers(),
|
||||
polygons: _buildSectorPolygons(),
|
||||
showControls: true,
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:geosector_app/chat/services/chat_info_service.dart';
|
||||
|
||||
/// Fonction helper pour créer une NavigationDestination avec badge
|
||||
NavigationDestination createBadgedNavigationDestination({
|
||||
required Icon icon,
|
||||
required Icon selectedIcon,
|
||||
required String label,
|
||||
bool showBadge = false,
|
||||
}) {
|
||||
if (!showBadge) {
|
||||
return NavigationDestination(
|
||||
icon: icon,
|
||||
selectedIcon: selectedIcon,
|
||||
label: label,
|
||||
);
|
||||
}
|
||||
|
||||
// Créer les icônes avec badge
|
||||
final badgedIcon = BadgedIcon(
|
||||
icon: icon.icon!,
|
||||
showBadge: true,
|
||||
);
|
||||
|
||||
final badgedSelectedIcon = BadgedIcon(
|
||||
icon: selectedIcon.icon!,
|
||||
showBadge: true,
|
||||
);
|
||||
|
||||
return NavigationDestination(
|
||||
icon: badgedIcon,
|
||||
selectedIcon: badgedSelectedIcon,
|
||||
label: label,
|
||||
);
|
||||
}
|
||||
|
||||
/// Widget pour afficher un badge sur une icône
|
||||
class BadgedIcon extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final bool showBadge;
|
||||
final Color? color;
|
||||
final double? size;
|
||||
|
||||
const BadgedIcon({
|
||||
super.key,
|
||||
required this.icon,
|
||||
this.showBadge = false,
|
||||
this.color,
|
||||
this.size,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final iconWidget = Icon(icon, color: color, size: size);
|
||||
|
||||
if (!showBadge) {
|
||||
return iconWidget;
|
||||
}
|
||||
|
||||
return AnimatedBuilder(
|
||||
animation: ChatInfoService.instance,
|
||||
builder: (context, _) {
|
||||
final unreadCount = ChatInfoService.instance.unreadMessages;
|
||||
final badgeLabel = ChatInfoService.instance.badgeLabel;
|
||||
|
||||
if (unreadCount == 0) {
|
||||
return iconWidget;
|
||||
}
|
||||
|
||||
return Badge(
|
||||
label: Text(
|
||||
badgeLabel,
|
||||
style: const TextStyle(fontSize: 10),
|
||||
),
|
||||
backgroundColor: Colors.red,
|
||||
textColor: Colors.white,
|
||||
child: iconWidget,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:flutter_map_cache/flutter_map_cache.dart';
|
||||
import 'package:http_cache_file_store/http_cache_file_store.dart';
|
||||
@@ -46,6 +47,9 @@ class MapboxMap extends StatefulWidget {
|
||||
|
||||
/// Désactive le drag de la carte
|
||||
final bool disableDrag;
|
||||
|
||||
/// Utiliser OpenStreetMap au lieu de Mapbox (en cas de problème de token)
|
||||
final bool useOpenStreetMap;
|
||||
|
||||
const MapboxMap({
|
||||
super.key,
|
||||
@@ -60,6 +64,7 @@ class MapboxMap extends StatefulWidget {
|
||||
this.showControls = true,
|
||||
this.mapStyle,
|
||||
this.disableDrag = false,
|
||||
this.useOpenStreetMap = false,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -70,7 +75,8 @@ class _MapboxMapState extends State<MapboxMap> {
|
||||
/// Contrôleur de carte interne
|
||||
late final MapController _mapController;
|
||||
|
||||
/// Niveau de zoom actuel
|
||||
/// Niveau de zoom actuel (utilisé pour l'affichage futur)
|
||||
// ignore: unused_field
|
||||
double _currentZoom = 13.0;
|
||||
|
||||
/// Provider de cache pour les tuiles
|
||||
@@ -91,7 +97,9 @@ class _MapboxMapState extends State<MapboxMap> {
|
||||
Future<void> _initializeCache() async {
|
||||
try {
|
||||
final dir = await getTemporaryDirectory();
|
||||
final cacheStore = FileCacheStore('${dir.path}${Platform.pathSeparator}MapboxTileCache');
|
||||
// Utiliser un nom de cache différent selon le provider
|
||||
final cacheName = widget.useOpenStreetMap ? 'OSMTileCache' : 'MapboxTileCache';
|
||||
final cacheStore = FileCacheStore('${dir.path}${Platform.pathSeparator}$cacheName');
|
||||
|
||||
_tileProvider = CachedTileProvider(
|
||||
store: cacheStore,
|
||||
@@ -105,12 +113,14 @@ class _MapboxMapState extends State<MapboxMap> {
|
||||
_cacheInitialized = true;
|
||||
});
|
||||
}
|
||||
debugPrint('MapboxMap: Cache initialisé avec succès pour ${widget.useOpenStreetMap ? "OpenStreetMap" : "Mapbox"}');
|
||||
} catch (e) {
|
||||
debugPrint('Erreur lors de l\'initialisation du cache: $e');
|
||||
debugPrint('MapboxMap: Erreur lors de l\'initialisation du cache: $e');
|
||||
// En cas d'erreur, on continue sans cache
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_cacheInitialized = true;
|
||||
_tileProvider = null; // Utiliser NetworkTileProvider en fallback
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -138,7 +148,7 @@ class _MapboxMapState extends State<MapboxMap> {
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
color: Colors.black.withValues(alpha: 0.2),
|
||||
blurRadius: 6,
|
||||
offset: const Offset(0, 3),
|
||||
),
|
||||
@@ -155,19 +165,41 @@ class _MapboxMapState extends State<MapboxMap> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Déterminer l'URL du template de tuiles Mapbox
|
||||
// Utiliser l'environnement actuel pour obtenir la bonne clé API
|
||||
final String environment = ApiService.instance.getCurrentEnvironment();
|
||||
final String mapboxToken = AppKeys.getMapboxApiKey(environment);
|
||||
final String mapStyle = widget.mapStyle ?? 'mapbox/streets-v11';
|
||||
final String urlTemplate = 'https://api.mapbox.com/styles/v1/$mapStyle/tiles/256/{z}/{x}/{y}@2x?access_token=$mapboxToken';
|
||||
String urlTemplate;
|
||||
|
||||
if (widget.useOpenStreetMap) {
|
||||
// Utiliser OpenStreetMap comme alternative
|
||||
urlTemplate = 'https://tile.openstreetmap.org/{z}/{x}/{y}.png';
|
||||
debugPrint('MapboxMap: Utilisation d\'OpenStreetMap');
|
||||
} else {
|
||||
// Déterminer l'URL du template de tuiles Mapbox
|
||||
// Utiliser l'environnement actuel pour obtenir la bonne clé API
|
||||
final String environment = ApiService.instance.getCurrentEnvironment();
|
||||
final String mapboxToken = AppKeys.getMapboxApiKey(environment);
|
||||
|
||||
// Essayer différentes API Mapbox selon la plateforme
|
||||
if (kIsWeb) {
|
||||
// Sur web, on peut utiliser l'API styles
|
||||
urlTemplate = 'https://api.mapbox.com/styles/v1/mapbox/streets-v11/tiles/256/{z}/{x}/{y}@2x?access_token=$mapboxToken';
|
||||
} else {
|
||||
// Sur mobile, utiliser l'API v4 qui fonctionne mieux avec les tokens standards
|
||||
// Format: mapbox.streets pour les rues, mapbox.satellite pour satellite
|
||||
urlTemplate = 'https://api.tiles.mapbox.com/v4/mapbox.streets/{z}/{x}/{y}@2x.png?access_token=$mapboxToken';
|
||||
}
|
||||
|
||||
// Debug pour vérifier la configuration
|
||||
debugPrint('MapboxMap: Plateforme: ${kIsWeb ? "Web" : "Mobile"}');
|
||||
debugPrint('MapboxMap: Environnement: $environment');
|
||||
debugPrint('MapboxMap: Token: ${mapboxToken.substring(0, 10)}...'); // Afficher seulement le début du token
|
||||
debugPrint('MapboxMap: URL Template: ${urlTemplate.substring(0, 50)}...');
|
||||
}
|
||||
|
||||
// Afficher un indicateur pendant l'initialisation du cache
|
||||
if (!_cacheInitialized) {
|
||||
return Stack(
|
||||
children: [
|
||||
// Carte sans cache en attendant
|
||||
_buildMapContent(urlTemplate, mapboxToken),
|
||||
_buildMapContent(urlTemplate),
|
||||
// Indicateur discret
|
||||
const Positioned(
|
||||
top: 8,
|
||||
@@ -194,10 +226,10 @@ class _MapboxMapState extends State<MapboxMap> {
|
||||
);
|
||||
}
|
||||
|
||||
return _buildMapContent(urlTemplate, mapboxToken);
|
||||
return _buildMapContent(urlTemplate);
|
||||
}
|
||||
|
||||
Widget _buildMapContent(String urlTemplate, String mapboxToken) {
|
||||
Widget _buildMapContent(String urlTemplate) {
|
||||
return Stack(
|
||||
children: [
|
||||
// Carte principale
|
||||
@@ -232,13 +264,24 @@ class _MapboxMapState extends State<MapboxMap> {
|
||||
urlTemplate: urlTemplate,
|
||||
userAgentPackageName: 'app.geosector.fr',
|
||||
maxNativeZoom: 19,
|
||||
additionalOptions: {
|
||||
'accessToken': mapboxToken,
|
||||
},
|
||||
// Utilise le cache si disponible
|
||||
maxZoom: 20,
|
||||
minZoom: 1,
|
||||
// Retirer tileSize pour utiliser la valeur par défaut
|
||||
// Les additionalOptions ne sont pas nécessaires car le token est dans l'URL
|
||||
// Utilise le cache si disponible sur web, NetworkTileProvider sur mobile
|
||||
tileProvider: _cacheInitialized && _tileProvider != null
|
||||
? _tileProvider!
|
||||
: NetworkTileProvider(),
|
||||
: NetworkTileProvider(
|
||||
headers: {
|
||||
'User-Agent': 'geosector_app/3.1.3',
|
||||
'Accept': '*/*',
|
||||
},
|
||||
),
|
||||
errorTileCallback: (tile, error, stackTrace) {
|
||||
debugPrint('MapboxMap: Erreur de chargement de tuile: $error');
|
||||
debugPrint('MapboxMap: Coordonnées de la tuile: ${tile.coordinates}');
|
||||
debugPrint('MapboxMap: Stack trace: $stackTrace');
|
||||
},
|
||||
),
|
||||
|
||||
// Polygones
|
||||
|
||||
Reference in New Issue
Block a user