import 'dart:async'; import 'dart:convert'; import 'package:unionflow_mobile_apps/core/network/api_client.dart'; import 'package:flutter/foundation.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; import '../models/dashboard_stats_model.dart'; import '../../../../core/storage/dashboard_cache_manager.dart'; /// Service de mode hors ligne avec synchronisation pour le Dashboard class DashboardOfflineService { static const String _offlineQueueKey = 'dashboard_offline_queue'; static const String _lastSyncKey = 'dashboard_last_sync'; final DashboardCacheManager _cacheManager; final ApiClient _apiClient; final Connectivity _connectivity = Connectivity(); SharedPreferences? _prefs; StreamSubscription>? _connectivitySubscription; Timer? _syncTimer; final StreamController _statusController = StreamController.broadcast(); final StreamController _syncController = StreamController.broadcast(); final List _pendingActions = []; bool _isOnline = true; bool _isSyncing = false; DateTime? _lastSyncTime; // Streams publics Stream get statusStream => _statusController.stream; Stream get syncStream => _syncController.stream; DashboardOfflineService(this._cacheManager, this._apiClient); /// Initialise le service hors ligne Future initialize() async { debugPrint('📱 Initialisation du service hors ligne...'); _prefs = await SharedPreferences.getInstance(); // Charger les actions en attente await _loadPendingActions(); // Charger la dernière synchronisation _loadLastSyncTime(); // Vérifier la connectivité initiale final connectivityResult = await _connectivity.checkConnectivity(); _updateConnectivityStatus(connectivityResult); // Écouter les changements de connectivité _connectivitySubscription = _connectivity.onConnectivityChanged.listen( (List results) => _updateConnectivityStatus(results), ); // Démarrer la synchronisation automatique _startAutoSync(); debugPrint('✅ Service hors ligne initialisé'); } /// Met à jour le statut de connectivité void _updateConnectivityStatus(dynamic result) { final wasOnline = _isOnline; if (result is List) { _isOnline = result.any((r) => r != ConnectivityResult.none); } else if (result is ConnectivityResult) { _isOnline = result != ConnectivityResult.none; } else { _isOnline = false; } debugPrint('🌐 Connectivité: ${_isOnline ? 'En ligne' : 'Hors ligne'}'); _statusController.add(OfflineStatus( isOnline: _isOnline, pendingActionsCount: _pendingActions.length, lastSyncTime: _lastSyncTime, )); // Si on revient en ligne, synchroniser if (!wasOnline && _isOnline && _pendingActions.isNotEmpty) { _syncPendingActions(); } } /// Démarre la synchronisation automatique void _startAutoSync() { _syncTimer = Timer.periodic( const Duration(minutes: 5), (_) { if (_isOnline && _pendingActions.isNotEmpty) { _syncPendingActions(); } }, ); } /// Ajoute une action à la queue hors ligne Future queueAction(OfflineAction action) async { _pendingActions.add(action); await _savePendingActions(); debugPrint('📝 Action mise en queue: ${action.type} (${_pendingActions.length} en attente)'); _statusController.add(OfflineStatus( isOnline: _isOnline, pendingActionsCount: _pendingActions.length, lastSyncTime: _lastSyncTime, )); // Si en ligne, essayer de synchroniser immédiatement if (_isOnline) { _syncPendingActions(); } } /// Synchronise les actions en attente Future _syncPendingActions() async { if (_isSyncing || _pendingActions.isEmpty || !_isOnline) { return; } _isSyncing = true; debugPrint('🔄 Début de la synchronisation (${_pendingActions.length} actions)'); _syncController.add(SyncProgress( isActive: true, totalActions: _pendingActions.length, completedActions: 0, currentAction: _pendingActions.first.type.toString(), )); final actionsToSync = List.from(_pendingActions); int completedCount = 0; for (final action in actionsToSync) { try { await _executeAction(action); _pendingActions.remove(action); completedCount++; _syncController.add(SyncProgress( isActive: true, totalActions: actionsToSync.length, completedActions: completedCount, currentAction: completedCount < actionsToSync.length ? actionsToSync[completedCount].type.toString() : null, )); debugPrint('✅ Action synchronisée: ${action.type}'); } catch (e) { debugPrint('❌ Erreur lors de la synchronisation de ${action.type}: $e'); // Marquer l'action comme échouée si trop de tentatives action.retryCount++; if (action.retryCount >= 3) { _pendingActions.remove(action); debugPrint('🗑️ Action abandonnée après 3 tentatives: ${action.type}'); } } } await _savePendingActions(); _lastSyncTime = DateTime.now(); await _saveLastSyncTime(); _syncController.add(SyncProgress( isActive: false, totalActions: actionsToSync.length, completedActions: completedCount, currentAction: null, )); _statusController.add(OfflineStatus( isOnline: _isOnline, pendingActionsCount: _pendingActions.length, lastSyncTime: _lastSyncTime, )); _isSyncing = false; debugPrint('✅ Synchronisation terminée ($completedCount/${actionsToSync.length} réussies)'); } /// Exécute une action spécifique Future _executeAction(OfflineAction action) async { switch (action.type) { case OfflineActionType.refreshDashboard: await _syncDashboardData(action); break; case OfflineActionType.updatePreferences: await _syncUserPreferences(action); break; case OfflineActionType.markActivityRead: await _syncActivityRead(action); break; case OfflineActionType.joinEvent: await _syncEventJoin(action); break; case OfflineActionType.exportReport: await _syncReportExport(action); break; } } /// Synchronise les données du dashboard (rafraîchit le cache) Future _syncDashboardData(OfflineAction action) async { final orgId = action.data['organizationId'] as String?; final userId = action.data['userId'] as String?; if (orgId == null || userId == null) return; final response = await _apiClient.get('/api/dashboard/stats', queryParameters: { 'organisationId': orgId, }); if (response.statusCode == 200 && response.data != null) { await _cacheManager.setKey( 'dashboard_${orgId}_$userId', response.data as Map, ); } } /// Synchronise les préférences utilisateur Future _syncUserPreferences(OfflineAction action) async { final userId = action.data['userId'] as String?; final preferences = action.data['preferences'] as Map?; if (userId == null || preferences == null) return; await _apiClient.put('/api/membres/$userId/preferences', data: preferences); } /// Synchronise le marquage d'activité comme lue Future _syncActivityRead(OfflineAction action) async { final activityId = action.data['activityId'] as String?; if (activityId == null) return; await _apiClient.put('/api/notifications/$activityId/read'); } /// Synchronise l'inscription à un événement Future _syncEventJoin(OfflineAction action) async { final eventId = action.data['eventId'] as String?; final membreId = action.data['membreId'] as String?; if (eventId == null || membreId == null) return; await _apiClient.post('/api/evenements/$eventId/inscription', data: { 'membreId': membreId, }); } /// Synchronise l'export de rapport Future _syncReportExport(OfflineAction action) async { final reportType = action.data['reportType'] as String?; final params = action.data['params'] as Map?; if (reportType == null) return; await _apiClient.post('/api/export/$reportType', data: params ?? {}); } /// Sauvegarde les actions en attente Future _savePendingActions() async { if (_prefs == null) return; final actionsJson = _pendingActions .map((action) => action.toJson()) .toList(); await _prefs!.setString(_offlineQueueKey, jsonEncode(actionsJson)); } /// Charge les actions en attente Future _loadPendingActions() async { if (_prefs == null) return; final actionsJsonString = _prefs!.getString(_offlineQueueKey); if (actionsJsonString != null) { try { final actionsJson = jsonDecode(actionsJsonString) as List; _pendingActions.clear(); _pendingActions.addAll( actionsJson.map((json) => OfflineAction.fromJson(json)), ); debugPrint('📋 ${_pendingActions.length} actions chargées depuis le cache'); } catch (e) { debugPrint('❌ Erreur lors du chargement des actions: $e'); await _prefs!.remove(_offlineQueueKey); } } } /// Sauvegarde l'heure de dernière synchronisation Future _saveLastSyncTime() async { if (_prefs == null || _lastSyncTime == null) return; await _prefs!.setInt(_lastSyncKey, _lastSyncTime!.millisecondsSinceEpoch); } /// Charge l'heure de dernière synchronisation void _loadLastSyncTime() { if (_prefs == null) return; final lastSyncMs = _prefs!.getInt(_lastSyncKey); if (lastSyncMs != null) { _lastSyncTime = DateTime.fromMillisecondsSinceEpoch(lastSyncMs); } } /// Force une synchronisation manuelle Future forceSync() async { if (!_isOnline) { throw Exception('Impossible de synchroniser hors ligne'); } await _syncPendingActions(); } /// Obtient les données en mode hors ligne Future getOfflineData( String organizationId, String userId, ) async { final m = _cacheManager.getKey>('dashboard_${organizationId}_$userId'); return m != null ? DashboardDataModel.fromJson(m) : null; } /// Vérifie si des données sont disponibles hors ligne Future hasOfflineData(String organizationId, String userId) async { final data = await getOfflineData(organizationId, userId); return data != null; } /// Obtient les statistiques du mode hors ligne OfflineStats getStats() { return OfflineStats( isOnline: _isOnline, pendingActionsCount: _pendingActions.length, lastSyncTime: _lastSyncTime, isSyncing: _isSyncing, cacheStats: _cacheManager.getCacheStats(), ); } /// Nettoie les anciennes actions Future cleanupOldActions() async { final cutoffTime = DateTime.now().subtract(const Duration(days: 7)); _pendingActions.removeWhere((action) => action.timestamp.isBefore(cutoffTime)); await _savePendingActions(); } /// Libère les ressources void dispose() { _connectivitySubscription?.cancel(); _syncTimer?.cancel(); _statusController.close(); _syncController.close(); } } /// Action hors ligne class OfflineAction { final String id; final OfflineActionType type; final Map data; final DateTime timestamp; int retryCount; OfflineAction({ required this.id, required this.type, required this.data, required this.timestamp, this.retryCount = 0, }); factory OfflineAction.fromJson(Map json) { return OfflineAction( id: json['id'] as String, type: OfflineActionType.values.firstWhere( (t) => t.name == json['type'], ), data: json['data'] as Map, timestamp: DateTime.parse(json['timestamp'] as String), retryCount: json['retryCount'] as int? ?? 0, ); } Map toJson() { return { 'id': id, 'type': type.name, 'data': data, 'timestamp': timestamp.toIso8601String(), 'retryCount': retryCount, }; } } /// Types d'actions hors ligne enum OfflineActionType { refreshDashboard, updatePreferences, markActivityRead, joinEvent, exportReport, } /// Statut hors ligne class OfflineStatus { final bool isOnline; final int pendingActionsCount; final DateTime? lastSyncTime; const OfflineStatus({ required this.isOnline, required this.pendingActionsCount, this.lastSyncTime, }); String get statusText { if (isOnline) { if (pendingActionsCount > 0) { return 'En ligne - $pendingActionsCount actions en attente'; } else { return 'En ligne - Synchronisé'; } } else { return 'Hors ligne - Mode cache activé'; } } } /// Progression de synchronisation class SyncProgress { final bool isActive; final int totalActions; final int completedActions; final String? currentAction; const SyncProgress({ required this.isActive, required this.totalActions, required this.completedActions, this.currentAction, }); double get progress { if (totalActions == 0) return 1.0; return completedActions / totalActions; } String get progressText { if (!isActive) return 'Synchronisation terminée'; if (currentAction != null) { return 'Synchronisation: $currentAction ($completedActions/$totalActions)'; } return 'Synchronisation en cours... ($completedActions/$totalActions)'; } } /// Statistiques du mode hors ligne class OfflineStats { final bool isOnline; final int pendingActionsCount; final DateTime? lastSyncTime; final bool isSyncing; final Map cacheStats; const OfflineStats({ required this.isOnline, required this.pendingActionsCount, this.lastSyncTime, required this.isSyncing, required this.cacheStats, }); String get lastSyncText { if (lastSyncTime == null) return 'Jamais synchronisé'; final now = DateTime.now(); final diff = now.difference(lastSyncTime!); if (diff.inMinutes < 1) return 'Synchronisé à l\'instant'; if (diff.inMinutes < 60) return 'Synchronisé il y a ${diff.inMinutes}min'; if (diff.inHours < 24) return 'Synchronisé il y a ${diff.inHours}h'; return 'Synchronisé il y a ${diff.inDays}j'; } }