Files
EM2_ERP/em2rp/lib/providers/equipment_provider.dart

534 lines
16 KiB
Dart

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
final List<EquipmentModel> _paginatedEquipment = [];
bool _hasMore = true;
bool _isLoadingMore = false;
String? _lastVisible;
// Cache complet pour getEquipmentsByIds et compatibilité
final List<EquipmentModel> _equipment = [];
List<String> _models = [];
List<String> _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<EquipmentModel> get equipment => _usePagination ? _paginatedEquipment : _filteredEquipment;
List<EquipmentModel> get allEquipment => _equipment;
List<String> get models => _models;
List<String> 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<void> 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<void> 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<dynamic>;
print('[EquipmentProvider] Page $pageCount: ${equipmentsData.length} equipments');
final pageEquipments = equipmentsData.map((data) {
final id = data['id'] as String;
return EquipmentModel.fromMap(data as Map<String, dynamic>, 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<List<EquipmentModel>> getEquipmentsByIds(List<String> equipmentIds) async {
if (equipmentIds.isEmpty) return [];
print('[EquipmentProvider] Loading ${equipmentIds.length} equipments by IDs...');
try {
// Vérifier d'abord le cache local
final cachedEquipments = <EquipmentModel>[];
final missingIds = <String>[];
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 = <String>{};
final brandSet = <String>{};
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<EquipmentModel> 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<void> 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<void> 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<dynamic>)
.map((data) {
final map = data as Map<String, dynamic>;
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<void> 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<void> refresh() async {
if (_usePagination) {
await reload();
} else {
await loadEquipments();
}
}
/// Stream des équipements (pour compatibilité avec ancien code)
Stream<List<EquipmentModel>> get equipmentStream async* {
if (!_isInitialized && !_usePagination) {
await loadEquipments();
}
yield equipment;
}
/// Supprimer un équipement
Future<void> 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<void> 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<void> 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<void> loadBrands() async {
await ensureLoaded();
_extractUniqueValues();
}
/// Charger les modèles
Future<void> loadModels() async {
await ensureLoaded();
_extractUniqueValues();
}
/// Charger les modèles d'une marque spécifique
Future<List<String>> 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<List<String>> 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;
}
}