392 lines
11 KiB
Dart
392 lines
11 KiB
Dart
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<DashboardStatsModel> _statsController =
|
|
StreamController<DashboardStatsModel>.broadcast();
|
|
final StreamController<RecentActivityModel> _activityController =
|
|
StreamController<RecentActivityModel>.broadcast();
|
|
final StreamController<UpcomingEventModel> _eventController =
|
|
StreamController<UpcomingEventModel>.broadcast();
|
|
final StreamController<DashboardNotification> _notificationController =
|
|
StreamController<DashboardNotification>.broadcast();
|
|
final StreamController<ConnectionStatus> _connectionController =
|
|
StreamController<ConnectionStatus>.broadcast();
|
|
|
|
// Getters pour les streams
|
|
Stream<DashboardStatsModel> get statsStream => _statsController.stream;
|
|
Stream<RecentActivityModel> get activityStream => _activityController.stream;
|
|
Stream<UpcomingEventModel> get eventStream => _eventController.stream;
|
|
Stream<DashboardNotification> get notificationStream => _notificationController.stream;
|
|
Stream<ConnectionStatus> get connectionStream => _connectionController.stream;
|
|
|
|
/// Initialise le service de notifications
|
|
Future<void> 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<void> _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<String> 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<String> 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<void> reconnect(String organizationId, String userId) async {
|
|
await disconnect();
|
|
_reconnectAttempts = 0;
|
|
await _connect(organizationId, userId);
|
|
}
|
|
|
|
/// Déconnecte le service
|
|
Future<void> 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<String, dynamic>? 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<String, dynamic> 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<String, dynamic>?,
|
|
actionUrl: json['actionUrl'] as String?,
|
|
isRead: json['isRead'] as bool? ?? false,
|
|
);
|
|
}
|
|
|
|
Map<String, dynamic> 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,
|
|
}
|