import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:em2rp/models/event_model.dart'; import 'package:em2rp/models/equipment_model.dart'; import 'package:em2rp/models/container_model.dart'; /// Type de conflit enum ConflictType { equipmentUnavailable, // Équipement non quantifiable utilisé insufficientQuantity, // Quantité insuffisante pour consommable/câble containerFullyUsed, // Boîte complète utilisée containerPartiallyUsed, // Certains équipements de la boîte utilisés } /// Informations sur un conflit de disponibilité class AvailabilityConflict { final String equipmentId; final String equipmentName; final EventModel conflictingEvent; final int overlapDays; final ConflictType type; // Pour les quantités (consommables/câbles) final int? totalQuantity; final int? availableQuantity; final int? requestedQuantity; final int? reservedQuantity; // Pour les boîtes final String? containerId; final String? containerName; final List? conflictingChildrenIds; AvailabilityConflict({ required this.equipmentId, required this.equipmentName, required this.conflictingEvent, required this.overlapDays, this.type = ConflictType.equipmentUnavailable, this.totalQuantity, this.availableQuantity, this.requestedQuantity, this.reservedQuantity, this.containerId, this.containerName, this.conflictingChildrenIds, }); /// Message descriptif du conflit String get conflictMessage { switch (type) { case ConflictType.equipmentUnavailable: return 'Équipement déjà utilisé'; case ConflictType.insufficientQuantity: return 'Stock insuffisant : $availableQuantity/$totalQuantity disponible (demandé: $requestedQuantity)'; case ConflictType.containerFullyUsed: return 'Boîte complète déjà utilisée'; case ConflictType.containerPartiallyUsed: final count = conflictingChildrenIds?.length ?? 0; return '$count équipement(s) de la boîte déjà utilisé(s)'; } } } /// Service pour vérifier la disponibilité du matériel class EventAvailabilityService { final FirebaseFirestore _firestore = FirebaseFirestore.instance; /// Vérifie si un équipement est disponible pour une plage de dates Future> checkEquipmentAvailability({ required String equipmentId, required String equipmentName, required DateTime startDate, required DateTime endDate, String? excludeEventId, // Pour exclure l'événement en cours d'édition }) async { final conflicts = []; try { // Récupérer TOUS les événements (on filtre côté client car arrayContains avec objet ne marche pas) final eventsSnapshot = await _firestore.collection('events').get(); for (var doc in eventsSnapshot.docs) { if (excludeEventId != null && doc.id == excludeEventId) { continue; // Ignorer l'événement en cours d'édition } try { final data = doc.data(); final event = EventModel.fromMap(data, doc.id); // Ignorer les événements annulés if (event.status == EventStatus.canceled) { continue; } // Vérifier si cet événement contient l'équipement recherché final assignedEquipment = event.assignedEquipment.firstWhere( (eq) => eq.equipmentId == equipmentId, orElse: () => EventEquipment(equipmentId: ''), ); // Si l'équipement est assigné à cet événement, il est indisponible // (peu importe le statut de préparation/chargement/retour) if (assignedEquipment.equipmentId.isNotEmpty) { // Calculer les dates réelles avec temps d'installation et démontage final eventRealStartDate = event.startDateTime.subtract( Duration(hours: event.installationTime), ); final eventRealEndDate = event.endDateTime.add( Duration(hours: event.disassemblyTime), ); // Vérifier le chevauchement des dates if (_datesOverlap(startDate, endDate, eventRealStartDate, eventRealEndDate)) { final overlapDays = _calculateOverlapDays( startDate, endDate, eventRealStartDate, eventRealEndDate, ); conflicts.add(AvailabilityConflict( equipmentId: equipmentId, equipmentName: equipmentName, conflictingEvent: event, overlapDays: overlapDays, )); } } } catch (e) { print('[EventAvailabilityService] Error processing event ${doc.id}: $e'); } } } catch (e) { print('[EventAvailabilityService] Error checking availability: $e'); } return conflicts; } /// Helper pour formater les dates dans les logs String _formatDate(DateTime date) { return '${date.day.toString().padLeft(2, '0')}/${date.month.toString().padLeft(2, '0')}/${date.year} ${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}'; } /// Vérifie la disponibilité pour une liste d'équipements Future>> checkMultipleEquipmentAvailability({ required List equipmentIds, required Map equipmentNames, required DateTime startDate, required DateTime endDate, String? excludeEventId, }) async { final allConflicts = >{}; for (var equipmentId in equipmentIds) { final conflicts = await checkEquipmentAvailability( equipmentId: equipmentId, equipmentName: equipmentNames[equipmentId] ?? equipmentId, startDate: startDate, endDate: endDate, excludeEventId: excludeEventId, ); if (conflicts.isNotEmpty) { allConflicts[equipmentId] = conflicts; } } return allConflicts; } /// Vérifie si deux plages de dates se chevauchent bool _datesOverlap(DateTime start1, DateTime end1, DateTime start2, DateTime end2) { // Deux plages se chevauchent si elles ne sont PAS complètement séparées // Elles sont séparées si : end1 < start2 OU end2 < start1 // Donc elles se chevauchent si : NOT (end1 < start2 OU end2 < start1) // Équivalent à : end1 >= start2 ET end2 >= start1 return !end1.isBefore(start2) && !end2.isBefore(start1); } /// Calcule le nombre de jours de chevauchement int _calculateOverlapDays(DateTime start1, DateTime end1, DateTime start2, DateTime end2) { final overlapStart = start1.isAfter(start2) ? start1 : start2; final overlapEnd = end1.isBefore(end2) ? end1 : end2; return overlapEnd.difference(overlapStart).inDays + 1; } /// Récupère la quantité disponible pour un consommable/câble Future getAvailableQuantity({ required EquipmentModel equipment, required DateTime startDate, required DateTime endDate, String? excludeEventId, }) async { if (!equipment.hasQuantity) { return 1; // Équipement non consommable } final totalQuantity = equipment.totalQuantity ?? 0; int reservedQuantity = 0; try { // Récupérer tous les événements (on filtre côté client) final eventsSnapshot = await _firestore.collection('events').get(); for (var doc in eventsSnapshot.docs) { if (excludeEventId != null && doc.id == excludeEventId) { continue; } try { final event = EventModel.fromMap(doc.data(), doc.id); // Ignorer les événements annulés if (event.status == EventStatus.canceled) { continue; } // Calculer les dates réelles avec temps d'installation et démontage final eventRealStartDate = event.startDateTime.subtract( Duration(hours: event.installationTime), ); final eventRealEndDate = event.endDateTime.add( Duration(hours: event.disassemblyTime), ); // Vérifier le chevauchement des dates if (_datesOverlap(startDate, endDate, eventRealStartDate, eventRealEndDate)) { final assignedEquipment = event.assignedEquipment.firstWhere( (eq) => eq.equipmentId == equipment.id, orElse: () => EventEquipment(equipmentId: ''), ); // Si l'équipement est assigné, réserver la quantité // (peu importe le statut de préparation/retour) if (assignedEquipment.equipmentId.isNotEmpty) { reservedQuantity += assignedEquipment.quantity; } } } catch (e) { print('[EventAvailabilityService] Error processing event ${doc.id} for quantity: $e'); } } } catch (e) { print('[EventAvailabilityService] Error getting available quantity: $e'); } return totalQuantity - reservedQuantity; } /// Vérifie la disponibilité d'un équipement avec gestion des quantités Future> checkEquipmentAvailabilityWithQuantity({ required EquipmentModel equipment, required int requestedQuantity, required DateTime startDate, required DateTime endDate, String? excludeEventId, }) async { final conflicts = []; // Si équipement quantifiable (consommable/câble) if (equipment.hasQuantity) { final totalQuantity = equipment.totalQuantity ?? 0; final availableQty = await getAvailableQuantity( equipment: equipment, startDate: startDate, endDate: endDate, excludeEventId: excludeEventId, ); final reservedQty = totalQuantity - availableQty; // ✅ Ne créer un conflit que si la quantité est VRAIMENT insuffisante if (availableQty < requestedQuantity) { // Trouver les événements qui réservent cette quantité final eventsSnapshot = await _firestore.collection('events').get(); for (var doc in eventsSnapshot.docs) { if (excludeEventId != null && doc.id == excludeEventId) continue; try { final event = EventModel.fromMap(doc.data(), doc.id); if (_datesOverlap(startDate, endDate, event.startDateTime, event.endDateTime)) { final assignedEquipment = event.assignedEquipment.firstWhere( (eq) => eq.equipmentId == equipment.id, orElse: () => EventEquipment(equipmentId: ''), ); if (assignedEquipment.equipmentId.isNotEmpty && !assignedEquipment.isReturned) { conflicts.add(AvailabilityConflict( equipmentId: equipment.id, equipmentName: equipment.name, conflictingEvent: event, overlapDays: _calculateOverlapDays(startDate, endDate, event.startDateTime, event.endDateTime), type: ConflictType.insufficientQuantity, totalQuantity: totalQuantity, availableQuantity: availableQty, requestedQuantity: requestedQuantity, reservedQuantity: reservedQty, )); } } } catch (e) { print('[EventAvailabilityService] Error processing event ${doc.id}: $e'); } } } } else { // Équipement non quantifiable : vérification classique return await checkEquipmentAvailability( equipmentId: equipment.id, equipmentName: equipment.name, startDate: startDate, endDate: endDate, excludeEventId: excludeEventId, ); } return conflicts; } /// Vérifie la disponibilité d'une boîte et de son contenu Future> checkContainerAvailability({ required ContainerModel container, required List containerEquipment, required DateTime startDate, required DateTime endDate, String? excludeEventId, }) async { final conflicts = []; final conflictingChildrenIds = []; // Vérifier d'abord si la boîte complète est utilisée final eventsSnapshot = await _firestore.collection('events').get(); bool isContainerFullyUsed = false; EventModel? containerConflictingEvent; for (var doc in eventsSnapshot.docs) { if (excludeEventId != null && doc.id == excludeEventId) continue; try { final event = EventModel.fromMap(doc.data(), doc.id); // Ignorer les événements annulés if (event.status == EventStatus.canceled) { continue; } // Calculer les dates réelles avec temps d'installation et démontage final eventRealStartDate = event.startDateTime.subtract( Duration(hours: event.installationTime), ); final eventRealEndDate = event.endDateTime.add( Duration(hours: event.disassemblyTime), ); // Vérifier si cette boîte est assignée if (event.assignedContainers.contains(container.id)) { if (_datesOverlap(startDate, endDate, eventRealStartDate, eventRealEndDate)) { isContainerFullyUsed = true; containerConflictingEvent = event; break; } } } catch (e) { print('[EventAvailabilityService] Error processing event ${doc.id}: $e'); } } if (isContainerFullyUsed && containerConflictingEvent != null) { // Boîte complète utilisée conflicts.add(AvailabilityConflict( equipmentId: container.id, equipmentName: container.name, conflictingEvent: containerConflictingEvent, overlapDays: _calculateOverlapDays( startDate, endDate, containerConflictingEvent.startDateTime, containerConflictingEvent.endDateTime, ), type: ConflictType.containerFullyUsed, containerId: container.id, containerName: container.name, )); } else { // Vérifier chaque équipement enfant individuellement for (var equipment in containerEquipment) { final equipmentConflicts = await checkEquipmentAvailability( equipmentId: equipment.id, equipmentName: equipment.name, startDate: startDate, endDate: endDate, excludeEventId: excludeEventId, ); if (equipmentConflicts.isNotEmpty) { conflictingChildrenIds.add(equipment.id); conflicts.addAll(equipmentConflicts); } } // Si au moins un enfant en conflit, ajouter un conflit pour la boîte if (conflictingChildrenIds.isNotEmpty && conflicts.isNotEmpty) { conflicts.insert( 0, AvailabilityConflict( equipmentId: container.id, equipmentName: container.name, conflictingEvent: conflicts.first.conflictingEvent, overlapDays: conflicts.first.overlapDays, type: ConflictType.containerPartiallyUsed, containerId: container.id, containerName: container.name, conflictingChildrenIds: conflictingChildrenIds, ), ); } } return conflicts; } }