import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:em2rp/models/equipment_model.dart'; import 'package:em2rp/models/container_model.dart'; import 'package:em2rp/models/alert_model.dart'; import 'package:em2rp/models/maintenance_model.dart'; import 'package:em2rp/services/api_service.dart'; import 'package:em2rp/services/data_service.dart'; class EquipmentService { final FirebaseFirestore _firestore = FirebaseFirestore.instance; final ApiService _apiService = apiService; final DataService _dataService = DataService(apiService); // Collection references (utilisées seulement pour les lectures) CollectionReference get _equipmentCollection => _firestore.collection('equipments'); // ============================================================================ // CRUD Operations - Utilise le backend sécurisé // ============================================================================ /// Créer un nouvel équipement (via Cloud Function) Future createEquipment(EquipmentModel equipment) async { try { await _apiService.call('createEquipment', equipment.toMap()..['id'] = equipment.id); } catch (e) { print('Error creating equipment: $e'); rethrow; } } /// Mettre à jour un équipement (via Cloud Function) Future updateEquipment(String id, Map data) async { try { await _apiService.call('updateEquipment', { 'equipmentId': id, 'data': data, }); } catch (e) { print('Error updating equipment: $e'); rethrow; } } /// Supprimer un équipement (via Cloud Function) Future 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 getEquipmentById(String id) async { try { final doc = await _equipmentCollection.doc(id).get(); if (doc.exists) { return EquipmentModel.fromMap(doc.data() as Map, doc.id); } return null; } catch (e) { print('Error getting equipment: $e'); rethrow; } } /// Récupérer les équipements avec filtres (stream temps réel) Stream> 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 equipmentList = snapshot.docs .map((doc) => EquipmentModel.fromMap(doc.data() as Map, 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; } } // ============================================================================ // Availability & Stock Management - Logique métier côté client // ============================================================================ /// Vérifier la disponibilité d'un équipement pour une période donnée Future> checkAvailability( String equipmentId, DateTime startDate, DateTime endDate, ) async { try { final conflicts = []; // Récupérer tous les événements qui chevauchent la période final eventsQuery = await _firestore.collection('events') .where('StartDateTime', isLessThanOrEqualTo: Timestamp.fromDate(endDate)) .where('EndDateTime', isGreaterThanOrEqualTo: Timestamp.fromDate(startDate)) .get(); for (var eventDoc in eventsQuery.docs) { final eventData = eventDoc.data(); 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> findAlternatives( String model, DateTime startDate, DateTime endDate, ) async { try { // Récupérer tous les équipements du même modèle final equipmentQuery = await _firestore.collection('equipments') .where('model', isEqualTo: model) .get(); final alternatives = []; for (var doc in equipmentQuery.docs) { final equipment = EquipmentModel.fromMap( doc.data(), 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 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 checkCriticalStock() async { try { final equipmentQuery = await _firestore.collection('equipments') .where('category', whereIn: [ equipmentCategoryToString(EquipmentCategory.consumable), equipmentCategoryToString(EquipmentCategory.cable), ]) .get(); for (var doc in equipmentQuery.docs) { final equipment = EquipmentModel.fromMap( doc.data(), 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 _createLowStockAlert(EquipmentModel equipment) async { try { // Vérifier si une alerte existe déjà pour cet équipement final existingAlerts = await _firestore.collection('alerts') .where('equipmentId', isEqualTo: equipment.id) .where('type', isEqualTo: alertTypeToString(AlertType.lowStock)) .where('isRead', isEqualTo: false) .get(); if (existingAlerts.docs.isEmpty) { final alert = AlertModel( id: _firestore.collection('alerts').doc().id, type: AlertType.lowStock, message: 'Stock critique pour ${equipment.name} (${equipment.model ?? ""}): ${equipment.availableQuantity}/${equipment.criticalThreshold}', equipmentId: equipment.id, createdAt: DateTime.now(), ); await _firestore.collection('alerts').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> getAllModels() async { try { final equipmentQuery = await _firestore.collection('equipments').get(); final models = {}; for (var doc in equipmentQuery.docs) { final data = doc.data(); 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> getAllBrands() async { try { final equipmentQuery = await _firestore.collection('equipments').get(); final brands = {}; for (var doc in equipmentQuery.docs) { final data = doc.data(); 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> getModelsByBrand(String brand) async { try { final equipmentQuery = await _firestore.collection('equipments') .where('brand', isEqualTo: brand) .get(); final models = {}; for (var doc in equipmentQuery.docs) { final data = doc.data(); 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 isIdUnique(String id) async { try { final doc = await _firestore.collection('equipments').doc(id).get(); return !doc.exists; } catch (e) { print('Error checking ID uniqueness: $e'); rethrow; } } /// Récupérer toutes les boîtes/containers disponibles Future> getBoxes() async { try { final containersData = await _dataService.getContainers(); final boxes = []; 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> getEquipmentsByIds(List ids) async { try { if (ids.isEmpty) return []; final equipments = []; // Firestore limite les requêtes whereIn à 10 éléments // On doit donc diviser en plusieurs requêtes si nécessaire for (int i = 0; i < ids.length; i += 10) { final batch = ids.skip(i).take(10).toList(); final query = await _firestore.collection('equipments') .where(FieldPath.documentId, whereIn: batch) .get(); for (var doc in query.docs) { equipments.add( EquipmentModel.fromMap( doc.data(), doc.id, ), ); } } return equipments; } catch (e) { print('Error getting equipments by IDs: $e'); rethrow; } } /// Récupérer les maintenances pour un équipement Future> getMaintenancesForEquipment(String equipmentId) async { try { final maintenanceQuery = await _firestore .collection('maintenances') .where('equipmentIds', arrayContains: equipmentId) .orderBy('scheduledDate', descending: true) .get(); final maintenances = []; for (var doc in maintenanceQuery.docs) { maintenances.add( MaintenanceModel.fromMap( doc.data(), doc.id, ), ); } return maintenances; } catch (e) { print('Error getting maintenances for equipment: $e'); rethrow; } } }