Refactoring - Version OK

This commit is contained in:
dahoud
2025-11-17 16:02:04 +00:00
parent 3f00a26308
commit 3b9ffac8cd
198 changed files with 18010 additions and 11383 deletions

View File

@@ -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,
}