534 lines
16 KiB
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;
|
|
}
|
|
}
|
|
|