 3fab69cb00
			
		
	
	3fab69cb00
	
	
	
		
			
			Ajout de la gestion des containers (création, édition, suppression, affichage des détails).
Introduction d'un système de génération de QR codes unifié et d'un mode de sélection multiple.
**Features:**
- **Gestion des Containers :**
    - Nouvelle page de gestion des containers (`container_management_page.dart`) avec recherche et filtres.
    - Formulaire de création/édition de containers (`container_form_page.dart`) avec génération d'ID automatique.
    - Page de détails d'un container (`container_detail_page.dart`) affichant son contenu et ses caractéristiques.
    - Ajout des routes et du provider (`ContainerProvider`) nécessaires.
- **Modèle de Données :**
    - Ajout du `ContainerModel` pour représenter les boîtes, flight cases, etc.
    - Le modèle `EquipmentModel` a été enrichi avec des caractéristiques physiques (poids, dimensions).
- **QR Codes :**
    - Nouveau service unifié (`UnifiedPDFGeneratorService`) pour générer des PDFs de QR codes pour n'importe quelle entité.
    - Services `PDFGeneratorService` et `ContainerPDFGeneratorService` transformés en wrappers pour maintenir la compatibilité.
    - Amélioration de la performance de la génération de QR codes en masse.
- **Interface Utilisateur (UI/UX) :**
    - Nouvelle page de détails pour le matériel (`equipment_detail_page.dart`).
    - Ajout d'un `SelectionModeMixin` pour gérer la sélection multiple dans les pages de gestion.
    - Dialogues réutilisables pour l'affichage de QR codes (`QRCodeDialog`) et la sélection de format d'impression (`QRCodeFormatSelectorDialog`).
    - Ajout d'un bouton "Gérer les boîtes" sur la page de gestion du matériel.
**Refactorisation :**
- L' `IdGenerator` a été déplacé dans le répertoire `utils` et étendu pour gérer les containers.
- Mise à jour de nombreuses dépendances `pubspec.yaml` vers des versions plus récentes.
- Séparation de la logique d'affichage des containers et du matériel dans des widgets dédiés (`ContainerHeaderCard`, `EquipmentParentContainers`, etc.).
		
	
		
			
				
	
	
		
			433 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			433 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
| import 'package:cloud_firestore/cloud_firestore.dart';
 | |
| import 'package:em2rp/models/equipment_model.dart';
 | |
| import 'package:em2rp/models/alert_model.dart';
 | |
| import 'package:em2rp/models/maintenance_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;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /// Récupérer plusieurs équipements par leurs IDs
 | |
|   Future<List<EquipmentModel>> getEquipmentsByIds(List<String> ids) async {
 | |
|     try {
 | |
|       if (ids.isEmpty) return [];
 | |
| 
 | |
|       final equipments = <EquipmentModel>[];
 | |
| 
 | |
|       // 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 _equipmentCollection
 | |
|             .where(FieldPath.documentId, whereIn: batch)
 | |
|             .get();
 | |
| 
 | |
|         for (var doc in query.docs) {
 | |
|           equipments.add(
 | |
|             EquipmentModel.fromMap(
 | |
|               doc.data() as Map<String, dynamic>,
 | |
|               doc.id,
 | |
|             ),
 | |
|           );
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       return equipments;
 | |
|     } catch (e) {
 | |
|       print('Error getting equipments by IDs: $e');
 | |
|       rethrow;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /// Récupérer les maintenances pour un équipement
 | |
|   Future<List<MaintenanceModel>> getMaintenancesForEquipment(String equipmentId) async {
 | |
|     try {
 | |
|       final maintenanceQuery = await _firestore
 | |
|           .collection('maintenances')
 | |
|           .where('equipmentIds', arrayContains: equipmentId)
 | |
|           .orderBy('scheduledDate', descending: true)
 | |
|           .get();
 | |
| 
 | |
|       final maintenances = <MaintenanceModel>[];
 | |
|       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;
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 |