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:
@@ -141,7 +141,6 @@ class ContainerService {
|
||||
}
|
||||
}
|
||||
|
||||
/// Vérifier la disponibilité d'un container et de son contenu pour un événement
|
||||
Future<Map<String, dynamic>> checkContainerAvailability({
|
||||
required String containerId,
|
||||
required DateTime startDate,
|
||||
@@ -149,43 +148,21 @@ class ContainerService {
|
||||
String? excludeEventId,
|
||||
}) async {
|
||||
try {
|
||||
final container = await getContainerById(containerId);
|
||||
if (container == null) {
|
||||
return {'available': false, 'message': 'Container non trouvé'};
|
||||
}
|
||||
|
||||
// Vérifier le statut du container
|
||||
if (container.status != EquipmentStatus.available) {
|
||||
return {
|
||||
'available': false,
|
||||
'message': 'Container ${container.name} n\'est pas disponible (statut: ${container.status})',
|
||||
};
|
||||
}
|
||||
|
||||
// Vérifier la disponibilité de chaque équipement dans le container
|
||||
List<String> unavailableEquipment = [];
|
||||
|
||||
if (container.equipmentIds.isNotEmpty) {
|
||||
final equipmentsData = await _dataService.getEquipmentsByIds(container.equipmentIds);
|
||||
|
||||
for (var data in equipmentsData) {
|
||||
final id = data['id'] as String;
|
||||
final equipment = EquipmentModel.fromMap(data, id);
|
||||
if (equipment.status != EquipmentStatus.available) {
|
||||
unavailableEquipment.add('${equipment.name} (${equipment.status})');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (unavailableEquipment.isNotEmpty) {
|
||||
return {
|
||||
'available': false,
|
||||
'message': 'Certains équipements ne sont pas disponibles',
|
||||
'unavailableItems': unavailableEquipment,
|
||||
};
|
||||
}
|
||||
|
||||
return {'available': true, 'message': 'Container et tout son contenu disponibles'};
|
||||
final result = await _dataService.checkContainerAvailability(
|
||||
containerId: containerId,
|
||||
startDate: startDate,
|
||||
endDate: endDate,
|
||||
excludeEventId: excludeEventId,
|
||||
);
|
||||
return {
|
||||
'available': result['isAvailable'] ?? false,
|
||||
'message': result['isAvailable'] == true
|
||||
? 'Container et tout son contenu disponibles'
|
||||
: 'Container non disponible ou en conflit',
|
||||
'conflictType': result['conflictType'],
|
||||
'containerConflicts': result['containerConflicts'],
|
||||
'equipmentConflicts': result['equipmentConflicts'],
|
||||
};
|
||||
} catch (e) {
|
||||
print('Error checking container availability: $e');
|
||||
return {'available': false, 'message': 'Erreur: $e'};
|
||||
|
||||
@@ -779,6 +779,26 @@ class DataService {
|
||||
}
|
||||
}
|
||||
|
||||
/// Vérifie la disponibilité d'un container
|
||||
Future<Map<String, dynamic>> checkContainerAvailability({
|
||||
required String containerId,
|
||||
required DateTime startDate,
|
||||
required DateTime endDate,
|
||||
String? excludeEventId,
|
||||
}) async {
|
||||
try {
|
||||
final result = await _apiService.call('checkContainerAvailability', {
|
||||
'containerId': containerId,
|
||||
'startDate': startDate.toIso8601String(),
|
||||
'endDate': endDate.toIso8601String(),
|
||||
if (excludeEventId != null) 'excludeEventId': excludeEventId,
|
||||
});
|
||||
return result;
|
||||
} catch (e) {
|
||||
throw Exception('Erreur lors de la vérification de disponibilité du container: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Récupère tous les IDs d'équipements et conteneurs en conflit pour une période
|
||||
/// Optimisé : une seule requête au lieu d'une par équipement
|
||||
Future<Map<String, dynamic>> getConflictingEquipmentIds({
|
||||
|
||||
@@ -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;
|
||||
final isAvailable = result['isAvailable'] as bool? ?? true;
|
||||
if (!isAvailable) {
|
||||
final conflictType = result['conflictType'] as String?;
|
||||
|
||||
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;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import 'package:flutter/material.dart';
|
||||
import 'package:em2rp/models/equipment_model.dart';
|
||||
import 'package:em2rp/models/container_model.dart';
|
||||
import 'package:em2rp/models/event_model.dart';
|
||||
import 'package:em2rp/services/event_availability_service.dart';
|
||||
import 'package:em2rp/services/data_service.dart';
|
||||
import 'package:em2rp/services/api_service.dart';
|
||||
import 'package:em2rp/utils/colors.dart';
|
||||
@@ -88,7 +87,6 @@ class EquipmentSelectionDialog extends StatefulWidget {
|
||||
class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
final ScrollController _scrollController = ScrollController(); // Pr├®serve la position de scroll
|
||||
final EventAvailabilityService _availabilityService = EventAvailabilityService();
|
||||
final DataService _dataService = DataService(apiService);
|
||||
|
||||
EquipmentCategory? _selectedCategory;
|
||||
@@ -279,7 +277,7 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
||||
DebugLog.info('[EquipmentSelectionDialog] Loaded ${newEquipments.length} equipments, total: ${_paginatedEquipments.length}, hasMore: $_hasMoreEquipments');
|
||||
|
||||
// Charger les quantites pour les consommables/cbles de cette page
|
||||
await _loadAvailableQuantities(newEquipments);
|
||||
_loadAvailableQuantities(newEquipments);
|
||||
|
||||
// Vrifier si on doit charger d'autres lments (ex: tout a t filtr)
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
@@ -442,7 +440,7 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
||||
}
|
||||
|
||||
/// Charge les quantit├®s disponibles pour les consommables/c├óbles d'une liste d'├®quipements
|
||||
Future<void> _loadAvailableQuantities(List<EquipmentModel> equipments) async {
|
||||
void _loadAvailableQuantities(List<EquipmentModel> equipments) {
|
||||
if (!mounted) return;
|
||||
|
||||
try {
|
||||
@@ -453,12 +451,13 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
||||
for (var eq in consumables) {
|
||||
// Ne recharger que si on n'a pas d├®j├á la quantit├®
|
||||
if (!_availableQuantities.containsKey(eq.id)) {
|
||||
final available = await _availabilityService.getAvailableQuantity(
|
||||
equipment: eq,
|
||||
startDate: widget.startDate,
|
||||
endDate: widget.endDate,
|
||||
excludeEventId: widget.excludeEventId,
|
||||
);
|
||||
int available = eq.totalQuantity ?? 0;
|
||||
if (_equipmentQuantities.containsKey(eq.id)) {
|
||||
final qtyInfo = _equipmentQuantities[eq.id];
|
||||
if (qtyInfo != null && qtyInfo['availableQuantity'] != null) {
|
||||
available = (qtyInfo['availableQuantity'] as num).toInt();
|
||||
}
|
||||
}
|
||||
_availableQuantities[eq.id] = available;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user