refactor: Passage à la pagination côté serveur pour les équipements et containers
Cette mise à jour refactorise en profondeur le chargement des données pour les équipements et les containers, en remplaçant la récupération complète de la collection par un système de pagination côté serveur. Ce changement améliore considérablement les performances, réduit la consommation de mémoire et accélère le temps de chargement initial, en particulier pour les larges inventaires.
**Changements Backend (Cloud Functions) :**
- **Nouveaux Endpoints Paginés :**
- `getEquipmentsPaginated` et `getContainersPaginated` ont été créés pour remplacer les anciens `getEquipments` et `getContainers`.
- Ces nouvelles fonctions supportent le filtrage (catégorie, statut, type), la recherche textuelle et le tri directement côté serveur, limitant la quantité de données transférées.
- La pagination est gérée via les paramètres `limit` et `startAfter`, assurant un chargement par lots efficace.
- **Optimisation de `getContainersPaginated` :**
- Peuple désormais les containers avec leurs équipements enfants via une requête `in` optimisée, réduisant le nombre de lectures Firestore.
- **Suppression des Anciens Endpoints :** Les fonctions `getEquipments` et `getContainers`, qui chargeaient l'intégralité des collections, ont été supprimées.
- **Nouveau Script de Migration :** Ajout d'un script (`migrate_equipment_ids.js`) pour s'assurer que chaque équipement dans Firestore possède un champ `id` correspondant à son ID de document, ce qui est crucial pour le tri et la pagination.
**Changements Frontend (Flutter) :**
- **`EquipmentProvider` et `ContainerProvider` :**
- La logique de chargement a été entièrement réécrite pour utiliser les nouveaux endpoints paginés.
- Introduction d'un mode `usePagination` pour basculer entre le chargement paginé (pour les pages de gestion) et le chargement complet (pour les dialogues de sélection).
- Implémentation de `loadFirstPage` et `loadNextPage` pour gérer le scroll infini.
- Ajout d'un "debouncing" sur la recherche pour éviter les appels API excessifs lors de la saisie.
- **Pages de Gestion (`EquipmentManagementPage`, `ContainerManagementPage`) :**
- Utilisent désormais un `ScrollController` pour déclencher `loadNextPage` et implémenter un scroll infini.
- Le chargement initial et les rechargements (après filtre) sont beaucoup plus rapides.
- Refonte de l'UI avec un nouveau widget `SearchActionsBar` pour uniformiser la barre de recherche et les actions.
- **Dialogue de Sélection d'Équipement (`EquipmentSelectionDialog`) :**
- Passe également à un système de lazy loading basé sur des `ChoiceChip` pour afficher soit les équipements, soit les containers.
- Charge les pages de manière asynchrone au fur et à mesure du scroll, améliorant drastiquement la réactivité du dialogue.
- La logique de chargement des données a été fiabilisée pour attendre la disponibilité des données avant l'affichage.
- **Optimisations diverses :**
- Les sections qui listent les événements associés à un équipement (`EquipmentCurrentEventsSection`, etc.) chargent désormais uniquement les containers pertinents via `getContainersByIds` au lieu de toute la collection.
- Le calcul du statut d'un équipement (`EquipmentStatusBadge`) est maintenant synchrone, simplifiant le code et évitant des `FutureBuilder`.
**Correction mineure :**
- **Nom de l'application :** Le nom de l'application a été mis à jour de "EM2 ERP" à "EM2 Hub" dans `main.dart` et dans les exports ICS.
This commit is contained in:
@@ -1,27 +1,48 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'dart:async';
|
||||
import 'package:em2rp/models/container_model.dart';
|
||||
import 'package:em2rp/models/equipment_model.dart';
|
||||
import 'package:em2rp/services/container_service.dart';
|
||||
import 'package:em2rp/services/data_service.dart';
|
||||
import 'package:em2rp/services/api_service.dart';
|
||||
import 'package:em2rp/utils/debug_log.dart';
|
||||
|
||||
class ContainerProvider with ChangeNotifier {
|
||||
final ContainerService _containerService = ContainerService();
|
||||
final DataService _dataService = DataService(FirebaseFunctionsApiService());
|
||||
|
||||
// Timer pour le debouncing de la recherche
|
||||
Timer? _searchDebounceTimer;
|
||||
|
||||
// Liste paginée pour la page de gestion
|
||||
List<ContainerModel> _paginatedContainers = [];
|
||||
bool _hasMore = true;
|
||||
bool _isLoadingMore = false;
|
||||
String? _lastVisible;
|
||||
|
||||
// Cache complet pour compatibilité
|
||||
List<ContainerModel> _containers = [];
|
||||
|
||||
// Filtres et recherche
|
||||
ContainerType? _selectedType;
|
||||
EquipmentStatus? _selectedStatus;
|
||||
String _searchQuery = '';
|
||||
bool _isLoading = false;
|
||||
bool _isInitialized = false;
|
||||
|
||||
List<ContainerModel> get containers => _containers;
|
||||
// Mode de chargement (pagination vs full)
|
||||
bool _usePagination = false;
|
||||
|
||||
// Getters
|
||||
List<ContainerModel> get containers => _usePagination ? _paginatedContainers : _containers;
|
||||
ContainerType? get selectedType => _selectedType;
|
||||
EquipmentStatus? get selectedStatus => _selectedStatus;
|
||||
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 conteneurs sont chargés (charge si nécessaire)
|
||||
Future<void> ensureLoaded() async {
|
||||
@@ -31,19 +52,43 @@ class ContainerProvider with ChangeNotifier {
|
||||
await loadContainers();
|
||||
}
|
||||
|
||||
/// Charger tous les containers via l'API
|
||||
/// Charger tous les containers via l'API (avec pagination automatique)
|
||||
Future<void> loadContainers() async {
|
||||
_isLoading = true;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final containers = await _containerService.getContainers(
|
||||
type: _selectedType,
|
||||
status: _selectedStatus,
|
||||
searchQuery: _searchQuery,
|
||||
);
|
||||
_containers.clear();
|
||||
String? lastVisible;
|
||||
bool hasMore = true;
|
||||
int pageCount = 0;
|
||||
|
||||
// Charger toutes les pages en boucle
|
||||
while (hasMore) {
|
||||
pageCount++;
|
||||
print('[ContainerProvider] Loading page $pageCount...');
|
||||
|
||||
final result = await _dataService.getContainersPaginated(
|
||||
limit: 100, // Charger 100 par page pour aller plus vite
|
||||
startAfter: lastVisible,
|
||||
sortBy: 'id',
|
||||
sortOrder: 'asc',
|
||||
type: _selectedType?.name,
|
||||
status: _selectedStatus?.name,
|
||||
searchQuery: _searchQuery,
|
||||
);
|
||||
|
||||
final containers = (result['containers'] as List<dynamic>)
|
||||
.map((data) => ContainerModel.fromMap(data, data['id'] as String))
|
||||
.toList();
|
||||
|
||||
_containers.addAll(containers);
|
||||
hasMore = result['hasMore'] as bool? ?? false;
|
||||
lastVisible = result['lastVisible'] as String?;
|
||||
|
||||
print('[ContainerProvider] Loaded ${containers.length} containers, total: ${_containers.length}, hasMore: $hasMore');
|
||||
}
|
||||
|
||||
_containers = containers;
|
||||
_isLoading = false;
|
||||
_isInitialized = true;
|
||||
notifyListeners();
|
||||
@@ -80,22 +125,144 @@ class ContainerProvider with ChangeNotifier {
|
||||
}
|
||||
|
||||
/// Définir le type sélectionné
|
||||
/// Définir le type sélectionné
|
||||
void setSelectedType(ContainerType? type) {
|
||||
void setSelectedType(ContainerType? type) async {
|
||||
if (_selectedType == type) return;
|
||||
_selectedType = type;
|
||||
notifyListeners();
|
||||
if (_usePagination) {
|
||||
await reload();
|
||||
} else {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// Définir le statut sélectionné
|
||||
void setSelectedStatus(EquipmentStatus? status) {
|
||||
void setSelectedStatus(EquipmentStatus? status) async {
|
||||
if (_selectedStatus == status) return;
|
||||
_selectedStatus = status;
|
||||
notifyListeners();
|
||||
if (_usePagination) {
|
||||
await reload();
|
||||
} else {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// Définir la requête de recherche
|
||||
/// 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();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PAGINATION - Nouvelles méthodes
|
||||
// ============================================================================
|
||||
|
||||
/// Active le mode pagination (pour la page de gestion)
|
||||
void enablePagination() {
|
||||
if (!_usePagination) {
|
||||
_usePagination = true;
|
||||
DebugLog.info('[ContainerProvider] Pagination mode enabled');
|
||||
}
|
||||
}
|
||||
|
||||
/// Désactive le mode pagination (pour les autres pages)
|
||||
void disablePagination() {
|
||||
if (_usePagination) {
|
||||
_usePagination = false;
|
||||
DebugLog.info('[ContainerProvider] Pagination mode disabled');
|
||||
}
|
||||
}
|
||||
|
||||
/// Charge la première page (réinitialise tout)
|
||||
Future<void> loadFirstPage() async {
|
||||
DebugLog.info('[ContainerProvider] Loading first page...');
|
||||
|
||||
_paginatedContainers.clear();
|
||||
_lastVisible = null;
|
||||
_hasMore = true;
|
||||
_isLoading = true;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
await loadNextPage();
|
||||
_isInitialized = true;
|
||||
} catch (e) {
|
||||
DebugLog.error('[ContainerProvider] Error loading first page', e);
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Charge la page suivante (scroll infini)
|
||||
Future<void> loadNextPage() async {
|
||||
if (_isLoadingMore || !_hasMore) {
|
||||
DebugLog.info('[ContainerProvider] Skip loadNextPage: isLoadingMore=$_isLoadingMore, hasMore=$_hasMore');
|
||||
return;
|
||||
}
|
||||
|
||||
DebugLog.info('[ContainerProvider] Loading next page... (current: ${_paginatedContainers.length})');
|
||||
|
||||
_isLoadingMore = true;
|
||||
_isLoading = true;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final result = await _dataService.getContainersPaginated(
|
||||
limit: 20,
|
||||
startAfter: _lastVisible,
|
||||
type: _selectedType != null ? containerTypeToString(_selectedType!) : null,
|
||||
searchQuery: _searchQuery.isNotEmpty ? _searchQuery : null,
|
||||
sortBy: 'id',
|
||||
sortOrder: 'asc',
|
||||
);
|
||||
|
||||
final newContainers = (result['containers'] as List<dynamic>)
|
||||
.map((data) {
|
||||
final map = data as Map<String, dynamic>;
|
||||
return ContainerModel.fromMap(map, map['id'] as String);
|
||||
})
|
||||
.toList();
|
||||
|
||||
_paginatedContainers.addAll(newContainers);
|
||||
_hasMore = result['hasMore'] as bool? ?? false;
|
||||
_lastVisible = result['lastVisible'] as String?;
|
||||
|
||||
DebugLog.info('[ContainerProvider] Loaded ${newContainers.length} containers, total: ${_paginatedContainers.length}, hasMore: $_hasMore');
|
||||
|
||||
_isLoadingMore = false;
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
DebugLog.error('[ContainerProvider] Error loading next page', e);
|
||||
_isLoadingMore = false;
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Recharge en changeant de filtre ou recherche
|
||||
Future<void> reload() async {
|
||||
DebugLog.info('[ContainerProvider] Reloading with new filters...');
|
||||
await loadFirstPage();
|
||||
}
|
||||
|
||||
/// Créer un nouveau container
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:em2rp/models/container_model.dart';
|
||||
import 'package:em2rp/services/data_service.dart';
|
||||
import 'package:em2rp/services/api_service.dart';
|
||||
|
||||
import '../models/equipment_model.dart';
|
||||
|
||||
class ContainerProvider extends ChangeNotifier {
|
||||
final DataService _dataService = DataService(FirebaseFunctionsApiService());
|
||||
|
||||
List<ContainerModel> _containers = [];
|
||||
ContainerType? _selectedType;
|
||||
EquipmentStatus? _selectedStatus;
|
||||
String _searchQuery = '';
|
||||
bool _isLoading = false;
|
||||
|
||||
// Getters
|
||||
List<ContainerModel> get containers => _filteredContainers;
|
||||
ContainerType? get selectedType => _selectedType;
|
||||
EquipmentStatus? get selectedStatus => _selectedStatus;
|
||||
String get searchQuery => _searchQuery;
|
||||
bool get isLoading => _isLoading;
|
||||
|
||||
/// Charger tous les conteneurs via l'API
|
||||
Future<void> loadContainers() async {
|
||||
_isLoading = true;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final containersData = await _dataService.getContainers();
|
||||
|
||||
_containers = containersData.map((data) {
|
||||
return ContainerModel.fromMap(data, data['id'] as String);
|
||||
}).toList();
|
||||
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
print('Error loading containers: $e');
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Obtenir les conteneurs filtrés
|
||||
List<ContainerModel> get _filteredContainers {
|
||||
var filtered = _containers;
|
||||
|
||||
if (_selectedType != null) {
|
||||
filtered = filtered.where((c) => c.type == _selectedType).toList();
|
||||
}
|
||||
|
||||
if (_selectedStatus != null) {
|
||||
filtered = filtered.where((c) => c.status == _selectedStatus).toList();
|
||||
}
|
||||
|
||||
if (_searchQuery.isNotEmpty) {
|
||||
final query = _searchQuery.toLowerCase();
|
||||
filtered = filtered.where((c) {
|
||||
return c.name.toLowerCase().contains(query) ||
|
||||
c.id.toLowerCase().contains(query);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
/// Définir le filtre de type
|
||||
void setSelectedType(ContainerType? type) {
|
||||
_selectedType = type;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Définir le filtre de statut
|
||||
void setSelectedStatus(EquipmentStatus? status) {
|
||||
_selectedStatus = status;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Définir la requête de recherche
|
||||
void setSearchQuery(String query) {
|
||||
_searchQuery = query;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Réinitialiser tous les filtres
|
||||
void clearFilters() {
|
||||
_selectedType = null;
|
||||
_selectedStatus = null;
|
||||
_searchQuery = '';
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Recharger les conteneurs
|
||||
Future<void> refresh() async {
|
||||
await loadContainers();
|
||||
}
|
||||
|
||||
/// Obtenir un conteneur par ID
|
||||
ContainerModel? getById(String id) {
|
||||
try {
|
||||
return _containers.firstWhere((c) => c.id == id);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,43 @@
|
||||
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<EquipmentModel> _paginatedEquipment = [];
|
||||
bool _hasMore = true;
|
||||
bool _isLoadingMore = false;
|
||||
String? _lastVisible;
|
||||
|
||||
// Cache complet pour getEquipmentsByIds et compatibilité
|
||||
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; // Flag pour savoir si les équipements ont été chargés
|
||||
bool _isInitialized = false;
|
||||
|
||||
// Mode de chargement (pagination vs full)
|
||||
bool _usePagination = false;
|
||||
|
||||
// Constructeur - Ne charge PAS automatiquement
|
||||
// Les équipements seront chargés à la demande (page de gestion ou via getEquipmentsByIds)
|
||||
EquipmentProvider();
|
||||
|
||||
// Getters
|
||||
List<EquipmentModel> get equipment => _filteredEquipment;
|
||||
List<EquipmentModel> get allEquipment => _equipment; // Tous les équipements sans filtre
|
||||
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;
|
||||
@@ -31,42 +45,86 @@ class EquipmentProvider extends ChangeNotifier {
|
||||
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 {
|
||||
if (_isInitialized || _isLoading) {
|
||||
print('[EquipmentProvider] Equipment already loaded or loading, skipping...');
|
||||
// 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 la page de gestion)
|
||||
/// Charger tous les équipements via l'API (utilisé par les dialogs et sélection)
|
||||
Future<void> loadEquipments() async {
|
||||
print('[EquipmentProvider] Starting to load equipments...');
|
||||
print('[EquipmentProvider] Starting to load ALL equipments...');
|
||||
_isLoading = true;
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
print('[EquipmentProvider] Calling getEquipments API...');
|
||||
final equipmentsData = await _dataService.getEquipments();
|
||||
print('[EquipmentProvider] Received ${equipmentsData.length} equipments from API');
|
||||
_equipment.clear();
|
||||
String? lastVisible;
|
||||
bool hasMore = true;
|
||||
int pageCount = 0;
|
||||
|
||||
_equipment = equipmentsData.map((data) {
|
||||
return EquipmentModel.fromMap(data, data['id'] as String);
|
||||
}).toList();
|
||||
print('[EquipmentProvider] Mapped ${_equipment.length} equipment models');
|
||||
// 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');
|
||||
print('[EquipmentProvider] Equipment loading complete: ${_equipment.length} equipments');
|
||||
} catch (e) {
|
||||
print('[EquipmentProvider] Error loading equipments: $e');
|
||||
_isLoading = false;
|
||||
@@ -118,7 +176,8 @@ class EquipmentProvider extends ChangeNotifier {
|
||||
final equipmentsData = await _dataService.getEquipmentsByIds(missingIds);
|
||||
|
||||
final loadedEquipments = equipmentsData.map((data) {
|
||||
return EquipmentModel.fromMap(data, data['id'] as String);
|
||||
final id = data['id'] as String; // L'ID vient du backend
|
||||
return EquipmentModel.fromMap(data, id);
|
||||
}).toList();
|
||||
|
||||
// Ajouter au cache
|
||||
@@ -185,58 +244,205 @@ class EquipmentProvider extends ChangeNotifier {
|
||||
return filtered;
|
||||
}
|
||||
|
||||
/// Définir le filtre de catégorie
|
||||
void setSelectedCategory(EquipmentCategory? category) {
|
||||
_selectedCategory = category;
|
||||
// ============================================================================
|
||||
// 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) {
|
||||
void setSelectedStatus(EquipmentStatus? status) async {
|
||||
if (_selectedStatus == status) return;
|
||||
_selectedStatus = status;
|
||||
notifyListeners();
|
||||
if (_usePagination) {
|
||||
await reload();
|
||||
} else {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// Définir le filtre de modèle
|
||||
void setSelectedModel(String? model) {
|
||||
void setSelectedModel(String? model) async {
|
||||
if (_selectedModel == model) return;
|
||||
_selectedModel = model;
|
||||
notifyListeners();
|
||||
if (_usePagination) {
|
||||
await reload();
|
||||
} else {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// Définir la requête de recherche
|
||||
/// Définir la requête de recherche (avec debouncing)
|
||||
void setSearchQuery(String query) {
|
||||
if (_searchQuery == query) return;
|
||||
_searchQuery = query;
|
||||
notifyListeners();
|
||||
|
||||
// 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() {
|
||||
void clearFilters() async {
|
||||
_selectedCategory = null;
|
||||
_selectedStatus = null;
|
||||
_selectedModel = null;
|
||||
_searchQuery = '';
|
||||
notifyListeners();
|
||||
if (_usePagination) {
|
||||
await reload();
|
||||
} else {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// Recharger les équipements
|
||||
// ============================================================================
|
||||
// MÉTHODES COMPATIBILITÉ (pour ancien code)
|
||||
// ============================================================================
|
||||
|
||||
/// Recharger les équipements (ancien système)
|
||||
Future<void> refresh() async {
|
||||
await loadEquipments();
|
||||
if (_usePagination) {
|
||||
await reload();
|
||||
} else {
|
||||
await loadEquipments();
|
||||
}
|
||||
}
|
||||
|
||||
// === MÉTHODES STREAM (COMPATIBILITÉ) ===
|
||||
|
||||
/// Stream des équipements (pour compatibilité avec ancien code)
|
||||
Stream<List<EquipmentModel>> get equipmentStream async* {
|
||||
yield _equipment;
|
||||
if (!_isInitialized && !_usePagination) {
|
||||
await loadEquipments();
|
||||
}
|
||||
yield equipment;
|
||||
}
|
||||
|
||||
/// Supprimer un équipement
|
||||
Future<void> deleteEquipment(String equipmentId) async {
|
||||
try {
|
||||
await _dataService.deleteEquipment(equipmentId);
|
||||
await loadEquipments(); // Recharger la liste
|
||||
if (_usePagination) {
|
||||
await reload();
|
||||
} else {
|
||||
await loadEquipments();
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error deleting equipment: $e');
|
||||
DebugLog.error('[EquipmentProvider] Error deleting equipment', e);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
@@ -245,9 +451,13 @@ class EquipmentProvider extends ChangeNotifier {
|
||||
Future<void> addEquipment(EquipmentModel equipment) async {
|
||||
try {
|
||||
await _dataService.createEquipment(equipment.id, equipment.toMap());
|
||||
await loadEquipments(); // Recharger la liste
|
||||
if (_usePagination) {
|
||||
await reload();
|
||||
} else {
|
||||
await loadEquipments();
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error adding equipment: $e');
|
||||
DebugLog.error('[EquipmentProvider] Error adding equipment', e);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
@@ -256,52 +466,67 @@ class EquipmentProvider extends ChangeNotifier {
|
||||
Future<void> updateEquipment(EquipmentModel equipment) async {
|
||||
try {
|
||||
await _dataService.updateEquipment(equipment.id, equipment.toMap());
|
||||
await loadEquipments(); // Recharger la liste
|
||||
if (_usePagination) {
|
||||
await reload();
|
||||
} else {
|
||||
await loadEquipments();
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error updating equipment: $e');
|
||||
DebugLog.error('[EquipmentProvider] Error updating equipment', e);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Charger les marques
|
||||
Future<void> loadBrands() async {
|
||||
// Les marques sont déjà chargées avec loadEquipments
|
||||
await ensureLoaded();
|
||||
_extractUniqueValues();
|
||||
}
|
||||
|
||||
/// Charger les modèles
|
||||
Future<void> loadModels() async {
|
||||
// Les modèles sont déjà chargés avec loadEquipments
|
||||
await ensureLoaded();
|
||||
_extractUniqueValues();
|
||||
}
|
||||
|
||||
/// Charger les modèles d'une marque spécifique
|
||||
Future<List<String>> loadModelsByBrand(String brand) async {
|
||||
// Filtrer les modèles par marque
|
||||
final modelsByBrand = _equipment
|
||||
.where((eq) => eq.brand == brand && eq.model != null)
|
||||
.map((eq) => eq.model!)
|
||||
await ensureLoaded();
|
||||
return _equipment
|
||||
.where((eq) => eq.brand?.toLowerCase() == brand.toLowerCase())
|
||||
.map((eq) => eq.model ?? '')
|
||||
.where((model) => model.isNotEmpty)
|
||||
.toSet()
|
||||
.toList();
|
||||
return modelsByBrand;
|
||||
.toList()
|
||||
..sort();
|
||||
}
|
||||
|
||||
/// Charger les sous-catégories d'une catégorie spécifique
|
||||
Future<List<String>> loadSubCategoriesByCategory(EquipmentCategory category) async {
|
||||
// Filtrer les sous-catégories par catégorie
|
||||
final subCategoriesByCategory = _equipment
|
||||
.where((eq) => eq.category == category && eq.subCategory != null && eq.subCategory!.isNotEmpty)
|
||||
.map((eq) => eq.subCategory!)
|
||||
await ensureLoaded();
|
||||
return _equipment
|
||||
.where((eq) => eq.category == category)
|
||||
.map((eq) => eq.subCategory ?? '')
|
||||
.where((sub) => sub.isNotEmpty)
|
||||
.toSet()
|
||||
.toList()
|
||||
..sort();
|
||||
return subCategoriesByCategory;
|
||||
}
|
||||
|
||||
/// Calculer le statut réel d'un équipement (compatibilité)
|
||||
Future<EquipmentStatus> calculateRealStatus(EquipmentModel equipment) async {
|
||||
// Pour l'instant, retourner le statut stocké
|
||||
// TODO: Implémenter le calcul réel si nécessaire
|
||||
/// 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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user