import 'dart:async'; import 'dart:convert'; 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 '../cache/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'; static const String _offlineModeKey = 'dashboard_offline_mode'; final DashboardCacheManager _cacheManager; 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); /// 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 Future _syncDashboardData(OfflineAction action) async { // TODO: Implémenter la synchronisation des données await Future.delayed(const Duration(milliseconds: 500)); // Simulation } /// Synchronise les préférences utilisateur Future _syncUserPreferences(OfflineAction action) async { // TODO: Implémenter la synchronisation des préférences await Future.delayed(const Duration(milliseconds: 300)); // Simulation } /// Synchronise le marquage d'activité comme lue Future _syncActivityRead(OfflineAction action) async { // TODO: Implémenter la synchronisation du marquage await Future.delayed(const Duration(milliseconds: 200)); // Simulation } /// Synchronise l'inscription à un événement Future _syncEventJoin(OfflineAction action) async { // TODO: Implémenter la synchronisation d'inscription await Future.delayed(const Duration(milliseconds: 400)); // Simulation } /// Synchronise l'export de rapport Future _syncReportExport(OfflineAction action) async { // TODO: Implémenter la synchronisation d'export await Future.delayed(const Duration(milliseconds: 800)); // Simulation } /// 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 forcSync() 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 { return await _cacheManager.getCachedDashboardData(organizationId, userId); } /// 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'; } }