Add equipment management features (and qr generation support)
This commit is contained in:
		
							
								
								
									
										373
									
								
								em2rp/lib/services/equipment_service.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										373
									
								
								em2rp/lib/services/equipment_service.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,373 @@ | ||||
| import 'package:cloud_firestore/cloud_firestore.dart'; | ||||
| import 'package:em2rp/models/equipment_model.dart'; | ||||
| import 'package:em2rp/models/alert_model.dart'; | ||||
|  | ||||
| class EquipmentService { | ||||
|   final FirebaseFirestore _firestore = FirebaseFirestore.instance; | ||||
|  | ||||
|   // Collection references | ||||
|   CollectionReference get _equipmentCollection => _firestore.collection('equipments'); | ||||
|   CollectionReference get _alertsCollection => _firestore.collection('alerts'); | ||||
|   CollectionReference get _eventsCollection => _firestore.collection('events'); | ||||
|  | ||||
|   // CRUD Operations | ||||
|  | ||||
|   /// Créer un nouvel équipement | ||||
|   Future<void> createEquipment(EquipmentModel equipment) async { | ||||
|     try { | ||||
|       await _equipmentCollection.doc(equipment.id).set(equipment.toMap()); | ||||
|     } catch (e) { | ||||
|       print('Error creating equipment: $e'); | ||||
|       rethrow; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /// Mettre à jour un équipement | ||||
|   Future<void> updateEquipment(String id, Map<String, dynamic> data) async { | ||||
|     try { | ||||
|       data['updatedAt'] = Timestamp.fromDate(DateTime.now()); | ||||
|       await _equipmentCollection.doc(id).update(data); | ||||
|     } catch (e) { | ||||
|       print('Error updating equipment: $e'); | ||||
|       rethrow; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /// Supprimer un équipement | ||||
|   Future<void> deleteEquipment(String id) async { | ||||
|     try { | ||||
|       await _equipmentCollection.doc(id).delete(); | ||||
|     } catch (e) { | ||||
|       print('Error deleting equipment: $e'); | ||||
|       rethrow; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /// Récupérer un équipement par ID | ||||
|   Future<EquipmentModel?> getEquipmentById(String id) async { | ||||
|     try { | ||||
|       final doc = await _equipmentCollection.doc(id).get(); | ||||
|       if (doc.exists) { | ||||
|         return EquipmentModel.fromMap(doc.data() as Map<String, dynamic>, doc.id); | ||||
|       } | ||||
|       return null; | ||||
|     } catch (e) { | ||||
|       print('Error getting equipment: $e'); | ||||
|       rethrow; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /// Récupérer les équipements avec filtres | ||||
|   Stream<List<EquipmentModel>> getEquipment({ | ||||
|     EquipmentCategory? category, | ||||
|     EquipmentStatus? status, | ||||
|     String? model, | ||||
|     String? searchQuery, | ||||
|   }) { | ||||
|     try { | ||||
|       Query query = _equipmentCollection; | ||||
|  | ||||
|       // Filtre par catégorie | ||||
|       if (category != null) { | ||||
|         query = query.where('category', isEqualTo: equipmentCategoryToString(category)); | ||||
|       } | ||||
|  | ||||
|       // Filtre par statut | ||||
|       if (status != null) { | ||||
|         query = query.where('status', isEqualTo: equipmentStatusToString(status)); | ||||
|       } | ||||
|  | ||||
|       // Filtre par modèle | ||||
|       if (model != null && model.isNotEmpty) { | ||||
|         query = query.where('model', isEqualTo: model); | ||||
|       } | ||||
|  | ||||
|       return query.snapshots().map((snapshot) { | ||||
|         List<EquipmentModel> equipmentList = snapshot.docs | ||||
|             .map((doc) => EquipmentModel.fromMap(doc.data() as Map<String, dynamic>, doc.id)) | ||||
|             .toList(); | ||||
|  | ||||
|         // Filtre par recherche texte (côté client car Firestore ne supporte pas les recherches texte complexes) | ||||
|         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 streaming equipment: $e'); | ||||
|       rethrow; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /// Vérifier la disponibilité d'un équipement pour une période donnée | ||||
|   Future<List<String>> checkAvailability( | ||||
|     String equipmentId, | ||||
|     DateTime startDate, | ||||
|     DateTime endDate, | ||||
|   ) async { | ||||
|     try { | ||||
|       final conflicts = <String>[]; | ||||
|  | ||||
|       // Récupérer tous les événements qui chevauchent la période | ||||
|       final eventsQuery = await _eventsCollection | ||||
|           .where('StartDateTime', isLessThanOrEqualTo: Timestamp.fromDate(endDate)) | ||||
|           .where('EndDateTime', isGreaterThanOrEqualTo: Timestamp.fromDate(startDate)) | ||||
|           .get(); | ||||
|  | ||||
|       for (var eventDoc in eventsQuery.docs) { | ||||
|         final eventData = eventDoc.data() as Map<String, dynamic>; | ||||
|         final assignedEquipmentRaw = eventData['assignedEquipment'] ?? []; | ||||
|  | ||||
|         if (assignedEquipmentRaw is List) { | ||||
|           for (var eq in assignedEquipmentRaw) { | ||||
|             if (eq is Map && eq['equipmentId'] == equipmentId) { | ||||
|               conflicts.add(eventDoc.id); | ||||
|               break; | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       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 { | ||||
|       // Récupérer tous les équipements du même modèle | ||||
|       final equipmentQuery = await _equipmentCollection | ||||
|           .where('model', isEqualTo: model) | ||||
|           .get(); | ||||
|  | ||||
|       final alternatives = <EquipmentModel>[]; | ||||
|  | ||||
|       for (var doc in equipmentQuery.docs) { | ||||
|         final equipment = EquipmentModel.fromMap( | ||||
|           doc.data() as Map<String, dynamic>, | ||||
|           doc.id, | ||||
|         ); | ||||
|  | ||||
|         // Vérifier la disponibilité | ||||
|         final conflicts = await checkAvailability(equipment.id, startDate, endDate); | ||||
|  | ||||
|         if (conflicts.isEmpty && equipment.status == EquipmentStatus.available) { | ||||
|           alternatives.add(equipment); | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       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; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /// Vérifier les stocks critiques et créer des alertes | ||||
|   Future<void> checkCriticalStock() async { | ||||
|     try { | ||||
|       final equipmentQuery = await _equipmentCollection | ||||
|           .where('category', whereIn: [ | ||||
|             equipmentCategoryToString(EquipmentCategory.consumable), | ||||
|             equipmentCategoryToString(EquipmentCategory.cable), | ||||
|           ]) | ||||
|           .get(); | ||||
|  | ||||
|       for (var doc in equipmentQuery.docs) { | ||||
|         final equipment = EquipmentModel.fromMap( | ||||
|           doc.data() as Map<String, dynamic>, | ||||
|           doc.id, | ||||
|         ); | ||||
|  | ||||
|         if (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 { | ||||
|       // Vérifier si une alerte existe déjà pour cet équipement | ||||
|       final existingAlerts = await _alertsCollection | ||||
|           .where('equipmentId', isEqualTo: equipment.id) | ||||
|           .where('type', isEqualTo: alertTypeToString(AlertType.lowStock)) | ||||
|           .where('isRead', isEqualTo: false) | ||||
|           .get(); | ||||
|  | ||||
|       if (existingAlerts.docs.isEmpty) { | ||||
|         final alert = AlertModel( | ||||
|           id: _alertsCollection.doc().id, | ||||
|           type: AlertType.lowStock, | ||||
|           message: 'Stock critique pour ${equipment.name} (${equipment.model ?? ""}): ${equipment.availableQuantity}/${equipment.criticalThreshold}', | ||||
|           equipmentId: equipment.id, | ||||
|           createdAt: DateTime.now(), | ||||
|         ); | ||||
|  | ||||
|         await _alertsCollection.doc(alert.id).set(alert.toMap()); | ||||
|       } | ||||
|     } catch (e) { | ||||
|       print('Error creating low stock alert: $e'); | ||||
|       rethrow; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /// 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 tous les modèles uniques (pour l'indexation/autocomplete) | ||||
|   Future<List<String>> getAllModels() async { | ||||
|     try { | ||||
|       final equipmentQuery = await _equipmentCollection.get(); | ||||
|       final models = <String>{}; | ||||
|  | ||||
|       for (var doc in equipmentQuery.docs) { | ||||
|         final data = doc.data() as Map<String, dynamic>; | ||||
|         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 equipmentQuery = await _equipmentCollection.get(); | ||||
|       final brands = <String>{}; | ||||
|  | ||||
|       for (var doc in equipmentQuery.docs) { | ||||
|         final data = doc.data() as Map<String, dynamic>; | ||||
|         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 equipmentQuery = await _equipmentCollection | ||||
|           .where('brand', isEqualTo: brand) | ||||
|           .get(); | ||||
|       final models = <String>{}; | ||||
|  | ||||
|       for (var doc in equipmentQuery.docs) { | ||||
|         final data = doc.data() as Map<String, dynamic>; | ||||
|         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; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /// Vérifier si un ID existe déjà | ||||
|   Future<bool> isIdUnique(String id) async { | ||||
|     try { | ||||
|       final doc = await _equipmentCollection.doc(id).get(); | ||||
|       return !doc.exists; | ||||
|     } catch (e) { | ||||
|       print('Error checking ID uniqueness: $e'); | ||||
|       rethrow; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /// Récupérer toutes les boîtes (équipements qui peuvent contenir d'autres équipements) | ||||
|   Future<List<EquipmentModel>> getBoxes() async { | ||||
|     try { | ||||
|       // Les boîtes sont généralement des équipements de catégorie "structure" ou "other" | ||||
|       // On pourrait aussi ajouter un champ spécifique "isBox" dans le modèle | ||||
|       final equipmentQuery = await _equipmentCollection | ||||
|           .where('category', whereIn: [ | ||||
|             equipmentCategoryToString(EquipmentCategory.structure), | ||||
|             equipmentCategoryToString(EquipmentCategory.other), | ||||
|           ]) | ||||
|           .get(); | ||||
|  | ||||
|       final boxes = <EquipmentModel>[]; | ||||
|       for (var doc in equipmentQuery.docs) { | ||||
|         final equipment = EquipmentModel.fromMap( | ||||
|           doc.data() as Map<String, dynamic>, | ||||
|           doc.id, | ||||
|         ); | ||||
|         // On pourrait ajouter un filtre supplémentaire ici si besoin | ||||
|         boxes.add(equipment); | ||||
|       } | ||||
|  | ||||
|       return boxes; | ||||
|     } catch (e) { | ||||
|       print('Error getting boxes: $e'); | ||||
|       rethrow; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 ElPoyo
					ElPoyo