import 'package:flutter/foundation.dart'; import 'dart:async'; import 'package:em2rp/models/equipment_model.dart'; import 'package:em2rp/services/data_service.dart'; import 'package:em2rp/services/api_service.dart'; import 'package:em2rp/utils/debug_log.dart'; class EquipmentProvider extends ChangeNotifier { final DataService _dataService = DataService(FirebaseFunctionsApiService()); // Timer pour le debouncing de la recherche Timer? _searchDebounceTimer; // Liste paginée pour la page de gestion List _paginatedEquipment = []; bool _hasMore = true; bool _isLoadingMore = false; String? _lastVisible; // Cache complet pour getEquipmentsByIds et compatibilité List _equipment = []; List _models = []; List _brands = []; // Filtres et recherche EquipmentCategory? _selectedCategory; EquipmentStatus? _selectedStatus; String? _selectedModel; String _searchQuery = ''; bool _isLoading = false; bool _isInitialized = false; // Mode de chargement (pagination vs full) bool _usePagination = false; EquipmentProvider(); // Getters List get equipment => _usePagination ? _paginatedEquipment : _filteredEquipment; List get allEquipment => _equipment; List get models => _models; List get brands => _brands; EquipmentCategory? get selectedCategory => _selectedCategory; EquipmentStatus? get selectedStatus => _selectedStatus; String? get selectedModel => _selectedModel; String get searchQuery => _searchQuery; bool get isLoading => _isLoading; bool get isLoadingMore => _isLoadingMore; bool get hasMore => _hasMore; bool get isInitialized => _isInitialized; bool get usePagination => _usePagination; /// S'assure que les équipements sont chargés (charge si nécessaire) Future ensureLoaded() async { // Si déjà en train de charger, attendre if (_isLoading) { print('[EquipmentProvider] Equipment loading in progress, waiting...'); return; } // Si initialisé MAIS _equipment est vide, forcer le rechargement if (_isInitialized && _equipment.isEmpty) { print('[EquipmentProvider] Equipment marked as initialized but _equipment is empty! Force reloading...'); _isInitialized = false; // Réinitialiser le flag await loadEquipments(); return; } // Si déjà initialisé avec des données, ne rien faire if (_isInitialized) { print('[EquipmentProvider] Equipment already loaded (${_equipment.length} items), skipping...'); return; } print('[EquipmentProvider] Equipment not loaded, loading now...'); await loadEquipments(); } /// Charger tous les équipements via l'API (utilisé par les dialogs et sélection) Future loadEquipments() async { print('[EquipmentProvider] Starting to load ALL equipments...'); _isLoading = true; notifyListeners(); try { _equipment.clear(); String? lastVisible; bool hasMore = true; int pageCount = 0; // Charger toutes les pages en boucle while (hasMore) { pageCount++; print('[EquipmentProvider] Loading page $pageCount...'); final result = await _dataService.getEquipmentsPaginated( limit: 100, // Charger 100 par page pour aller plus vite startAfter: lastVisible, sortBy: 'id', sortOrder: 'asc', ); final equipmentsData = result['equipments'] as List; print('[EquipmentProvider] Page $pageCount: ${equipmentsData.length} equipments'); final pageEquipments = equipmentsData.map((data) { final id = data['id'] as String; return EquipmentModel.fromMap(data as Map, id); }).toList(); _equipment.addAll(pageEquipments); hasMore = result['hasMore'] as bool? ?? false; lastVisible = result['lastVisible'] as String?; if (!hasMore) { print('[EquipmentProvider] All pages loaded. Total: ${_equipment.length} equipments'); } } // Extraire les modèles et marques uniques _extractUniqueValues(); _isInitialized = true; _isLoading = false; notifyListeners(); print('[EquipmentProvider] Equipment loading complete: ${_equipment.length} equipments'); } catch (e) { print('[EquipmentProvider] Error loading equipments: $e'); _isLoading = false; notifyListeners(); rethrow; } } /// Charge plusieurs équipements par leurs IDs (optimisé pour les détails d'événement) Future> getEquipmentsByIds(List equipmentIds) async { if (equipmentIds.isEmpty) return []; print('[EquipmentProvider] Loading ${equipmentIds.length} equipments by IDs...'); try { // Vérifier d'abord le cache local final cachedEquipments = []; final missingIds = []; for (final id in equipmentIds) { final cached = _equipment.firstWhere( (eq) => eq.id == id, orElse: () => EquipmentModel( id: '', name: '', category: EquipmentCategory.other, status: EquipmentStatus.available, maintenanceIds: [], createdAt: DateTime.now(), updatedAt: DateTime.now(), ), ); if (cached.id.isNotEmpty) { cachedEquipments.add(cached); } else { missingIds.add(id); } } print('[EquipmentProvider] Found ${cachedEquipments.length} in cache, ${missingIds.length} missing'); // Si tous sont en cache, retourner directement if (missingIds.isEmpty) { return cachedEquipments; } // Charger les manquants depuis l'API final equipmentsData = await _dataService.getEquipmentsByIds(missingIds); final loadedEquipments = equipmentsData.map((data) { final id = data['id'] as String; // L'ID vient du backend return EquipmentModel.fromMap(data, id); }).toList(); // Ajouter au cache for (final eq in loadedEquipments) { if (!_equipment.any((e) => e.id == eq.id)) { _equipment.add(eq); } } print('[EquipmentProvider] Loaded ${loadedEquipments.length} equipments from API'); // Retourner tous les équipements (cache + chargés) return [...cachedEquipments, ...loadedEquipments]; } catch (e) { print('[EquipmentProvider] Error loading equipments by IDs: $e'); rethrow; } } /// Extraire modèles et marques uniques void _extractUniqueValues() { final modelSet = {}; final brandSet = {}; for (final eq in _equipment) { if (eq.model != null && eq.model!.isNotEmpty) { modelSet.add(eq.model!); } if (eq.brand != null && eq.brand!.isNotEmpty) { brandSet.add(eq.brand!); } } _models = modelSet.toList()..sort(); _brands = brandSet.toList()..sort(); } /// Obtenir les équipements filtrés List get _filteredEquipment { var filtered = _equipment; if (_selectedCategory != null) { filtered = filtered.where((eq) => eq.category == _selectedCategory).toList(); } if (_selectedStatus != null) { filtered = filtered.where((eq) => eq.status == _selectedStatus).toList(); } if (_selectedModel != null && _selectedModel!.isNotEmpty) { filtered = filtered.where((eq) => eq.model == _selectedModel).toList(); } if (_searchQuery.isNotEmpty) { final query = _searchQuery.toLowerCase(); filtered = filtered.where((eq) { return eq.name.toLowerCase().contains(query) || eq.id.toLowerCase().contains(query) || (eq.model?.toLowerCase().contains(query) ?? false) || (eq.brand?.toLowerCase().contains(query) ?? false); }).toList(); } return filtered; } // ============================================================================ // PAGINATION - Nouvelles méthodes // ============================================================================ /// Active le mode pagination (pour la page de gestion) void enablePagination() { if (!_usePagination) { _usePagination = true; DebugLog.info('[EquipmentProvider] Pagination mode enabled'); } } /// Désactive le mode pagination (pour les autres pages) void disablePagination() { if (_usePagination) { _usePagination = false; DebugLog.info('[EquipmentProvider] Pagination mode disabled'); } } /// Charge la première page (réinitialise tout) Future loadFirstPage() async { DebugLog.info('[EquipmentProvider] Loading first page...'); _paginatedEquipment.clear(); _lastVisible = null; _hasMore = true; _isLoading = true; notifyListeners(); try { await loadNextPage(); _isInitialized = true; } catch (e) { DebugLog.error('[EquipmentProvider] Error loading first page', e); _isLoading = false; notifyListeners(); rethrow; } } /// Charge la page suivante (scroll infini) Future loadNextPage() async { if (_isLoadingMore || !_hasMore) { DebugLog.info('[EquipmentProvider] Skip loadNextPage: isLoadingMore=$_isLoadingMore, hasMore=$_hasMore'); return; } DebugLog.info('[EquipmentProvider] Loading next page... (current: ${_paginatedEquipment.length})'); _isLoadingMore = true; _isLoading = true; notifyListeners(); try { final result = await _dataService.getEquipmentsPaginated( limit: 20, startAfter: _lastVisible, category: _selectedCategory?.name, status: _selectedStatus?.name, searchQuery: _searchQuery.isNotEmpty ? _searchQuery : null, sortBy: 'id', sortOrder: 'asc', ); final newEquipments = (result['equipments'] as List) .map((data) { final map = data as Map; final id = map['id'] as String; // L'ID vient du backend dans le JSON return EquipmentModel.fromMap(map, id); }) .toList(); _paginatedEquipment.addAll(newEquipments); _hasMore = result['hasMore'] as bool? ?? false; _lastVisible = result['lastVisible'] as String?; DebugLog.info('[EquipmentProvider] Loaded ${newEquipments.length} equipments, total: ${_paginatedEquipment.length}, hasMore: $_hasMore'); _isLoadingMore = false; _isLoading = false; notifyListeners(); } catch (e) { DebugLog.error('[EquipmentProvider] Error loading next page', e); _isLoadingMore = false; _isLoading = false; notifyListeners(); rethrow; } } /// Recharge en changeant de filtre ou recherche Future reload() async { DebugLog.info('[EquipmentProvider] Reloading with new filters...'); await loadFirstPage(); } /// Définir le filtre de catégorie void setSelectedCategory(EquipmentCategory? category) async { if (_selectedCategory == category) return; _selectedCategory = category; if (_usePagination) { await reload(); } else { notifyListeners(); } } /// Définir le filtre de statut void setSelectedStatus(EquipmentStatus? status) async { if (_selectedStatus == status) return; _selectedStatus = status; if (_usePagination) { await reload(); } else { notifyListeners(); } } /// Définir le filtre de modèle void setSelectedModel(String? model) async { if (_selectedModel == model) return; _selectedModel = model; if (_usePagination) { await reload(); } else { notifyListeners(); } } /// Définir la requête de recherche (avec debouncing) void setSearchQuery(String query) { if (_searchQuery == query) return; _searchQuery = query; // Annuler le timer précédent _searchDebounceTimer?.cancel(); if (_usePagination) { // Attendre 500ms avant de recharger (debouncing) _searchDebounceTimer = Timer(const Duration(milliseconds: 500), () { reload(); }); } else { notifyListeners(); } } @override void dispose() { _searchDebounceTimer?.cancel(); super.dispose(); } /// Réinitialiser tous les filtres void clearFilters() async { _selectedCategory = null; _selectedStatus = null; _selectedModel = null; _searchQuery = ''; if (_usePagination) { await reload(); } else { notifyListeners(); } } // ============================================================================ // MÉTHODES COMPATIBILITÉ (pour ancien code) // ============================================================================ /// Recharger les équipements (ancien système) Future refresh() async { if (_usePagination) { await reload(); } else { await loadEquipments(); } } /// Stream des équipements (pour compatibilité avec ancien code) Stream> get equipmentStream async* { if (!_isInitialized && !_usePagination) { await loadEquipments(); } yield equipment; } /// Supprimer un équipement Future deleteEquipment(String equipmentId) async { try { await _dataService.deleteEquipment(equipmentId); if (_usePagination) { await reload(); } else { await loadEquipments(); } } catch (e) { DebugLog.error('[EquipmentProvider] Error deleting equipment', e); rethrow; } } /// Ajouter un équipement Future addEquipment(EquipmentModel equipment) async { try { await _dataService.createEquipment(equipment.id, equipment.toMap()); if (_usePagination) { await reload(); } else { await loadEquipments(); } } catch (e) { DebugLog.error('[EquipmentProvider] Error adding equipment', e); rethrow; } } /// Mettre à jour un équipement Future updateEquipment(EquipmentModel equipment) async { try { await _dataService.updateEquipment(equipment.id, equipment.toMap()); if (_usePagination) { await reload(); } else { await loadEquipments(); } } catch (e) { DebugLog.error('[EquipmentProvider] Error updating equipment', e); rethrow; } } /// Charger les marques Future loadBrands() async { await ensureLoaded(); _extractUniqueValues(); } /// Charger les modèles Future loadModels() async { await ensureLoaded(); _extractUniqueValues(); } /// Charger les modèles d'une marque spécifique Future> loadModelsByBrand(String brand) async { await ensureLoaded(); return _equipment .where((eq) => eq.brand?.toLowerCase() == brand.toLowerCase()) .map((eq) => eq.model ?? '') .where((model) => model.isNotEmpty) .toSet() .toList() ..sort(); } /// Charger les sous-catégories d'une catégorie spécifique Future> loadSubCategoriesByCategory(EquipmentCategory category) async { await ensureLoaded(); return _equipment .where((eq) => eq.category == category) .map((eq) => eq.subCategory ?? '') .where((sub) => sub.isNotEmpty) .toSet() .toList() ..sort(); } /// Calculer le statut réel d'un équipement (pour badge) EquipmentStatus calculateRealStatus(EquipmentModel equipment) { // Pour les consommables/câbles, vérifier le seuil critique if (equipment.category == EquipmentCategory.consumable || equipment.category == EquipmentCategory.cable) { final availableQty = equipment.availableQuantity ?? 0; final criticalThreshold = equipment.criticalThreshold ?? 0; if (criticalThreshold > 0 && availableQty <= criticalThreshold) { return EquipmentStatus.maintenance; // Utiliser maintenance pour indiquer un problème } } // Sinon retourner le statut de base return equipment.status; } }