import 'dart:async'; import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; import '../models/dashboard_stats_model.dart'; import '../../config/dashboard_config.dart'; /// Service de notifications temps réel pour le Dashboard class DashboardNotificationService { static const String _wsEndpoint = 'ws://localhost:8080/ws/dashboard'; WebSocketChannel? _channel; StreamSubscription? _subscription; Timer? _reconnectTimer; Timer? _heartbeatTimer; bool _isConnected = false; bool _shouldReconnect = true; int _reconnectAttempts = 0; static const int _maxReconnectAttempts = 5; static const Duration _reconnectDelay = Duration(seconds: 5); static const Duration _heartbeatInterval = Duration(seconds: 30); // Streams pour les différents types de notifications final StreamController _statsController = StreamController.broadcast(); final StreamController _activityController = StreamController.broadcast(); final StreamController _eventController = StreamController.broadcast(); final StreamController _notificationController = StreamController.broadcast(); final StreamController _connectionController = StreamController.broadcast(); // Getters pour les streams Stream get statsStream => _statsController.stream; Stream get activityStream => _activityController.stream; Stream get eventStream => _eventController.stream; Stream get notificationStream => _notificationController.stream; Stream get connectionStream => _connectionController.stream; /// Initialise le service de notifications Future initialize(String organizationId, String userId) async { if (!DashboardConfig.enableNotifications) { debugPrint('📱 Notifications désactivées dans la configuration'); return; } debugPrint('📱 Initialisation du service de notifications...'); await _connect(organizationId, userId); } /// Établit la connexion WebSocket Future _connect(String organizationId, String userId) async { if (_isConnected) return; try { final uri = Uri.parse('$_wsEndpoint?orgId=$organizationId&userId=$userId'); _channel = WebSocketChannel.connect(uri); debugPrint('📱 Connexion WebSocket en cours...'); _connectionController.add(ConnectionStatus.connecting); // Écouter les messages _subscription = _channel!.stream.listen( _handleMessage, onError: _handleError, onDone: _handleDisconnection, ); _isConnected = true; _reconnectAttempts = 0; _connectionController.add(ConnectionStatus.connected); // Démarrer le heartbeat _startHeartbeat(); debugPrint('✅ Connexion WebSocket établie'); } catch (e) { debugPrint('❌ Erreur de connexion WebSocket: $e'); _connectionController.add(ConnectionStatus.error); _scheduleReconnect(organizationId, userId); } } /// Gère les messages reçus void _handleMessage(dynamic message) { try { final data = jsonDecode(message as String); final type = data['type'] as String?; final payload = data['payload']; debugPrint('📨 Message reçu: $type'); switch (type) { case 'stats_update': final stats = DashboardStatsModel.fromJson(payload); _statsController.add(stats); break; case 'new_activity': final activity = RecentActivityModel.fromJson(payload); _activityController.add(activity); break; case 'event_update': final event = UpcomingEventModel.fromJson(payload); _eventController.add(event); break; case 'notification': final notification = DashboardNotification.fromJson(payload); _notificationController.add(notification); break; case 'pong': // Réponse au heartbeat debugPrint('💓 Heartbeat reçu'); break; default: debugPrint('⚠️ Type de message inconnu: $type'); } } catch (e) { debugPrint('❌ Erreur de parsing du message: $e'); } } /// Gère les erreurs de connexion void _handleError(error) { debugPrint('❌ Erreur WebSocket: $error'); _isConnected = false; _connectionController.add(ConnectionStatus.error); } /// Gère la déconnexion void _handleDisconnection() { debugPrint('🔌 Connexion WebSocket fermée'); _isConnected = false; _connectionController.add(ConnectionStatus.disconnected); if (_shouldReconnect) { // Programmer une reconnexion _scheduleReconnect('', ''); // Les IDs seront récupérés du contexte } } /// Programme une tentative de reconnexion void _scheduleReconnect(String organizationId, String userId) { if (_reconnectAttempts >= _maxReconnectAttempts) { debugPrint('❌ Nombre maximum de tentatives de reconnexion atteint'); _connectionController.add(ConnectionStatus.failed); return; } _reconnectAttempts++; final delay = _reconnectDelay * _reconnectAttempts; debugPrint('🔄 Reconnexion programmée dans ${delay.inSeconds}s (tentative $_reconnectAttempts)'); _reconnectTimer = Timer(delay, () { if (_shouldReconnect) { _connect(organizationId, userId); } }); } /// Démarre le heartbeat void _startHeartbeat() { _heartbeatTimer = Timer.periodic(_heartbeatInterval, (timer) { if (_isConnected && _channel != null) { try { _channel!.sink.add(jsonEncode({ 'type': 'ping', 'timestamp': DateTime.now().toIso8601String(), })); } catch (e) { debugPrint('❌ Erreur lors de l\'envoi du heartbeat: $e'); } } }); } /// Envoie une demande de rafraîchissement void requestRefresh(String organizationId, String userId) { if (_isConnected && _channel != null) { try { _channel!.sink.add(jsonEncode({ 'type': 'refresh_request', 'payload': { 'organizationId': organizationId, 'userId': userId, 'timestamp': DateTime.now().toIso8601String(), }, })); debugPrint('📤 Demande de rafraîchissement envoyée'); } catch (e) { debugPrint('❌ Erreur lors de l\'envoi de la demande: $e'); } } } /// S'abonne aux notifications pour un type spécifique void subscribeToNotifications(List notificationTypes) { if (_isConnected && _channel != null) { try { _channel!.sink.add(jsonEncode({ 'type': 'subscribe', 'payload': { 'notificationTypes': notificationTypes, 'timestamp': DateTime.now().toIso8601String(), }, })); debugPrint('📋 Abonnement aux notifications: $notificationTypes'); } catch (e) { debugPrint('❌ Erreur lors de l\'abonnement: $e'); } } } /// Se désabonne des notifications void unsubscribeFromNotifications(List notificationTypes) { if (_isConnected && _channel != null) { try { _channel!.sink.add(jsonEncode({ 'type': 'unsubscribe', 'payload': { 'notificationTypes': notificationTypes, 'timestamp': DateTime.now().toIso8601String(), }, })); debugPrint('📋 Désabonnement des notifications: $notificationTypes'); } catch (e) { debugPrint('❌ Erreur lors du désabonnement: $e'); } } } /// Obtient le statut de la connexion bool get isConnected => _isConnected; /// Obtient le nombre de tentatives de reconnexion int get reconnectAttempts => _reconnectAttempts; /// Force une reconnexion Future reconnect(String organizationId, String userId) async { await disconnect(); _reconnectAttempts = 0; await _connect(organizationId, userId); } /// Déconnecte le service Future disconnect() async { _shouldReconnect = false; _reconnectTimer?.cancel(); _heartbeatTimer?.cancel(); if (_channel != null) { await _channel!.sink.close(); _channel = null; } await _subscription?.cancel(); _subscription = null; _isConnected = false; _connectionController.add(ConnectionStatus.disconnected); debugPrint('🔌 Service de notifications déconnecté'); } /// Libère les ressources void dispose() { disconnect(); _statsController.close(); _activityController.close(); _eventController.close(); _notificationController.close(); _connectionController.close(); } } /// Statut de la connexion enum ConnectionStatus { disconnected, connecting, connected, error, failed, } /// Notification du dashboard class DashboardNotification { final String id; final String type; final String title; final String message; final NotificationPriority priority; final DateTime timestamp; final Map? data; final String? actionUrl; final bool isRead; const DashboardNotification({ required this.id, required this.type, required this.title, required this.message, required this.priority, required this.timestamp, this.data, this.actionUrl, this.isRead = false, }); factory DashboardNotification.fromJson(Map json) { return DashboardNotification( id: json['id'] as String, type: json['type'] as String, title: json['title'] as String, message: json['message'] as String, priority: NotificationPriority.values.firstWhere( (p) => p.name == json['priority'], orElse: () => NotificationPriority.normal, ), timestamp: DateTime.parse(json['timestamp'] as String), data: json['data'] as Map?, actionUrl: json['actionUrl'] as String?, isRead: json['isRead'] as bool? ?? false, ); } Map toJson() { return { 'id': id, 'type': type, 'title': title, 'message': message, 'priority': priority.name, 'timestamp': timestamp.toIso8601String(), 'data': data, 'actionUrl': actionUrl, 'isRead': isRead, }; } /// Obtient l'icône pour le type de notification String get icon { switch (type) { case 'new_member': return '👤'; case 'new_event': return '📅'; case 'contribution': return '💰'; case 'urgent': return '🚨'; case 'system': return '⚙️'; default: return '📢'; } } /// Obtient la couleur pour la priorité String get priorityColor { switch (priority) { case NotificationPriority.low: return '#6B7280'; case NotificationPriority.normal: return '#3B82F6'; case NotificationPriority.high: return '#F59E0B'; case NotificationPriority.urgent: return '#EF4444'; } } } /// Priorité des notifications enum NotificationPriority { low, normal, high, urgent, }