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:
2025-08-19 19:38:03 +02:00
parent 4f7247eb2d
commit 3443277d4a
185 changed files with 109354 additions and 102937 deletions

View File

@@ -1,107 +0,0 @@
/// Service API pour la communication avec le backend du chat
///
/// Ce service gère toutes les requêtes HTTP vers l'API chat
library;
class ChatApiService {
final String baseUrl;
final String? authToken;
ChatApiService({
required this.baseUrl,
this.authToken,
});
/// Récupère les conversations
Future<Map<String, dynamic>> fetchConversations() async {
// TODO: Implémenter la requête HTTP
throw UnimplementedError();
}
/// Récupère les messages d'une conversation
Future<Map<String, dynamic>> fetchMessages(String conversationId,
{int page = 1, int limit = 50}) async {
// TODO: Implémenter la requête HTTP
throw UnimplementedError();
}
/// Crée une nouvelle conversation
Future<Map<String, dynamic>> createConversation(
Map<String, dynamic> data) async {
// TODO: Implémenter la requête HTTP
throw UnimplementedError();
}
/// Envoie un message
Future<Map<String, dynamic>> sendMessage(
String conversationId, Map<String, dynamic> messageData) async {
// TODO: Implémenter la requête HTTP
throw UnimplementedError();
}
/// Marque un message comme lu
Future<Map<String, dynamic>> markMessageAsRead(String messageId) async {
// TODO: Implémenter la requête HTTP
throw UnimplementedError();
}
/// Ajoute un participant
Future<Map<String, dynamic>> addParticipant(
String conversationId, Map<String, dynamic> participantData) async {
// TODO: Implémenter la requête HTTP
throw UnimplementedError();
}
/// Retire un participant
Future<Map<String, dynamic>> removeParticipant(
String conversationId, String participantId) async {
// TODO: Implémenter la requête HTTP
throw UnimplementedError();
}
/// Crée un utilisateur anonyme
Future<Map<String, dynamic>> createAnonymousUser(
{String? name, String? email}) async {
// TODO: Implémenter la requête HTTP
throw UnimplementedError();
}
/// Récupère les annonces
Future<Map<String, dynamic>> fetchAnnouncements() async {
// TODO: Implémenter la requête HTTP
throw UnimplementedError();
}
/// Crée une annonce
Future<Map<String, dynamic>> createAnnouncement(
Map<String, dynamic> data) async {
// TODO: Implémenter la requête HTTP
throw UnimplementedError();
}
/// Récupère les statistiques d'une annonce
Future<Map<String, dynamic>> fetchAnnouncementStats(
String conversationId) async {
// TODO: Implémenter la requête HTTP
throw UnimplementedError();
}
/// Récupère les cibles d'audience disponibles
Future<Map<String, dynamic>> fetchAvailableAudienceTargets() async {
// TODO: Implémenter la requête HTTP
throw UnimplementedError();
}
/// Met à jour une conversation
Future<Map<String, dynamic>> updateConversation(
String id, Map<String, dynamic> data) async {
// TODO: Implémenter la requête HTTP
throw UnimplementedError();
}
/// Supprime une conversation
Future<void> deleteConversation(String id) async {
// TODO: Implémenter la requête HTTP
throw UnimplementedError();
}
}

View File

@@ -0,0 +1,238 @@
import 'package:flutter/services.dart';
import 'package:yaml/yaml.dart';
/// Classe pour charger et gérer la configuration du chat depuis chat_config.yaml
class ChatConfigLoader {
static ChatConfigLoader? _instance;
static ChatConfigLoader get instance => _instance ??= ChatConfigLoader._();
Map<String, dynamic>? _config;
ChatConfigLoader._();
/// Charger la configuration depuis le fichier YAML
Future<void> loadConfig() async {
try {
// Charger le fichier YAML
final yamlString = await rootBundle.loadString('lib/chat/chat_config.yaml');
// Vérifier que le contenu n'est pas vide
if (yamlString.isEmpty) {
print('Fichier de configuration chat vide, utilisation de la configuration par défaut');
_config = _getDefaultConfig();
return;
}
// Parser le YAML
dynamic yamlMap;
try {
yamlMap = loadYaml(yamlString);
} catch (parseError) {
print('Erreur de parsing YAML (utilisation de la config par défaut): $parseError');
print('Contenu YAML problématique (premiers 500 caractères): ${yamlString.substring(0, yamlString.length > 500 ? 500 : yamlString.length)}');
_config = _getDefaultConfig();
return;
}
// Convertir en Map<String, dynamic>
_config = _convertYamlToMap(yamlMap);
print('Configuration chat chargée avec succès');
} catch (e) {
print('Erreur lors du chargement de la configuration chat: $e');
// Utiliser une configuration par défaut en cas d'erreur
_config = _getDefaultConfig();
}
}
/// Convertir YamlMap en Map standard
dynamic _convertYamlToMap(dynamic yamlData) {
if (yamlData is YamlMap) {
final map = <String, dynamic>{};
yamlData.forEach((key, value) {
map[key.toString()] = _convertYamlToMap(value);
});
return map;
} else if (yamlData is YamlList) {
return yamlData.map((item) => _convertYamlToMap(item)).toList();
} else {
return yamlData;
}
}
/// Obtenir les permissions pour un rôle
Map<String, dynamic> getPermissionsForRole(int role) {
if (_config == null) {
return {};
}
final permissions = _config!['chat_permissions'] as Map<String, dynamic>?;
if (permissions == null) {
return {};
}
return permissions['role_$role'] as Map<String, dynamic>? ?? {};
}
/// Vérifier si un utilisateur peut envoyer un message à un autre
bool canSendMessageTo({
required int senderRole,
required int recipientRole,
int? senderEntite,
int? recipientEntite,
}) {
final permissions = getPermissionsForRole(senderRole);
final canMessageWith = permissions['can_message_with'] as List<dynamic>?;
if (canMessageWith == null) {
return false;
}
for (final rule in canMessageWith) {
if (rule['role'] == recipientRole) {
final condition = rule['condition'] as String?;
switch (condition) {
case 'same_entite':
return senderEntite != null &&
recipientEntite != null &&
senderEntite == recipientEntite;
case 'all':
return true;
default:
return false;
}
}
}
return false;
}
/// Obtenir les destinataires possibles pour un rôle
List<Map<String, dynamic>> getPossibleRecipientsConfig(int role) {
final permissions = getPermissionsForRole(role);
final canMessageWith = permissions['can_message_with'] as List<dynamic>?;
if (canMessageWith == null) {
return [];
}
return canMessageWith.map((rule) {
return {
'role': rule['role'],
'condition': rule['condition'],
'description': rule['description'],
'allow_selection': rule['allow_selection'] ?? false,
'allow_broadcast': rule['allow_broadcast'] ?? false,
};
}).toList();
}
/// Obtenir le nom d'un rôle
String getRoleName(int role) {
final permissions = getPermissionsForRole(role);
return permissions['name'] as String? ?? 'Utilisateur';
}
/// Obtenir la description d'un rôle
String getRoleDescription(int role) {
final permissions = getPermissionsForRole(role);
return permissions['description'] as String? ?? '';
}
/// Obtenir le texte d'aide pour un rôle
String getHelpText(int role) {
final permissions = getPermissionsForRole(role);
return permissions['help_text'] as String? ?? '';
}
/// Vérifier si un rôle peut créer des groupes
bool canCreateGroup(int role) {
final permissions = getPermissionsForRole(role);
return permissions['can_create_group'] as bool? ?? false;
}
/// Vérifier si un rôle peut faire du broadcast
bool canBroadcast(int role) {
final permissions = getPermissionsForRole(role);
return permissions['can_broadcast'] as bool? ?? false;
}
/// Obtenir la configuration UI
Map<String, dynamic> getUIConfig() {
return _config?['ui_config'] as Map<String, dynamic>? ?? {};
}
/// Obtenir les messages de l'interface
Map<String, dynamic> getUIMessages() {
final uiConfig = getUIConfig();
return uiConfig['messages'] as Map<String, dynamic>? ?? {};
}
/// Obtenir la couleur d'un rôle
String getRoleColor(int role) {
final uiConfig = getUIConfig();
final roleColors = uiConfig['role_colors'] as Map<String, dynamic>?;
return roleColors?[role.toString()] as String? ?? '#64748B';
}
/// Obtenir les informations du module
Map<String, dynamic> getModuleInfo() {
return _config?['module_info'] as Map<String, dynamic>? ?? {
'version': '1.0.0',
'name': 'Chat Module Light',
'description': 'Module de chat autonome et portable pour GEOSECTOR'
};
}
/// Obtenir la version du module
String getModuleVersion() {
final moduleInfo = getModuleInfo();
return moduleInfo['version'] as String? ?? '1.0.0';
}
/// Configuration par défaut si le fichier YAML n'est pas trouvé
Map<String, dynamic> _getDefaultConfig() {
return {
'chat_permissions': {
'role_1': {
'name': 'Membre',
'can_message_with': [
{'role': 1, 'condition': 'same_entite'},
{'role': 2, 'condition': 'same_entite'},
],
'can_create_group': false,
'can_broadcast': false,
'help_text': 'Vous pouvez discuter avec les membres de votre amicale',
},
'role_2': {
'name': 'Admin Amicale',
'can_message_with': [
{'role': 1, 'condition': 'same_entite'},
{'role': 2, 'condition': 'same_entite'},
{'role': 9, 'condition': 'all'},
],
'can_create_group': true,
'can_broadcast': false,
'help_text': 'Vous pouvez discuter avec les membres et les super admins',
},
'role_9': {
'name': 'Super Admin',
'can_message_with': [
{'role': 2, 'condition': 'all', 'allow_selection': true, 'allow_broadcast': true},
],
'can_create_group': true,
'can_broadcast': true,
'help_text': 'Vous pouvez envoyer des messages aux administrateurs d\'amicale',
},
},
'ui_config': {
'show_role_badge': true,
'enable_autocomplete': true,
'messages': {
'no_permission': 'Vous n\'avez pas la permission',
'search_placeholder': 'Rechercher...',
},
},
};
}
}

View File

@@ -0,0 +1,112 @@
import 'package:flutter/foundation.dart';
/// Service pour gérer les informations globales du chat (badges, stats)
/// Récupère les infos depuis l'API au login et les maintient à jour
class ChatInfoService extends ChangeNotifier {
static ChatInfoService? _instance;
static ChatInfoService get instance => _instance ??= ChatInfoService._();
ChatInfoService._();
// Stats du chat
int _totalRooms = 0;
int _unreadMessages = 0;
DateTime? _lastUpdate;
// Getters
int get totalRooms => _totalRooms;
int get unreadMessages => _unreadMessages;
DateTime? get lastUpdate => _lastUpdate;
bool get hasUnread => _unreadMessages > 0;
/// Met à jour les infos depuis la réponse de login
/// Attend une structure : { "chat": { "total_rooms": 5, "unread_messages": 12 } }
void updateFromLogin(Map<String, dynamic> loginData) {
debugPrint('📊 ChatInfoService: Mise à jour depuis login');
final chatData = loginData['chat'];
if (chatData != null && chatData is Map<String, dynamic>) {
_totalRooms = chatData['total_rooms'] ?? 0;
_unreadMessages = chatData['unread_messages'] ?? 0;
_lastUpdate = DateTime.now();
debugPrint('💬 Chat stats - Rooms: $_totalRooms, Non lus: $_unreadMessages');
notifyListeners();
} else {
debugPrint('⚠️ Pas de données chat dans la réponse login');
}
}
/// Met à jour directement les stats
void updateStats({int? totalRooms, int? unreadMessages}) {
bool hasChanged = false;
if (totalRooms != null && totalRooms != _totalRooms) {
_totalRooms = totalRooms;
hasChanged = true;
}
if (unreadMessages != null && unreadMessages != _unreadMessages) {
_unreadMessages = unreadMessages;
hasChanged = true;
}
if (hasChanged) {
_lastUpdate = DateTime.now();
notifyListeners();
}
}
/// Décrémente le nombre de messages non lus
void decrementUnread(int count) {
if (count > 0) {
_unreadMessages = (_unreadMessages - count).clamp(0, 999);
_lastUpdate = DateTime.now();
notifyListeners();
}
}
/// Marque tous les messages d'une room comme lus
void markRoomAsRead(int messagesCount) {
if (messagesCount > 0) {
decrementUnread(messagesCount);
}
}
/// Incrémente le nombre de messages non lus (nouveau message reçu)
void incrementUnread(int count) {
if (count > 0) {
_unreadMessages = (_unreadMessages + count).clamp(0, 999);
_lastUpdate = DateTime.now();
notifyListeners();
}
}
/// Réinitialise les stats (au logout)
void reset() {
_totalRooms = 0;
_unreadMessages = 0;
_lastUpdate = null;
notifyListeners();
}
/// Force un refresh des stats depuis l'API
Future<void> refreshFromApi() async {
// Cette méthode pourrait appeler directement l'API
// Pour l'instant on la laisse vide, elle sera utile plus tard
debugPrint('📊 ChatInfoService: Refresh depuis API demandé');
}
/// Retourne un label formaté pour le badge
String get badgeLabel {
if (_unreadMessages == 0) return '';
if (_unreadMessages > 99) return '99+';
return _unreadMessages.toString();
}
/// Debug info
@override
String toString() {
return 'ChatInfoService(rooms: $_totalRooms, unread: $_unreadMessages, lastUpdate: $_lastUpdate)';
}
}

View File

@@ -0,0 +1,468 @@
import 'dart:async';
import 'package:dio/dio.dart';
import 'package:hive_flutter/hive_flutter.dart';
import '../models/room.dart';
import '../models/message.dart';
import 'chat_config_loader.dart';
import 'chat_info_service.dart';
import 'package:geosector_app/core/constants/app_keys.dart';
/// Service de chat avec règles métier configurables via YAML
/// Les permissions sont définies dans chat_config.yaml
class ChatService {
static ChatService? _instance;
static ChatService get instance => _instance!;
late final Dio _dio;
late final Box<Room> _roomsBox;
late final Box<Message> _messagesBox;
late int _currentUserId;
late String _currentUserName;
late int _currentUserRole;
late int? _currentUserEntite;
String? _authToken;
Timer? _syncTimer;
/// Initialisation avec gestion des rôles et configuration YAML
static Future<void> init({
required String apiUrl,
required int userId,
required String userName,
required int userRole,
int? userEntite,
String? authToken,
}) async {
_instance = ChatService._();
// Charger la configuration depuis le YAML
await ChatConfigLoader.instance.loadConfig();
// Initialiser Hive
await Hive.initFlutter();
Hive.registerAdapter(RoomAdapter());
Hive.registerAdapter(MessageAdapter());
// Ouvrir les boxes en utilisant les constantes centralisées
_instance!._roomsBox = await Hive.openBox<Room>(AppKeys.chatRoomsBoxName);
_instance!._messagesBox = await Hive.openBox<Message>(AppKeys.chatMessagesBoxName);
// Configurer l'utilisateur
_instance!._currentUserId = userId;
_instance!._currentUserName = userName;
_instance!._currentUserRole = userRole;
_instance!._currentUserEntite = userEntite;
_instance!._authToken = authToken;
// Configurer Dio
_instance!._dio = Dio(BaseOptions(
baseUrl: apiUrl,
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 10),
headers: authToken != null ? {'Authorization': 'Bearer $authToken'} : {},
));
// Démarrer la synchronisation
_instance!._startSync();
}
ChatService._();
/// Obtenir les destinataires possibles selon le rôle
Future<List<Map<String, dynamic>>> getPossibleRecipients({String? search}) async {
try {
// L'API utilise le token pour identifier l'utilisateur et son rôle
String endpoint = '/chat/recipients';
final params = <String, dynamic>{};
if (search != null && search.isNotEmpty) {
params['search'] = search;
}
final response = await _dio.get(endpoint, queryParameters: params);
// Gérer différents formats de réponse
if (response.data is List) {
return List<Map<String, dynamic>>.from(response.data);
} else if (response.data is Map && response.data['recipients'] != null) {
return List<Map<String, dynamic>>.from(response.data['recipients']);
} else if (response.data is Map && response.data['data'] != null) {
return List<Map<String, dynamic>>.from(response.data['data']);
} else {
print('⚠️ Format inattendu pour /chat/recipients: ${response.data.runtimeType}');
return [];
}
} catch (e) {
print('⚠️ Erreur getPossibleRecipients: $e');
// Fallback sur logique locale selon le rôle
return _getLocalRecipients();
}
}
/// Logique locale de récupération des destinataires
List<Map<String, dynamic>> _getLocalRecipients() {
// Cette méthode devrait accéder à la box membres de l'app principale
// Pour l'instant on retourne une liste vide
return [];
}
/// Vérifier si l'utilisateur peut créer une conversation avec un destinataire
bool canCreateConversationWith(int recipientRole, {int? recipientEntite}) {
return ChatConfigLoader.instance.canSendMessageTo(
senderRole: _currentUserRole,
recipientRole: recipientRole,
senderEntite: _currentUserEntite,
recipientEntite: recipientEntite,
);
}
/// Obtenir les rooms filtrées selon les permissions
Future<List<Room>> getRooms() async {
try {
// L'API filtre automatiquement selon le token Bearer
final response = await _dio.get('/chat/rooms');
// Debug : afficher le type et le contenu de la réponse
print('📊 Type de réponse /chat/rooms: ${response.data.runtimeType}');
if (response.data is Map) {
print('📊 Clés de la réponse: ${(response.data as Map).keys.toList()}');
}
// Gérer différents formats de réponse API
List<dynamic> roomsData;
if (response.data is Map) {
// La plupart du temps, l'API retourne un objet avec status et rooms
if (response.data['rooms'] != null) {
roomsData = response.data['rooms'] as List;
print('✅ Réponse API: status=${response.data['status']}, ${roomsData.length} rooms');
} else if (response.data['data'] != null) {
roomsData = response.data['data'] as List;
print('✅ Réponse avec propriété "data" (${roomsData.length} rooms)');
} else {
// Pas de propriété rooms ou data, liste vide
print('⚠️ Réponse sans rooms ni data: ${response.data}');
roomsData = [];
}
} else if (response.data is List) {
// Si c'est directement une liste (moins courant)
roomsData = response.data as List;
print('✅ Réponse est directement une liste avec ${roomsData.length} rooms');
} else {
// Format complètement inattendu
print('⚠️ Format de réponse inattendu pour /chat/rooms: ${response.data.runtimeType}');
roomsData = [];
}
final rooms = roomsData
.map((json) => Room.fromJson(json))
.toList();
// Sauvegarder dans Hive
await _roomsBox.clear();
for (final room in rooms) {
await _roomsBox.put(room.id, room);
}
// Mettre à jour les stats globales
final totalUnread = rooms.fold<int>(0, (sum, room) => sum + room.unreadCount);
ChatInfoService.instance.updateStats(
totalRooms: rooms.length,
unreadMessages: totalUnread,
);
return rooms;
} catch (e) {
print('Erreur lors de la récupération des rooms (utilisation du cache): $e');
// Fallback sur le cache local en cas d'erreur API (404, etc.)
return _roomsBox.values.toList()
..sort((a, b) => (b.lastMessageAt ?? b.createdAt)
.compareTo(a.lastMessageAt ?? a.createdAt));
}
}
/// Créer une room avec vérification des permissions
Future<Room?> createRoom({
required String title,
required List<int> participantIds,
String? type,
String? initialMessage,
}) async {
try {
// Vérifier les permissions localement d'abord
// L'API fera aussi une vérification
final data = {
'title': title,
'type': type ?? (_currentUserRole == 9 ? 'broadcast' : 'private'),
'participants': participantIds,
// L'API récupère le rôle et l'entité depuis le token
};
// Ajouter le message initial s'il est fourni
if (initialMessage != null && initialMessage.isNotEmpty) {
data['initial_message'] = initialMessage;
}
final response = await _dio.post('/chat/rooms', data: data);
final room = Room.fromJson(response.data);
await _roomsBox.put(room.id, room);
return room;
} catch (e) {
return null;
}
}
/// Créer une conversation one-to-one
Future<Room?> createPrivateRoom({
required int recipientId,
required String recipientName,
required int recipientRole,
int? recipientEntite,
String? initialMessage,
}) async {
// Vérifier les permissions via la configuration
if (!canCreateConversationWith(recipientRole, recipientEntite: recipientEntite)) {
final messages = ChatConfigLoader.instance.getUIMessages();
throw Exception(messages['no_permission'] ?? 'Permission refusée');
}
return createRoom(
title: recipientName,
participantIds: [recipientId],
type: 'private',
initialMessage: initialMessage,
);
}
/// Créer une conversation de groupe (pour superadmin)
Future<Room?> createGroupRoom({
required String title,
required List<int> adminIds,
String? initialMessage,
}) async {
if (_currentUserRole != 9) {
throw Exception('Seuls les superadmins peuvent créer des groupes');
}
return createRoom(
title: title,
participantIds: adminIds,
type: 'broadcast',
initialMessage: initialMessage,
);
}
/// Obtenir les messages d'une room avec pagination
Future<Map<String, dynamic>> getMessages(String roomId, {String? beforeMessageId}) async {
try {
final params = <String, dynamic>{
'limit': 50,
};
if (beforeMessageId != null) {
params['before'] = beforeMessageId;
}
final response = await _dio.get('/chat/rooms/$roomId/messages', queryParameters: params);
// Gérer différents formats de réponse
List<dynamic> messagesData;
bool hasMore = false;
if (response.data is List) {
// Si c'est directement une liste de messages
messagesData = response.data as List;
} else if (response.data is Map) {
// Si c'est un objet avec messages et has_more
messagesData = response.data['messages'] ?? response.data['data'] ?? [];
hasMore = response.data['has_more'] ?? false;
} else {
print('⚠️ Format inattendu pour les messages: ${response.data.runtimeType}');
messagesData = [];
}
final messages = messagesData
.map((json) => Message.fromJson(json, _currentUserId))
.toList();
// Sauvegarder dans Hive (en limitant à 100 messages par room)
await _saveMessagesToCache(roomId, messages);
return {
'messages': messages,
'has_more': hasMore,
};
} catch (e) {
print('Erreur getMessages: $e');
// Fallback sur le cache local
final cachedMessages = _messagesBox.values
.where((m) => m.roomId == roomId)
.toList()
..sort((a, b) => a.sentAt.compareTo(b.sentAt));
return {
'messages': cachedMessages,
'has_more': false,
};
}
}
/// Sauvegarder les messages dans le cache en limitant à 100 par room
Future<void> _saveMessagesToCache(String roomId, List<Message> newMessages) async {
// Obtenir tous les messages existants pour cette room
final existingMessages = _messagesBox.values
.where((m) => m.roomId == roomId)
.toList()
..sort((a, b) => b.sentAt.compareTo(a.sentAt)); // Plus récent en premier
// Ajouter les nouveaux messages
for (final message in newMessages) {
await _messagesBox.put(message.id, message);
}
// Si on dépasse 100 messages, supprimer les plus anciens
final allMessages = [...existingMessages, ...newMessages]
..sort((a, b) => b.sentAt.compareTo(a.sentAt));
if (allMessages.length > 100) {
final messagesToDelete = allMessages.skip(100).toList();
for (final message in messagesToDelete) {
await _messagesBox.delete(message.id);
}
}
}
/// Envoyer un message
Future<Message?> sendMessage(String roomId, String content) async {
final tempId = 'temp_${DateTime.now().millisecondsSinceEpoch}';
final tempMessage = Message(
id: tempId,
roomId: roomId,
content: content,
senderId: _currentUserId,
senderName: _currentUserName,
sentAt: DateTime.now(),
isMe: true,
);
await _messagesBox.put(tempId, tempMessage);
try {
final response = await _dio.post(
'/chat/rooms/$roomId/messages',
data: {
'content': content,
// L'API récupère le sender depuis le token
},
);
final finalMessage = Message.fromJson(response.data, _currentUserId);
await _messagesBox.delete(tempId);
await _messagesBox.put(finalMessage.id, finalMessage);
// Mettre à jour la room
final room = _roomsBox.get(roomId);
if (room != null) {
final updatedRoom = Room(
id: room.id,
title: room.title,
type: room.type,
createdAt: room.createdAt,
lastMessage: content,
lastMessageAt: DateTime.now(),
unreadCount: 0,
);
await _roomsBox.put(roomId, updatedRoom);
}
return finalMessage;
} catch (e) {
return tempMessage;
}
}
/// Marquer comme lu
Future<void> markAsRead(String roomId) async {
try {
await _dio.post('/chat/rooms/$roomId/read');
final room = _roomsBox.get(roomId);
if (room != null) {
// Décrémenter les messages non lus dans ChatInfoService
if (room.unreadCount > 0) {
ChatInfoService.instance.decrementUnread(room.unreadCount);
}
final updatedRoom = Room(
id: room.id,
title: room.title,
type: room.type,
createdAt: room.createdAt,
lastMessage: room.lastMessage,
lastMessageAt: room.lastMessageAt,
unreadCount: 0,
);
await _roomsBox.put(roomId, updatedRoom);
}
} catch (e) {
// Ignorer
}
}
/// Synchronisation périodique
void _startSync() {
_syncTimer?.cancel();
_syncTimer = Timer.periodic(const Duration(seconds: 30), (_) {
getRooms();
});
}
/// Nettoyer les ressources
void dispose() {
_syncTimer?.cancel();
_dio.close();
}
// Getters
Box<Room> get roomsBox => _roomsBox;
Box<Message> get messagesBox => _messagesBox;
int get currentUserId => _currentUserId;
/// Obtenir le rôle de l'utilisateur actuel
int getUserRole() => _currentUserRole;
int get currentUserRole => _currentUserRole;
String get currentUserName => _currentUserName;
/// Obtenir le label du rôle
String getRoleLabel(int role) {
switch (role) {
case 1:
return 'Membre';
case 2:
return 'Admin Amicale';
case 9:
return 'Super Admin';
default:
return 'Utilisateur';
}
}
/// Obtenir la description des permissions
String getPermissionsDescription() {
switch (_currentUserRole) {
case 1:
return 'Vous pouvez discuter avec les membres de votre amicale';
case 2:
return 'Vous pouvez discuter avec les membres et les super admins';
case 9:
return 'Vous pouvez envoyer des messages aux administrateurs d\'amicale';
default:
return '';
}
}
}

View File

@@ -1,214 +0,0 @@
# Notifications MQTT pour le Chat GEOSECTOR
## Vue d'ensemble
Ce système de notifications utilise MQTT pour fournir des notifications push en temps réel pour le module chat. Il offre une alternative légère à Firebase Cloud Messaging (FCM) et peut être auto-hébergé dans votre infrastructure.
## Architecture
### Composants principaux
1. **MqttNotificationService** (Flutter)
- Service de notification côté client
- Gère la connexion au broker MQTT
- Traite les messages entrants
- Affiche les notifications locales
2. **MqttConfig** (Flutter)
- Configuration centralisée pour MQTT
- Gestion des topics
- Paramètres de connexion
3. **MqttNotificationSender** (PHP)
- Service backend pour envoyer les notifications
- Interface avec la base de données
- Gestion des cibles d'audience
## Configuration du broker MQTT
### Container Incus
Le broker MQTT (Eclipse Mosquitto recommandé) doit être installé dans votre container Incus :
```bash
# Installer Mosquitto
apt-get update
apt-get install mosquitto mosquitto-clients
# Configurer Mosquitto
vi /etc/mosquitto/mosquitto.conf
```
Configuration recommandée :
```
listener 1883
allow_anonymous false
password_file /etc/mosquitto/passwd
# Pour SSL/TLS
listener 8883
cafile /etc/mosquitto/ca.crt
certfile /etc/mosquitto/server.crt
keyfile /etc/mosquitto/server.key
```
### Sécurité
Pour un environnement de production, il est fortement recommandé :
1. D'utiliser SSL/TLS (port 8883)
2. De configurer l'authentification par mot de passe
3. De limiter les IPs pouvant se connecter
4. De configurer des ACLs pour restreindre l'accès aux topics
## Structure des topics MQTT
### Topics utilisateur
- `chat/user/{userId}/messages` - Messages personnels pour l'utilisateur
- `chat/user/{userId}/groups/{groupId}` - Messages des groupes de l'utilisateur
### Topics globaux
- `chat/announcement` - Annonces générales
- `chat/broadcast` - Diffusions à grande échelle
### Topics conversation
- `chat/conversation/{conversationId}` - Messages spécifiques à une conversation
## Intégration Flutter
### Dépendances requises
Ajoutez ces dépendances à votre `pubspec.yaml` :
```yaml
dependencies:
mqtt5_client: ^4.0.0 # ou mqtt_client selon votre préférence
flutter_local_notifications: ^17.0.0
```
### Initialisation
```dart
// Dans main.dart
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final notificationService = MqttNotificationService();
await notificationService.initialize(userId: currentUserId);
runApp(const GeoSectorApp());
}
```
### Utilisation
```dart
// Écouter les messages
notificationService.onMessageTap = (messageId) {
// Naviguer vers le message
Navigator.pushNamed(context, '/chat/$messageId');
};
// Publier un message
await notificationService.publishMessage(
'chat/user/$userId/messages',
{'content': 'Test message'},
);
```
## Gestion des notifications
### Paramètres utilisateur
Les utilisateurs peuvent configurer :
- Activation/désactivation des notifications
- Conversations en silencieux
- Mode "Ne pas déranger"
- Aperçu du contenu
### Persistance des notifications
Les notifications sont enregistrées dans la table `chat_notifications` pour :
- Traçabilité
- Statistiques
- Synchronisation
## Tests
### Test de connexion
```dart
final service = MqttNotificationService();
await service.initialize(userId: 'test_user');
// Vérifie les logs pour confirmer la connexion
```
### Test d'envoi
```php
$sender = new MqttNotificationSender($db, $mqttConfig);
$result = $sender->sendMessageNotification(
'receiver_id',
'sender_id',
'message_id',
'Test message',
'conversation_id'
);
```
## Surveillance et maintenance
### Logs
Les logs sont disponibles dans :
- Logs Flutter (console debug)
- Logs Mosquitto (`/var/log/mosquitto/mosquitto.log`)
- Logs PHP (selon configuration)
### Métriques à surveiller
- Nombre de connexions actives
- Latence des messages
- Taux d'échec des notifications
- Consommation mémoire/CPU du broker
## Comparaison avec Firebase
### Avantages MQTT
1. **Auto-hébergé** : Contrôle total de l'infrastructure
2. **Léger** : Moins de ressources que Firebase
3. **Coût** : Gratuit (uniquement coûts d'infrastructure)
4. **Personnalisable** : Configuration fine du broker
### Inconvénients
1. **Maintenance** : Nécessite une gestion du broker
2. **Évolutivité** : Requiert dimensionnement et clustering
3. **Fonctionnalités** : Moins de services intégrés que Firebase
## Évolutions futures
1. **WebSocket** : Ajout optionnel pour temps réel strict
2. **Clustering** : Pour haute disponibilité
3. **Analytics** : Dashboard de monitoring
4. **Webhooks** : Intégration avec d'autres services
## Dépannage
### Problèmes courants
1. **Connexion échouée**
- Vérifier username/password
- Vérifier port/hostname
- Vérifier firewall
2. **Messages non reçus**
- Vérifier abonnement aux topics
- Vérifier QoS
- Vérifier paramètres notifications
3. **Performance dégradée**
- Augmenter keepAlive
- Ajuster reconnectInterval
- Vérifier charge serveur

View File

@@ -1,205 +0,0 @@
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter/foundation.dart';
/// Service de gestion des notifications chat
///
/// Gère l'envoi et la réception des notifications pour le module chat
class ChatNotificationService {
static final ChatNotificationService _instance =
ChatNotificationService._internal();
factory ChatNotificationService() => _instance;
ChatNotificationService._internal();
final FirebaseMessaging _firebaseMessaging = FirebaseMessaging.instance;
final FlutterLocalNotificationsPlugin _localNotifications =
FlutterLocalNotificationsPlugin();
// Callback pour les actions sur les notifications
Function(String messageId)? onMessageTap;
Function(Map<String, dynamic>)? onBackgroundMessage;
/// Initialise le service de notifications
Future<void> initialize() async {
// Demander les permissions
await _requestPermissions();
// Initialiser les notifications locales
await _initializeLocalNotifications();
// Configurer les handlers de messages
_configureFirebaseHandlers();
// Obtenir le token du device
await _initializeDeviceToken();
}
/// Demande les permissions pour les notifications
Future<bool> _requestPermissions() async {
NotificationSettings settings = await _firebaseMessaging.requestPermission(
alert: true,
badge: true,
sound: true,
provisional: false,
);
return settings.authorizationStatus == AuthorizationStatus.authorized;
}
/// Initialise les notifications locales
Future<void> _initializeLocalNotifications() async {
const AndroidInitializationSettings androidSettings =
AndroidInitializationSettings('@mipmap/ic_launcher');
final DarwinInitializationSettings iosSettings =
DarwinInitializationSettings(
requestAlertPermission: true,
requestBadgePermission: true,
requestSoundPermission: true,
onDidReceiveLocalNotification: _onDidReceiveLocalNotification,
);
final InitializationSettings initSettings = InitializationSettings(
android: androidSettings,
iOS: iosSettings,
);
await _localNotifications.initialize(
initSettings,
onDidReceiveNotificationResponse: _onNotificationTap,
);
}
/// Configure les handlers Firebase
void _configureFirebaseHandlers() {
// Message reçu quand l'app est au premier plan
FirebaseMessaging.onMessage.listen(_onForegroundMessage);
// Message reçu quand l'app est en arrière-plan
FirebaseMessaging.onMessageOpenedApp.listen(_onBackgroundMessageOpened);
// Handler pour les messages en arrière-plan terminé
FirebaseMessaging.onBackgroundMessage(_firebaseBackgroundHandler);
}
/// Handler pour les messages reçus au premier plan
Future<void> _onForegroundMessage(RemoteMessage message) async {
if (message.notification != null) {
// Afficher une notification locale
await _showLocalNotification(
title: message.notification!.title ?? 'Nouveau message',
body: message.notification!.body ?? '',
payload: message.data['messageId'] ?? '',
);
}
}
/// Handler pour les messages ouverts depuis l'arrière-plan
void _onBackgroundMessageOpened(RemoteMessage message) {
final messageId = message.data['messageId'];
if (messageId != null) {
onMessageTap?.call(messageId);
}
}
/// Affiche une notification locale
Future<void> _showLocalNotification({
required String title,
required String body,
required String payload,
}) async {
const AndroidNotificationDetails androidDetails =
AndroidNotificationDetails(
'chat_messages',
'Messages de chat',
channelDescription: 'Notifications pour les nouveaux messages de chat',
importance: Importance.high,
priority: Priority.high,
icon: '@mipmap/ic_launcher',
);
const DarwinNotificationDetails iosDetails = DarwinNotificationDetails(
presentAlert: true,
presentBadge: true,
presentSound: true,
);
const NotificationDetails notificationDetails = NotificationDetails(
android: androidDetails,
iOS: iosDetails,
);
await _localNotifications.show(
DateTime.now().microsecondsSinceEpoch,
title,
body,
notificationDetails,
payload: payload,
);
}
/// Handler pour le clic sur une notification
void _onNotificationTap(NotificationResponse response) {
final payload = response.payload;
if (payload != null) {
onMessageTap?.call(payload);
}
}
/// Handler pour les notifications iOS reçues au premier plan
void _onDidReceiveLocalNotification(
int id, String? title, String? body, String? payload) {
// Traitement spécifique iOS si nécessaire
}
/// Obtient et stocke le token du device
Future<String?> _initializeDeviceToken() async {
String? token = await _firebaseMessaging.getToken();
// Envoyer le token au serveur pour stocker
await _sendTokenToServer(token);
// Écouter les changements de token
_firebaseMessaging.onTokenRefresh.listen(_sendTokenToServer);
return token;
}
/// Envoie le token FCM au serveur
Future<void> _sendTokenToServer(String token) async {
try {
// Appel API pour enregistrer le token
// await chatApiService.registerDeviceToken(token);
debugPrint('Device token enregistré : $token');
} catch (e) {
debugPrint('Erreur lors de l\'enregistrement du token : $e');
}
}
/// S'abonner aux notifications pour une conversation
Future<void> subscribeToConversation(String conversationId) async {
await _firebaseMessaging.subscribeToTopic('chat_$conversationId');
}
/// Se désabonner des notifications pour une conversation
Future<void> unsubscribeFromConversation(String conversationId) async {
await _firebaseMessaging.unsubscribeFromTopic('chat_$conversationId');
}
/// Désactive temporairement les notifications
Future<void> pauseNotifications() async {
await _firebaseMessaging.setAutoInitEnabled(false);
}
/// Réactive les notifications
Future<void> resumeNotifications() async {
await _firebaseMessaging.setAutoInitEnabled(true);
}
}
/// Handler pour les messages en arrière-plan
@pragma('vm:entry-point')
Future<void> _firebaseBackgroundHandler(RemoteMessage message) async {
// Traitement des messages en arrière-plan
debugPrint('Message reçu en arrière-plan : ${message.messageId}');
}

View File

@@ -1,75 +0,0 @@
/// Configuration pour le broker MQTT
///
/// Centralise les paramètres de connexion au broker MQTT
library;
class MqttConfig {
// Configuration du serveur MQTT
static const String host = 'mqtt.geosector.fr';
static const int port = 1883;
static const int securePort = 8883;
static const bool useSsl = false;
// Configuration d'authentification
static const String username = 'geosector_chat';
static const String password = 'secure_password_here';
// Préfixes des topics MQTT
static const String topicBase = 'chat';
static const String topicUserMessages = '$topicBase/user';
static const String topicAnnouncements = '$topicBase/announcement';
static const String topicGroups = '$topicBase/groups';
static const String topicConversations = '$topicBase/conversation';
// Configuration des sessions
static const int keepAliveInterval = 60;
static const int reconnectInterval = 5;
static const bool cleanSession = true;
// Configuration des notifications
static const int notificationRetryCount = 3;
static const Duration notificationTimeout = Duration(seconds: 30);
/// Génère un client ID unique pour chaque session
static String generateClientId(String userId) {
return 'chat_${userId}_${DateTime.now().millisecondsSinceEpoch}';
}
/// Retourne l'URL complète du broker selon la configuration SSL
static String get brokerUrl {
if (useSsl) {
return '$host:$securePort';
} else {
return '$host:$port';
}
}
/// Retourne le topic pour les messages d'un utilisateur
static String getUserMessageTopic(String userId) {
return '$topicUserMessages/$userId/messages';
}
/// Retourne le topic pour les annonces globales
static String getAnnouncementTopic() {
return topicAnnouncements;
}
/// Retourne le topic pour une conversation spécifique
static String getConversationTopic(String conversationId) {
return '$topicConversations/$conversationId';
}
/// Retourne le topic pour un groupe spécifique
static String getGroupTopic(String groupId) {
return '$topicGroups/$groupId';
}
/// Retourne les topics auxquels un utilisateur doit s'abonner
static List<String> getUserSubscriptionTopics(String userId) {
return [
getUserMessageTopic(userId),
getAnnouncementTopic(),
// Ajoutez d'autres topics selon les besoins
];
}
}

View File

@@ -1,329 +0,0 @@
import 'dart:async';
import 'dart:convert';
import 'package:mqtt5_client/mqtt5_client.dart';
import 'package:mqtt5_client/mqtt5_server_client.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
/// Service de gestion des notifications chat via MQTT
///
/// Utilise MQTT pour recevoir des notifications en temps réel
/// et afficher des notifications locales
class MqttNotificationService {
static final MqttNotificationService _instance =
MqttNotificationService._internal();
factory MqttNotificationService() => _instance;
MqttNotificationService._internal();
late MqttServerClient _client;
final FlutterLocalNotificationsPlugin _localNotifications =
FlutterLocalNotificationsPlugin();
// Configuration
final String mqttHost;
final int mqttPort;
final String mqttUsername;
final String mqttPassword;
final String clientId;
// État
bool _initialized = false;
String? _userId;
StreamSubscription? _messageSubscription;
// Callbacks
Function(String messageId)? onMessageTap;
Function(Map<String, dynamic>)? onNotificationReceived;
MqttNotificationService({
this.mqttHost = 'mqtt.geosector.fr',
this.mqttPort = 1883,
this.mqttUsername = '',
this.mqttPassword = '',
String? clientId,
}) : clientId = clientId ??
'geosector_chat_${DateTime.now().millisecondsSinceEpoch}';
/// Initialise le service de notifications
Future<void> initialize({required String userId}) async {
if (_initialized) return;
_userId = userId;
// Initialiser les notifications locales
await _initializeLocalNotifications();
// Initialiser le client MQTT
await _initializeMqttClient();
_initialized = true;
}
/// Initialise le client MQTT
Future<void> _initializeMqttClient() async {
try {
_client = MqttServerClient.withPort(mqttHost, clientId, mqttPort);
_client.logging(on: kDebugMode);
_client.keepAlivePeriod = 60;
_client.onConnected = _onConnected;
_client.onDisconnected = _onDisconnected;
_client.onSubscribed = _onSubscribed;
_client.autoReconnect = true;
// Configurer les options de connexion
final connMessage = MqttConnectMessage()
.authenticateAs(mqttUsername, mqttPassword)
.withClientIdentifier(clientId)
.startClean()
.keepAliveFor(60);
_client.connectionMessage = connMessage;
// Se connecter
await _connect();
} catch (e) {
debugPrint('Erreur lors de l\'initialisation MQTT : $e');
rethrow;
}
}
/// Se connecte au broker MQTT
Future<void> _connect() async {
try {
await _client.connect();
} catch (e) {
debugPrint('Erreur de connexion MQTT : $e');
_client.disconnect();
rethrow;
}
}
/// Callback lors de la connexion
void _onConnected() {
debugPrint('Connecté au broker MQTT');
// S'abonner aux topics de l'utilisateur
if (_userId != null) {
_subscribeToUserTopics(_userId!);
}
// Écouter les messages
_messageSubscription = _client.updates.listen(_onMessageReceived);
}
/// Callback lors de la déconnexion
void _onDisconnected() {
debugPrint('Déconnecté du broker MQTT');
// Tenter une reconnexion
if (_client.autoReconnect) {
Future.delayed(const Duration(seconds: 5), () {
_connect();
});
}
}
/// Callback lors de l'abonnement
void _onSubscribed(MqttSubscription subscription) {
debugPrint('Abonné au topic : ${subscription.topic.rawTopic}');
}
/// S'abonner aux topics de l'utilisateur
void _subscribeToUserTopics(String userId) {
// Topic pour les messages personnels
_client.subscribe('chat/user/$userId/messages', MqttQos.atLeastOnce);
// Topic pour les annonces
_client.subscribe('chat/announcement', MqttQos.atLeastOnce);
// Topic pour les groupes de l'utilisateur (si disponibles)
_client.subscribe('chat/user/$userId/groups/+', MqttQos.atLeastOnce);
}
/// Gère les messages reçus
void _onMessageReceived(List<MqttReceivedMessage<MqttMessage>> messages) {
for (var message in messages) {
final topic = message.topic;
final payload = message.payload as MqttPublishMessage;
final messageText =
MqttUtilities.bytesToStringAsString(payload.payload.message!);
try {
final data = jsonDecode(messageText) as Map<String, dynamic>;
_handleNotification(topic, data);
} catch (e) {
debugPrint('Erreur lors du décodage du message : $e');
}
}
}
/// Traite la notification reçue
Future<void> _handleNotification(
String topic, Map<String, dynamic> data) async {
// Vérifier les paramètres de notification de l'utilisateur
if (!await _shouldShowNotification(data)) {
return;
}
String title = '';
String body = '';
String messageId = '';
String conversationId = '';
if (topic.startsWith('chat/user/')) {
// Message personnel
title = data['senderName'] ?? 'Nouveau message';
body = data['content'] ?? '';
messageId = data['messageId'] ?? '';
conversationId = data['conversationId'] ?? '';
} else if (topic.startsWith('chat/announcement')) {
// Annonce
title = data['title'] ?? 'Annonce';
body = data['content'] ?? '';
messageId = data['messageId'] ?? '';
conversationId = data['conversationId'] ?? '';
}
// Afficher la notification locale
await _showLocalNotification(
title: title,
body: body,
payload: jsonEncode({
'messageId': messageId,
'conversationId': conversationId,
}),
);
// Appeler le callback si défini
onNotificationReceived?.call(data);
}
/// Vérifie si la notification doit être affichée
Future<bool> _shouldShowNotification(Map<String, dynamic> data) async {
// TODO: Vérifier les paramètres de notification de l'utilisateur
// - Notifications désactivées
// - Conversation en silencieux
// - Mode Ne pas déranger
return true;
}
/// Initialise les notifications locales
Future<void> _initializeLocalNotifications() async {
const androidSettings =
AndroidInitializationSettings('@mipmap/ic_launcher');
const iosSettings = DarwinInitializationSettings(
requestAlertPermission: true,
requestBadgePermission: true,
requestSoundPermission: true,
);
const initSettings = InitializationSettings(
android: androidSettings,
iOS: iosSettings,
);
await _localNotifications.initialize(
initSettings,
onDidReceiveNotificationResponse: _onNotificationTap,
);
}
/// Affiche une notification locale
Future<void> _showLocalNotification({
required String title,
required String body,
required String payload,
}) async {
const androidDetails = AndroidNotificationDetails(
'chat_messages',
'Messages de chat',
channelDescription: 'Notifications pour les nouveaux messages de chat',
importance: Importance.high,
priority: Priority.high,
icon: '@mipmap/ic_launcher',
);
const iosDetails = DarwinNotificationDetails(
presentAlert: true,
presentBadge: true,
presentSound: true,
);
const notificationDetails = NotificationDetails(
android: androidDetails,
iOS: iosDetails,
);
await _localNotifications.show(
DateTime.now().microsecondsSinceEpoch,
title,
body,
notificationDetails,
payload: payload,
);
}
/// Handler pour le clic sur une notification
void _onNotificationTap(NotificationResponse response) {
final payload = response.payload;
if (payload != null) {
try {
final data = jsonDecode(payload) as Map<String, dynamic>;
final messageId = data['messageId'] as String?;
if (messageId != null) {
onMessageTap?.call(messageId);
}
} catch (e) {
debugPrint('Erreur lors du traitement du clic sur notification : $e');
}
}
}
/// Publie un message MQTT
Future<void> publishMessage(
String topic, Map<String, dynamic> message) async {
if (_client.connectionStatus?.state != MqttConnectionState.connected) {
await _connect();
}
final messagePayload = jsonEncode(message);
final builder = MqttPayloadBuilder();
builder.addString(messagePayload);
_client.publishMessage(topic, MqttQos.atLeastOnce, builder.payload!);
}
/// S'abonner à une conversation spécifique
Future<void> subscribeToConversation(String conversationId) async {
if (_client.connectionStatus?.state == MqttConnectionState.connected) {
_client.subscribe(
'chat/conversation/$conversationId', MqttQos.atLeastOnce);
}
}
/// Se désabonner d'une conversation
Future<void> unsubscribeFromConversation(String conversationId) async {
if (_client.connectionStatus?.state == MqttConnectionState.connected) {
_client.unsubscribeStringTopic('chat/conversation/$conversationId');
}
}
/// Désactive temporairement les notifications
void pauseNotifications() {
_client.pause();
}
/// Réactive les notifications
void resumeNotifications() {
_client.resume();
}
/// Libère les ressources
void dispose() {
_messageSubscription?.cancel();
_client.disconnect();
_initialized = false;
}
}

View File

@@ -1,48 +0,0 @@
/// Service de gestion de la file d'attente hors ligne
///
/// Ce service gère les opérations chat en mode hors ligne
/// et les synchronise lorsque la connexion revient
library;
class OfflineQueueService {
// TODO: Ajouter le service de connectivité
OfflineQueueService();
/// Ajoute une opération en attente
Future<void> addPendingOperation(
String operationType, Map<String, dynamic> data) async {
// TODO: Implémenter l'ajout à la file d'attente
throw UnimplementedError();
}
/// Traite les opérations en attente
Future<void> processPendingOperations() async {
// TODO: Implémenter le traitement des opérations
throw UnimplementedError();
}
/// Écoute les changements de connectivité
void listenToConnectivityChanges() {
// TODO: Implémenter l'écoute des changements
throw UnimplementedError();
}
/// Vérifie si une opération est en file d'attente
bool hasOperationInQueue(String operationType, String id) {
// TODO: Implémenter la vérification
throw UnimplementedError();
}
/// Supprime une opération de la file d'attente
Future<void> removeOperationFromQueue(String operationType, String id) async {
// TODO: Implémenter la suppression
throw UnimplementedError();
}
/// Dispose des ressources
void dispose() {
// TODO: Implémenter le dispose
throw UnimplementedError();
}
}