perf: suppression du téléchargement massif d'événements côté client (appel de la CF checkContainerAvailability et lecture synchrone des quantités)

This commit is contained in:
ElPoyo
2026-05-26 13:50:35 +02:00
parent 93c102012b
commit 845b6e91d2
4 changed files with 78 additions and 309 deletions
@@ -67,27 +67,19 @@ class AvailabilityConflict {
class EventAvailabilityService {
final DataService _dataService = DataService(apiService);
/// Helper pour récupérer uniquement la liste d'événements
Future<List<Map<String, dynamic>>> _getEventsList() async {
final result = await _dataService.getEvents();
final events = result['events'] as List<dynamic>? ?? [];
return events.map((e) => e as Map<String, dynamic>).toList();
}
/// Vérifie si un équipement est disponible pour une plage de dates via Cloud Function
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
String? excludeEventId,
}) async {
final conflicts = <AvailabilityConflict>[];
try {
if (kDebugMode) debugPrint('[EventAvailabilityService] Checking availability for equipment $equipmentId ($equipmentName)');
// Utiliser la Cloud Function pour vérifier la disponibilité
final result = await _dataService.checkEquipmentAvailability(
equipmentId: equipmentId,
startDate: startDate,
@@ -95,20 +87,12 @@ class EventAvailabilityService {
excludeEventId: excludeEventId,
);
if (kDebugMode) debugPrint('[EventAvailabilityService] Result for $equipmentId: $result');
final available = result['available'] as bool? ?? true;
if (kDebugMode) debugPrint('[EventAvailabilityService] Equipment $equipmentId available: $available');
if (!available) {
final conflictsData = result['conflicts'] as List<dynamic>? ?? [];
if (kDebugMode) debugPrint('[EventAvailabilityService] Found ${conflictsData.length} conflicts for equipment $equipmentId');
for (final conflictData in conflictsData) {
final conflict = conflictData as Map<String, dynamic>;
final eventId = conflict['eventId'] as String;
// Le backend retourne déjà eventData
final eventData = conflict['eventData'] as Map<String, dynamic>?;
if (eventData != null && eventData.isNotEmpty) {
@@ -120,10 +104,8 @@ class EventAvailabilityService {
conflictingEvent: event,
overlapDays: conflict['overlapDays'] as int? ?? 0,
));
if (kDebugMode) debugPrint('[EventAvailabilityService] Added conflict with event ${event.name}');
} catch (e) {
if (kDebugMode) debugPrint('[EventAvailabilityService] Error creating EventModel: $e');
if (kDebugMode) debugPrint('[EventAvailabilityService] EventData: $eventData');
}
}
}
@@ -132,7 +114,6 @@ class EventAvailabilityService {
if (kDebugMode) debugPrint('[EventAvailabilityService] Error checking availability: $e');
}
if (kDebugMode) debugPrint('[EventAvailabilityService] Returning ${conflicts.length} conflicts for equipment $equipmentId');
return conflicts;
}
@@ -160,164 +141,10 @@ class EventAvailabilityService {
}
}
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 via Cloud Function
final eventsData = await _getEventsList();
for (var eventData in eventsData) {
final eventId = eventData['id'] as String;
if (excludeEventId != null && eventId == excludeEventId) {
continue;
}
try {
final event = EventModel.fromMap(eventData, eventId);
// 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) {
if (kDebugMode) debugPrint('[EventAvailabilityService] Error processing event $eventId for quantity: $e');
}
}
} catch (e) {
if (kDebugMode) debugPrint('[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 eventsData = await _getEventsList();
for (var eventData in eventsData) {
final eventId = eventData['id'] as String;
if (excludeEventId != null && eventId == excludeEventId) continue;
try {
final event = EventModel.fromMap(eventData, eventId);
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) {
if (kDebugMode) debugPrint('[EventAvailabilityService] Error processing event $eventId: $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
/// Vérifie la disponibilité d'une boîte et de son contenu via le backend
Future<List<AvailabilityConflict>> checkContainerAvailability({
required ContainerModel container,
required List<EquipmentModel> containerEquipment,
@@ -326,99 +153,45 @@ class EventAvailabilityService {
String? excludeEventId,
}) async {
final conflicts = <AvailabilityConflict>[];
final conflictingChildrenIds = <String>[];
// Vérifier d'abord si la boîte complète est utilisée
final eventsData = await _getEventsList();
bool isContainerFullyUsed = false;
EventModel? containerConflictingEvent;
try {
final result = await _dataService.checkContainerAvailability(
containerId: container.id,
startDate: startDate,
endDate: endDate,
excludeEventId: excludeEventId,
);
for (var eventData in eventsData) {
final eventId = eventData['id'] as String;
if (excludeEventId != null && eventId == excludeEventId) continue;
try {
final event = EventModel.fromMap(eventData, eventId);
// 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;
final isAvailable = result['isAvailable'] as bool? ?? true;
if (!isAvailable) {
final conflictType = result['conflictType'] as String?;
if (conflictType == 'complete') {
final containerConflicts = result['containerConflicts'] as List<dynamic>? ?? [];
for (var conflictData in containerConflicts) {
final conflict = conflictData as Map<String, dynamic>;
final eventId = conflict['eventId'] as String;
final eventDoc = await _dataService.getEvents();
final eventData = (eventDoc['events'] as List<dynamic>).firstWhere((e) => e['id'] == eventId, orElse: () => null);
if (eventData != null) {
final event = EventModel.fromMap(eventData as Map<String, dynamic>, eventId);
conflicts.add(AvailabilityConflict(
equipmentId: container.id,
equipmentName: container.name,
conflictingEvent: event,
overlapDays: conflict['overlapDays'] as int? ?? 0,
type: ConflictType.containerFullyUsed,
containerId: container.id,
containerName: container.name,
));
}
}
}
} catch (e) {
if (kDebugMode) debugPrint('[EventAvailabilityService] Error processing event $eventId: $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,
),
);
}
} catch (e) {
if (kDebugMode) debugPrint('[EventAvailabilityService] Error checking container availability: $e');
}
return conflicts;
}
}