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:
@@ -277,6 +277,63 @@ class FirebaseFunctionsApiService implements ApiService {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Appelle une Cloud Function avec pagination
|
||||
Future<Map<String, dynamic>> callPaginated(
|
||||
String functionName,
|
||||
Map<String, dynamic> params,
|
||||
) async {
|
||||
try {
|
||||
final headers = await _getHeaders();
|
||||
final url = Uri.parse('$_baseUrl/$functionName');
|
||||
|
||||
DebugLog.info('[API] Calling paginated function: $functionName with params: $params');
|
||||
|
||||
final response = await http.post(
|
||||
url,
|
||||
headers: headers,
|
||||
body: jsonEncode({'data': params}),
|
||||
);
|
||||
|
||||
DebugLog.info('[API] Response status: ${response.statusCode}');
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
return data;
|
||||
} else {
|
||||
DebugLog.error('[API] Error response: ${response.body}');
|
||||
throw Exception('API call failed with status ${response.statusCode}: ${response.body}');
|
||||
}
|
||||
} catch (e) {
|
||||
DebugLog.error('[API] Exception in callPaginated: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Recherche rapide avec autocomplétion
|
||||
Future<List<Map<String, dynamic>>> quickSearch(
|
||||
String query, {
|
||||
int limit = 10,
|
||||
bool includeEquipments = true,
|
||||
bool includeContainers = true,
|
||||
}) async {
|
||||
try {
|
||||
final params = {
|
||||
'query': query,
|
||||
'limit': limit,
|
||||
'includeEquipments': includeEquipments.toString(),
|
||||
'includeContainers': includeContainers.toString(),
|
||||
};
|
||||
|
||||
final response = await callPaginated('quickSearch', params);
|
||||
final results = response['results'] as List<dynamic>? ?? [];
|
||||
|
||||
return results.cast<Map<String, dynamic>>();
|
||||
} catch (e) {
|
||||
DebugLog.error('[API] Error in quickSearch: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Exception personnalisée pour les erreurs API
|
||||
|
||||
@@ -169,7 +169,8 @@ class ContainerService {
|
||||
final equipmentsData = await _dataService.getEquipmentsByIds(container.equipmentIds);
|
||||
|
||||
for (var data in equipmentsData) {
|
||||
final equipment = EquipmentModel.fromMap(data, data['id'] as String);
|
||||
final id = data['id'] as String;
|
||||
final equipment = EquipmentModel.fromMap(data, id);
|
||||
if (equipment.status != EquipmentStatus.available) {
|
||||
unavailableEquipment.add('${equipment.name} (${equipment.status})');
|
||||
}
|
||||
@@ -202,7 +203,10 @@ class ContainerService {
|
||||
final equipmentsData = await _dataService.getEquipmentsByIds(container.equipmentIds);
|
||||
|
||||
return equipmentsData
|
||||
.map((data) => EquipmentModel.fromMap(data, data['id'] as String))
|
||||
.map((data) {
|
||||
final id = data['id'] as String;
|
||||
return EquipmentModel.fromMap(data, id);
|
||||
})
|
||||
.toList();
|
||||
} catch (e) {
|
||||
print('Error getting container equipment: $e');
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:em2rp/services/api_service.dart';
|
||||
import 'package:em2rp/utils/debug_log.dart';
|
||||
|
||||
/// Service générique pour les opérations de lecture de données via Cloud Functions
|
||||
class DataService {
|
||||
@@ -300,7 +301,7 @@ class DataService {
|
||||
}
|
||||
}
|
||||
|
||||
/// Récupère plusieurs conteneurs par leurs IDs
|
||||
/// Récupère plusieurs containers par leurs IDs
|
||||
Future<List<Map<String, dynamic>>> getContainersByIds(List<String> containerIds) async {
|
||||
try {
|
||||
if (containerIds.isEmpty) return [];
|
||||
@@ -318,37 +319,119 @@ class DataService {
|
||||
return containers.map((e) => e as Map<String, dynamic>).toList();
|
||||
} catch (e) {
|
||||
print('[DataService] Error getting containers by IDs: $e');
|
||||
throw Exception('Erreur lors de la récupération des conteneurs: $e');
|
||||
throw Exception('Erreur lors de la récupération des containers: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Récupère les maintenances (optionnellement filtrées par équipement)
|
||||
Future<List<Map<String, dynamic>>> getMaintenances({String? equipmentId}) async {
|
||||
try {
|
||||
final data = <String, dynamic>{};
|
||||
if (equipmentId != null) data['equipmentId'] = equipmentId;
|
||||
// ============================================================================
|
||||
// EQUIPMENTS & CONTAINERS - Pagination
|
||||
// ============================================================================
|
||||
|
||||
final result = await _apiService.call('getMaintenances', data);
|
||||
final maintenances = result['maintenances'] as List<dynamic>?;
|
||||
if (maintenances == null) return [];
|
||||
return maintenances.map((e) => e as Map<String, dynamic>).toList();
|
||||
/// Récupère les équipements avec pagination et filtrage
|
||||
Future<Map<String, dynamic>> getEquipmentsPaginated({
|
||||
int limit = 20,
|
||||
String? startAfter,
|
||||
String? category,
|
||||
String? status,
|
||||
String? searchQuery,
|
||||
String sortBy = 'id',
|
||||
String sortOrder = 'asc',
|
||||
}) async {
|
||||
try {
|
||||
final params = <String, dynamic>{
|
||||
'limit': limit,
|
||||
'sortBy': sortBy,
|
||||
'sortOrder': sortOrder,
|
||||
};
|
||||
|
||||
if (startAfter != null) params['startAfter'] = startAfter;
|
||||
if (category != null) params['category'] = category;
|
||||
if (status != null) params['status'] = status;
|
||||
if (searchQuery != null && searchQuery.isNotEmpty) {
|
||||
params['searchQuery'] = searchQuery;
|
||||
}
|
||||
|
||||
final result = await (_apiService as FirebaseFunctionsApiService).callPaginated(
|
||||
'getEquipmentsPaginated',
|
||||
params,
|
||||
);
|
||||
|
||||
return {
|
||||
'equipments': (result['equipments'] as List<dynamic>?)
|
||||
?.map((e) => e as Map<String, dynamic>)
|
||||
.toList() ?? [],
|
||||
'hasMore': result['hasMore'] as bool? ?? false,
|
||||
'lastVisible': result['lastVisible'] as String?,
|
||||
'total': result['total'] as int? ?? 0,
|
||||
};
|
||||
} catch (e) {
|
||||
throw Exception('Erreur lors de la récupération des maintenances: $e');
|
||||
DebugLog.error('[DataService] Error in getEquipmentsPaginated', e);
|
||||
throw Exception('Erreur lors de la récupération paginée des équipements: $e');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Récupère les containers contenant un équipement spécifique
|
||||
Future<List<Map<String, dynamic>>> getContainersByEquipment(String equipmentId) async {
|
||||
/// Récupère les containers avec pagination et filtrage
|
||||
Future<Map<String, dynamic>> getContainersPaginated({
|
||||
int limit = 20,
|
||||
String? startAfter,
|
||||
String? type,
|
||||
String? status,
|
||||
String? searchQuery,
|
||||
String? category,
|
||||
String sortBy = 'id',
|
||||
String sortOrder = 'asc',
|
||||
}) async {
|
||||
try {
|
||||
final result = await _apiService.call('getContainersByEquipment', {
|
||||
'equipmentId': equipmentId,
|
||||
});
|
||||
final containers = result['containers'] as List<dynamic>?;
|
||||
if (containers == null) return [];
|
||||
return containers.map((e) => e as Map<String, dynamic>).toList();
|
||||
final params = <String, dynamic>{
|
||||
'limit': limit,
|
||||
'sortBy': sortBy,
|
||||
'sortOrder': sortOrder,
|
||||
};
|
||||
|
||||
if (startAfter != null) params['startAfter'] = startAfter;
|
||||
if (type != null) params['type'] = type;
|
||||
if (status != null) params['status'] = status;
|
||||
if (category != null) params['category'] = category;
|
||||
if (searchQuery != null && searchQuery.isNotEmpty) {
|
||||
params['searchQuery'] = searchQuery;
|
||||
}
|
||||
|
||||
final result = await (_apiService as FirebaseFunctionsApiService).callPaginated(
|
||||
'getContainersPaginated',
|
||||
params,
|
||||
);
|
||||
|
||||
return {
|
||||
'containers': (result['containers'] as List<dynamic>?)
|
||||
?.map((e) => e as Map<String, dynamic>)
|
||||
.toList() ?? [],
|
||||
'hasMore': result['hasMore'] as bool? ?? false,
|
||||
'lastVisible': result['lastVisible'] as String?,
|
||||
'total': result['total'] as int? ?? 0,
|
||||
};
|
||||
} catch (e) {
|
||||
throw Exception('Erreur lors de la récupération des containers pour l\'équipement: $e');
|
||||
DebugLog.error('[DataService] Error in getContainersPaginated', e);
|
||||
throw Exception('Erreur lors de la récupération paginée des containers: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Recherche rapide (autocomplétion)
|
||||
Future<List<Map<String, dynamic>>> quickSearch(
|
||||
String query, {
|
||||
int limit = 10,
|
||||
bool includeEquipments = true,
|
||||
bool includeContainers = true,
|
||||
}) async {
|
||||
try {
|
||||
return await (_apiService as FirebaseFunctionsApiService).quickSearch(
|
||||
query,
|
||||
limit: limit,
|
||||
includeEquipments: includeEquipments,
|
||||
includeContainers: includeContainers,
|
||||
);
|
||||
} catch (e) {
|
||||
DebugLog.error('[DataService] Error in quickSearch', e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -454,6 +537,21 @@ class DataService {
|
||||
// MAINTENANCES
|
||||
// ============================================================================
|
||||
|
||||
/// Récupère toutes les maintenances
|
||||
Future<List<Map<String, dynamic>>> getMaintenances({String? equipmentId}) async {
|
||||
try {
|
||||
final data = <String, dynamic>{};
|
||||
if (equipmentId != null) data['equipmentId'] = equipmentId;
|
||||
|
||||
final result = await _apiService.call('getMaintenances', data);
|
||||
final maintenances = result['maintenances'] as List<dynamic>?;
|
||||
if (maintenances == null) return [];
|
||||
return maintenances.map((e) => e as Map<String, dynamic>).toList();
|
||||
} catch (e) {
|
||||
throw Exception('Erreur lors de la récupération des maintenances: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Supprime une maintenance
|
||||
Future<void> deleteMaintenance(String maintenanceId) async {
|
||||
try {
|
||||
@@ -463,6 +561,20 @@ class DataService {
|
||||
}
|
||||
}
|
||||
|
||||
/// Récupère les containers contenant un équipement
|
||||
Future<List<Map<String, dynamic>>> getContainersByEquipment(String equipmentId) async {
|
||||
try {
|
||||
final result = await _apiService.call('getContainersByEquipment', {
|
||||
'equipmentId': equipmentId,
|
||||
});
|
||||
final containers = result['containers'] as List<dynamic>?;
|
||||
if (containers == null) return [];
|
||||
return containers.map((e) => e as Map<String, dynamic>).toList();
|
||||
} catch (e) {
|
||||
throw Exception('Erreur lors de la récupération des containers: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// USERS
|
||||
// ============================================================================
|
||||
|
||||
@@ -9,6 +9,34 @@ class EquipmentService {
|
||||
final ApiService _apiService = apiService;
|
||||
final DataService _dataService = DataService(apiService);
|
||||
|
||||
// ============================================================================
|
||||
// Helper privée - Charge TOUS les équipements avec pagination
|
||||
// ============================================================================
|
||||
|
||||
/// Charge tous les équipements en utilisant la pagination
|
||||
Future<List<Map<String, dynamic>>> _getAllEquipmentsPaginated() async {
|
||||
final allEquipments = <Map<String, dynamic>>[];
|
||||
String? lastVisible;
|
||||
bool hasMore = true;
|
||||
|
||||
while (hasMore) {
|
||||
final result = await _dataService.getEquipmentsPaginated(
|
||||
limit: 100,
|
||||
startAfter: lastVisible,
|
||||
sortBy: 'id',
|
||||
sortOrder: 'asc',
|
||||
);
|
||||
|
||||
final equipments = result['equipments'] as List<dynamic>;
|
||||
allEquipments.addAll(equipments.cast<Map<String, dynamic>>());
|
||||
|
||||
hasMore = result['hasMore'] as bool? ?? false;
|
||||
lastVisible = result['lastVisible'] as String?;
|
||||
}
|
||||
|
||||
return allEquipments;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CRUD Operations - Utilise le backend sécurisé
|
||||
// ============================================================================
|
||||
@@ -82,10 +110,13 @@ class EquipmentService {
|
||||
String? searchQuery,
|
||||
}) async {
|
||||
try {
|
||||
final equipmentsData = await _dataService.getEquipments();
|
||||
final equipmentsData = await _getAllEquipmentsPaginated();
|
||||
|
||||
var equipmentList = equipmentsData
|
||||
.map((data) => EquipmentModel.fromMap(data, data['id'] as String))
|
||||
.map((data) {
|
||||
final id = data['id'] as String;
|
||||
return EquipmentModel.fromMap(data, id);
|
||||
})
|
||||
.toList();
|
||||
|
||||
// Filtres côté client
|
||||
@@ -165,7 +196,11 @@ class EquipmentService {
|
||||
});
|
||||
|
||||
final alternatives = (response['alternatives'] as List?)
|
||||
?.map((a) => EquipmentModel.fromMap(a as Map<String, dynamic>, a['id'] as String))
|
||||
?.map((a) {
|
||||
final map = a as Map<String, dynamic>;
|
||||
final id = map['id'] as String;
|
||||
return EquipmentModel.fromMap(map, id);
|
||||
})
|
||||
.toList() ?? [];
|
||||
|
||||
return alternatives;
|
||||
@@ -204,27 +239,6 @@ class EquipmentService {
|
||||
}
|
||||
}
|
||||
|
||||
/// Vérifier les stocks critiques et créer des alertes
|
||||
Future<void> checkCriticalStock() async {
|
||||
try {
|
||||
final equipmentsData = await _dataService.getEquipments();
|
||||
|
||||
for (var data in equipmentsData) {
|
||||
final equipment = EquipmentModel.fromMap(data, data['id'] as String);
|
||||
|
||||
// Filtrer uniquement les consommables et câbles
|
||||
if ((equipment.category == EquipmentCategory.consumable ||
|
||||
equipment.category == EquipmentCategory.cable) &&
|
||||
equipment.isCriticalStock) {
|
||||
await _createLowStockAlert(equipment);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error checking critical stock: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Créer une alerte de stock faible
|
||||
Future<void> _createLowStockAlert(EquipmentModel equipment) async {
|
||||
try {
|
||||
@@ -251,50 +265,10 @@ class EquipmentService {
|
||||
return equipmentId;
|
||||
}
|
||||
|
||||
/// Récupérer tous les modèles uniques (pour l'indexation/autocomplete)
|
||||
Future<List<String>> getAllModels() async {
|
||||
try {
|
||||
final equipmentsData = await _dataService.getEquipments();
|
||||
final models = <String>{};
|
||||
|
||||
for (var data in equipmentsData) {
|
||||
final model = data['model'] as String?;
|
||||
if (model != null && model.isNotEmpty) {
|
||||
models.add(model);
|
||||
}
|
||||
}
|
||||
|
||||
return models.toList()..sort();
|
||||
} catch (e) {
|
||||
print('Error getting all models: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Récupérer toutes les marques uniques (pour l'indexation/autocomplete)
|
||||
Future<List<String>> getAllBrands() async {
|
||||
try {
|
||||
final equipmentsData = await _dataService.getEquipments();
|
||||
final brands = <String>{};
|
||||
|
||||
for (var data in equipmentsData) {
|
||||
final brand = data['brand'] as String?;
|
||||
if (brand != null && brand.isNotEmpty) {
|
||||
brands.add(brand);
|
||||
}
|
||||
}
|
||||
|
||||
return brands.toList()..sort();
|
||||
} catch (e) {
|
||||
print('Error getting all brands: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Récupérer les modèles filtrés par marque
|
||||
Future<List<String>> getModelsByBrand(String brand) async {
|
||||
try {
|
||||
final equipmentsData = await _dataService.getEquipments();
|
||||
final equipmentsData = await _getAllEquipmentsPaginated();
|
||||
final models = <String>{};
|
||||
|
||||
for (var data in equipmentsData) {
|
||||
@@ -316,7 +290,7 @@ class EquipmentService {
|
||||
/// Récupérer les sous-catégories filtrées par catégorie
|
||||
Future<List<String>> getSubCategoriesByCategory(EquipmentCategory category) async {
|
||||
try {
|
||||
final equipmentsData = await _dataService.getEquipments();
|
||||
final equipmentsData = await _getAllEquipmentsPaginated();
|
||||
final subCategories = <String>{};
|
||||
|
||||
final categoryString = equipmentCategoryToString(category);
|
||||
@@ -375,7 +349,10 @@ class EquipmentService {
|
||||
final equipmentsData = await _dataService.getEquipmentsByIds(ids);
|
||||
|
||||
return equipmentsData
|
||||
.map((data) => EquipmentModel.fromMap(data, data['id'] as String))
|
||||
.map((data) {
|
||||
final id = data['id'] as String;
|
||||
return EquipmentModel.fromMap(data, id);
|
||||
})
|
||||
.toList();
|
||||
} catch (e) {
|
||||
print('Error getting equipments by IDs: $e');
|
||||
|
||||
@@ -231,7 +231,7 @@ END:VCALENDAR''';
|
||||
// Lien vers l'application
|
||||
buffer.writeln('');
|
||||
buffer.writeln('---');
|
||||
buffer.writeln('Généré par EM2 ERP ${AppVersion.fullVersion} http://app.em2events.fr');
|
||||
buffer.writeln('Généré par EM2 Hub ${AppVersion.fullVersion} http://app.em2events.fr');
|
||||
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user