Refactoring - Version OK
This commit is contained in:
@@ -0,0 +1,391 @@
|
||||
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,
|
||||
}
|
||||
Reference in New Issue
Block a user