Files
EM2_ERP/em2rp/lib/services/event_availability_service.dart

429 lines
15 KiB
Dart

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<String>? 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<List<AvailabilityConflict>> 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 = <AvailabilityConflict>[];
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<Map<String, List<AvailabilityConflict>>> checkMultipleEquipmentAvailability({
required List<String> equipmentIds,
required Map<String, String> equipmentNames,
required DateTime startDate,
required DateTime endDate,
String? excludeEventId,
}) async {
final allConflicts = <String, List<AvailabilityConflict>>{};
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<int> 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<List<AvailabilityConflict>> checkEquipmentAvailabilityWithQuantity({
required EquipmentModel equipment,
required int requestedQuantity,
required DateTime startDate,
required DateTime endDate,
String? excludeEventId,
}) async {
final conflicts = <AvailabilityConflict>[];
// 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<List<AvailabilityConflict>> checkContainerAvailability({
required ContainerModel container,
required List<EquipmentModel> containerEquipment,
required DateTime startDate,
required DateTime endDate,
String? excludeEventId,
}) async {
final conflicts = <AvailabilityConflict>[];
final conflictingChildrenIds = <String>[];
// 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;
}
}