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.
378 lines
12 KiB
Dart
378 lines
12 KiB
Dart
import 'package:em2rp/models/equipment_model.dart';
|
|
import 'package:em2rp/models/maintenance_model.dart';
|
|
import 'package:em2rp/models/container_model.dart';
|
|
import 'package:em2rp/services/api_service.dart';
|
|
import 'package:em2rp/services/data_service.dart';
|
|
import 'package:em2rp/services/maintenance_service.dart';
|
|
|
|
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é
|
|
// ============================================================================
|
|
|
|
/// Créer un nouvel équipement (via Cloud Function)
|
|
Future<void> createEquipment(EquipmentModel equipment) async {
|
|
try {
|
|
if (equipment.id.isEmpty) {
|
|
throw Exception('L\'ID de l\'équipement est requis pour la création');
|
|
}
|
|
|
|
final data = equipment.toMap();
|
|
data['id'] = equipment.id; // S'assurer que l'ID est inclus
|
|
|
|
await _apiService.call('createEquipment', data);
|
|
} catch (e) {
|
|
print('Error creating equipment: $e');
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
/// Mettre à jour un équipement (via Cloud Function)
|
|
Future<void> updateEquipment(String id, Map<String, dynamic> data) async {
|
|
try {
|
|
if (data.isEmpty) {
|
|
throw Exception('Aucune donnée à mettre à jour');
|
|
}
|
|
|
|
await _apiService.call('updateEquipment', {
|
|
'equipmentId': id,
|
|
'data': data,
|
|
});
|
|
} catch (e) {
|
|
print('Error updating equipment: $e');
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
/// Supprimer un équipement (via Cloud Function)
|
|
Future<void> deleteEquipment(String id) async {
|
|
try {
|
|
await _apiService.call('deleteEquipment', {'equipmentId': id});
|
|
} catch (e) {
|
|
print('Error deleting equipment: $e');
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// READ Operations - Utilise Firestore streams (temps réel)
|
|
// ============================================================================
|
|
|
|
/// Récupérer un équipement par ID
|
|
Future<EquipmentModel?> getEquipmentById(String id) async {
|
|
try {
|
|
final equipmentsData = await _dataService.getEquipmentsByIds([id]);
|
|
if (equipmentsData.isEmpty) return null;
|
|
|
|
return EquipmentModel.fromMap(equipmentsData.first, id);
|
|
} catch (e) {
|
|
print('Error getting equipment: $e');
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
/// Récupérer les équipements avec filtres
|
|
Future<List<EquipmentModel>> getEquipment({
|
|
EquipmentCategory? category,
|
|
EquipmentStatus? status,
|
|
String? model,
|
|
String? searchQuery,
|
|
}) async {
|
|
try {
|
|
final equipmentsData = await _getAllEquipmentsPaginated();
|
|
|
|
var equipmentList = equipmentsData
|
|
.map((data) {
|
|
final id = data['id'] as String;
|
|
return EquipmentModel.fromMap(data, id);
|
|
})
|
|
.toList();
|
|
|
|
// Filtres côté client
|
|
if (category != null) {
|
|
equipmentList = equipmentList
|
|
.where((e) => e.category == category)
|
|
.toList();
|
|
}
|
|
|
|
if (status != null) {
|
|
equipmentList = equipmentList
|
|
.where((e) => e.status == status)
|
|
.toList();
|
|
}
|
|
|
|
if (model != null && model.isNotEmpty) {
|
|
equipmentList = equipmentList
|
|
.where((e) => e.model == model)
|
|
.toList();
|
|
}
|
|
|
|
if (searchQuery != null && searchQuery.isNotEmpty) {
|
|
final lowerSearch = searchQuery.toLowerCase();
|
|
equipmentList = equipmentList.where((equipment) {
|
|
return equipment.name.toLowerCase().contains(lowerSearch) ||
|
|
(equipment.model?.toLowerCase().contains(lowerSearch) ?? false) ||
|
|
equipment.id.toLowerCase().contains(lowerSearch);
|
|
}).toList();
|
|
}
|
|
|
|
return equipmentList;
|
|
} catch (e) {
|
|
print('Error getting equipment: $e');
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Availability & Stock Management - Logique métier côté client
|
|
// ============================================================================
|
|
|
|
/// Vérifier la disponibilité d'un équipement pour une période donnée
|
|
Future<List<Map<String, dynamic>>> checkAvailability(
|
|
String equipmentId,
|
|
DateTime startDate,
|
|
DateTime endDate,
|
|
) async {
|
|
try {
|
|
final response = await _apiService.call('checkEquipmentAvailability', {
|
|
'equipmentId': equipmentId,
|
|
'startDate': startDate.toIso8601String(),
|
|
'endDate': endDate.toIso8601String(),
|
|
});
|
|
|
|
final conflicts = (response['conflicts'] as List?)
|
|
?.map((c) => c as Map<String, dynamic>)
|
|
.toList() ?? [];
|
|
|
|
return conflicts;
|
|
} catch (e) {
|
|
print('Error checking availability: $e');
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
/// Trouver des alternatives (même modèle) disponibles
|
|
Future<List<EquipmentModel>> findAlternatives(
|
|
String model,
|
|
DateTime startDate,
|
|
DateTime endDate,
|
|
) async {
|
|
try {
|
|
final response = await _apiService.call('findAlternativeEquipment', {
|
|
'model': model,
|
|
'startDate': startDate.toIso8601String(),
|
|
'endDate': endDate.toIso8601String(),
|
|
});
|
|
|
|
final alternatives = (response['alternatives'] as List?)
|
|
?.map((a) {
|
|
final map = a as Map<String, dynamic>;
|
|
final id = map['id'] as String;
|
|
return EquipmentModel.fromMap(map, id);
|
|
})
|
|
.toList() ?? [];
|
|
|
|
return alternatives;
|
|
} catch (e) {
|
|
print('Error finding alternatives: $e');
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
/// Mettre à jour le stock d'un consommable/câble
|
|
Future<void> updateStock(String id, int quantityChange) async {
|
|
try {
|
|
final equipment = await getEquipmentById(id);
|
|
if (equipment == null) {
|
|
throw Exception('Equipment not found');
|
|
}
|
|
|
|
if (!equipment.hasQuantity) {
|
|
throw Exception('Equipment does not have quantity tracking');
|
|
}
|
|
|
|
final newAvailableQuantity = (equipment.availableQuantity ?? 0) + quantityChange;
|
|
|
|
await updateEquipment(id, {
|
|
'availableQuantity': newAvailableQuantity,
|
|
});
|
|
|
|
// Vérifier si le seuil critique est atteint
|
|
if (equipment.criticalThreshold != null &&
|
|
newAvailableQuantity <= equipment.criticalThreshold!) {
|
|
await _createLowStockAlert(equipment);
|
|
}
|
|
} catch (e) {
|
|
print('Error updating stock: $e');
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
/// Créer une alerte de stock faible
|
|
Future<void> _createLowStockAlert(EquipmentModel equipment) async {
|
|
try {
|
|
// Note: Cette fonction pourrait utiliser une Cloud Function dédiée dans le futur
|
|
// Pour l'instant, on utilise l'API directement pour éviter de créer trop de fonctions
|
|
// Cette méthode est appelée rarement et en arrière-plan
|
|
await _apiService.call('createAlert', {
|
|
'type': 'LOW_STOCK',
|
|
'title': 'Stock critique',
|
|
'message': 'Stock critique pour ${equipment.name} (${equipment.model ?? ""}): ${equipment.availableQuantity}/${equipment.criticalThreshold}',
|
|
'severity': 'HIGH',
|
|
'equipmentId': equipment.id,
|
|
});
|
|
} catch (e) {
|
|
print('Error creating low stock alert: $e');
|
|
// Ne pas rethrow pour ne pas bloquer le processus
|
|
}
|
|
}
|
|
|
|
/// Générer les données du QR code (ID de l'équipement)
|
|
String generateQRCodeData(String equipmentId) {
|
|
// Pour l'instant, on retourne simplement l'ID
|
|
// On pourrait aussi générer une URL complète : https://app.em2events.fr/equipment/$equipmentId
|
|
return equipmentId;
|
|
}
|
|
|
|
/// Récupérer les modèles filtrés par marque
|
|
Future<List<String>> getModelsByBrand(String brand) async {
|
|
try {
|
|
final equipmentsData = await _getAllEquipmentsPaginated();
|
|
final models = <String>{};
|
|
|
|
for (var data in equipmentsData) {
|
|
if (data['brand'] == brand) {
|
|
final model = data['model'] as String?;
|
|
if (model != null && model.isNotEmpty) {
|
|
models.add(model);
|
|
}
|
|
}
|
|
}
|
|
|
|
return models.toList()..sort();
|
|
} catch (e) {
|
|
print('Error getting models by brand: $e');
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
/// Récupérer les sous-catégories filtrées par catégorie
|
|
Future<List<String>> getSubCategoriesByCategory(EquipmentCategory category) async {
|
|
try {
|
|
final equipmentsData = await _getAllEquipmentsPaginated();
|
|
final subCategories = <String>{};
|
|
|
|
final categoryString = equipmentCategoryToString(category);
|
|
|
|
for (var data in equipmentsData) {
|
|
if (data['category'] == categoryString) {
|
|
final subCategory = data['subCategory'] as String?;
|
|
if (subCategory != null && subCategory.isNotEmpty) {
|
|
subCategories.add(subCategory);
|
|
}
|
|
}
|
|
}
|
|
|
|
return subCategories.toList()..sort();
|
|
} catch (e) {
|
|
print('Error getting subcategories by category: $e');
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
/// Vérifier si un ID existe déjà
|
|
Future<bool> isIdUnique(String id) async {
|
|
try {
|
|
final equipment = await getEquipmentById(id);
|
|
return equipment == null;
|
|
} catch (e) {
|
|
print('Error checking ID uniqueness: $e');
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
/// Récupérer toutes les boîtes/containers disponibles
|
|
Future<List<ContainerModel>> getBoxes() async {
|
|
try {
|
|
final containersData = await _dataService.getContainers();
|
|
|
|
final boxes = <ContainerModel>[];
|
|
for (var data in containersData) {
|
|
final id = data['id'] as String;
|
|
final container = ContainerModel.fromMap(data, id);
|
|
boxes.add(container);
|
|
}
|
|
|
|
return boxes;
|
|
} catch (e) {
|
|
print('Error getting boxes: $e');
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
/// Récupérer plusieurs équipements par leurs IDs
|
|
Future<List<EquipmentModel>> getEquipmentsByIds(List<String> ids) async {
|
|
try {
|
|
if (ids.isEmpty) return [];
|
|
|
|
final equipmentsData = await _dataService.getEquipmentsByIds(ids);
|
|
|
|
return equipmentsData
|
|
.map((data) {
|
|
final id = data['id'] as String;
|
|
return EquipmentModel.fromMap(data, id);
|
|
})
|
|
.toList();
|
|
} catch (e) {
|
|
print('Error getting equipments by IDs: $e');
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
/// Récupérer les maintenances pour un équipement
|
|
/// Note: Cette méthode est maintenant déléguée au MaintenanceService
|
|
/// pour éviter la duplication de code
|
|
Future<List<MaintenanceModel>> getMaintenancesForEquipment(String equipmentId) async {
|
|
try {
|
|
// Déléguer au MaintenanceService qui utilise déjà les Cloud Functions
|
|
final maintenanceService = MaintenanceService();
|
|
return await maintenanceService.getMaintenancesByEquipment(equipmentId);
|
|
} catch (e) {
|
|
print('Error getting maintenances for equipment: $e');
|
|
rethrow;
|
|
}
|
|
}
|
|
}
|
|
|