Fix : écran d'ajout de materiel, correction des conflits pour les cables et consommables
This commit is contained in:
@@ -2021,10 +2021,23 @@ exports.getConflictingEquipmentIds = onRequest(httpOptions, withCors(async (req,
|
||||
|
||||
logger.info(`Found ${eventsSnapshot.docs.length} events to check`);
|
||||
|
||||
// Récupérer tous les équipements pour savoir lesquels sont quantifiables
|
||||
const equipmentsSnapshot = await db.collection('equipments').get();
|
||||
const equipmentsInfo = {};
|
||||
equipmentsSnapshot.docs.forEach(doc => {
|
||||
const data = doc.data();
|
||||
equipmentsInfo[doc.id] = {
|
||||
category: data.category,
|
||||
totalQuantity: data.totalQuantity || 0,
|
||||
hasQuantity: data.category === 'CABLE' || data.category === 'CONSUMABLE'
|
||||
};
|
||||
});
|
||||
|
||||
// Maps pour stocker les conflits
|
||||
const conflictingEquipmentIds = new Set();
|
||||
const conflictingContainerIds = new Set();
|
||||
const conflictDetails = {}; // { equipmentId/containerId: [{ eventId, eventName, startDate, endDate }] }
|
||||
const conflictDetails = {}; // { equipmentId/containerId: [{ eventId, eventName, startDate, endDate, quantity }] }
|
||||
const equipmentQuantities = {}; // { equipmentId: { totalQuantity, reservedQuantity, availableQuantity, reservations: [...] } }
|
||||
|
||||
for (const eventDoc of eventsSnapshot.docs) {
|
||||
// Exclure l'événement en cours d'édition
|
||||
@@ -2078,12 +2091,46 @@ exports.getConflictingEquipmentIds = onRequest(httpOptions, withCors(async (req,
|
||||
// Ajouter les équipements directement assignés
|
||||
for (const eq of assignedEquipment) {
|
||||
const equipmentId = eq.equipmentId;
|
||||
conflictingEquipmentIds.add(equipmentId);
|
||||
const quantity = eq.quantity || 1;
|
||||
const equipInfo = equipmentsInfo[equipmentId];
|
||||
|
||||
// Pour les équipements quantifiables, on ne les marque pas forcément comme "en conflit"
|
||||
// On calcule juste les quantités réservées
|
||||
if (equipInfo && equipInfo.hasQuantity) {
|
||||
// Initialiser les infos de quantité si nécessaire
|
||||
if (!equipmentQuantities[equipmentId]) {
|
||||
equipmentQuantities[equipmentId] = {
|
||||
totalQuantity: equipInfo.totalQuantity,
|
||||
reservedQuantity: 0,
|
||||
availableQuantity: equipInfo.totalQuantity,
|
||||
reservations: []
|
||||
};
|
||||
}
|
||||
|
||||
// Ajouter la réservation
|
||||
equipmentQuantities[equipmentId].reservedQuantity += quantity;
|
||||
equipmentQuantities[equipmentId].availableQuantity = equipInfo.totalQuantity - equipmentQuantities[equipmentId].reservedQuantity;
|
||||
equipmentQuantities[equipmentId].reservations.push({
|
||||
...conflictInfo,
|
||||
quantity: quantity
|
||||
});
|
||||
|
||||
// Ne marquer comme "en conflit" que si la quantité totale est épuisée
|
||||
if (equipmentQuantities[equipmentId].availableQuantity <= 0) {
|
||||
conflictingEquipmentIds.add(equipmentId);
|
||||
}
|
||||
} else {
|
||||
// Pour les équipements non quantifiables, comportement classique
|
||||
conflictingEquipmentIds.add(equipmentId);
|
||||
}
|
||||
|
||||
if (!conflictDetails[equipmentId]) {
|
||||
conflictDetails[equipmentId] = [];
|
||||
}
|
||||
conflictDetails[equipmentId].push(conflictInfo);
|
||||
conflictDetails[equipmentId].push({
|
||||
...conflictInfo,
|
||||
quantity: quantity
|
||||
});
|
||||
}
|
||||
|
||||
// Ajouter les conteneurs assignés
|
||||
@@ -2125,6 +2172,7 @@ exports.getConflictingEquipmentIds = onRequest(httpOptions, withCors(async (req,
|
||||
conflictingEquipmentIds: Array.from(conflictingEquipmentIds),
|
||||
conflictingContainerIds: Array.from(conflictingContainerIds),
|
||||
conflictDetails: conflictDetails,
|
||||
equipmentQuantities: equipmentQuantities, // NOUVEAU : Informations sur les quantités
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error("Error getting conflicting equipment IDs:", error);
|
||||
|
||||
@@ -6,6 +6,8 @@ import 'package:em2rp/models/event_model.dart';
|
||||
import 'package:em2rp/providers/equipment_provider.dart';
|
||||
import 'package:em2rp/providers/container_provider.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';
|
||||
|
||||
/// Type de sélection dans le dialog
|
||||
@@ -87,16 +89,23 @@ class EquipmentSelectionDialog extends StatefulWidget {
|
||||
class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
final EventAvailabilityService _availabilityService = EventAvailabilityService();
|
||||
final DataService _dataService = DataService(apiService);
|
||||
|
||||
EquipmentCategory? _selectedCategory;
|
||||
|
||||
Map<String, SelectedItem> _selectedItems = {};
|
||||
Map<String, int> _availableQuantities = {}; // Pour consommables
|
||||
Map<String, List<ContainerModel>> _recommendedContainers = {}; // Recommandations
|
||||
Map<String, List<AvailabilityConflict>> _equipmentConflicts = {}; // Conflits de disponibilité
|
||||
Map<String, List<AvailabilityConflict>> _equipmentConflicts = {}; // Conflits de disponibilité (détaillés)
|
||||
Map<String, ContainerConflictInfo> _containerConflicts = {}; // Conflits des conteneurs
|
||||
Set<String> _expandedContainers = {}; // Conteneurs dépliés dans la liste
|
||||
|
||||
// NOUVEAU : IDs en conflit récupérés en batch
|
||||
Set<String> _conflictingEquipmentIds = {};
|
||||
Set<String> _conflictingContainerIds = {};
|
||||
Map<String, dynamic> _conflictDetails = {}; // Détails des conflits par ID
|
||||
Map<String, dynamic> _equipmentQuantities = {}; // Infos de quantités pour câbles/consommables
|
||||
|
||||
bool _isLoadingQuantities = false;
|
||||
bool _isLoadingConflicts = false;
|
||||
String _searchQuery = '';
|
||||
@@ -111,7 +120,6 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
||||
_initializeAlreadyAssigned();
|
||||
_loadAvailableQuantities();
|
||||
_loadEquipmentConflicts();
|
||||
_loadContainerConflicts();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -224,59 +232,57 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Charge les conflits de disponibilité pour tous les équipements
|
||||
/// Charge les conflits de disponibilité pour tous les équipements et conteneurs
|
||||
/// Version optimisée : un seul appel API au lieu d'un par équipement
|
||||
Future<void> _loadEquipmentConflicts() async {
|
||||
setState(() => _isLoadingConflicts = true);
|
||||
|
||||
try {
|
||||
final equipmentProvider = context.read<EquipmentProvider>();
|
||||
final equipment = await equipmentProvider.equipmentStream.first;
|
||||
print('[EquipmentSelectionDialog] Loading conflicts (optimized batch method)...');
|
||||
|
||||
for (var eq in equipment) {
|
||||
// Pour les consommables/câbles, vérifier avec gestion de quantité
|
||||
if (eq.hasQuantity) {
|
||||
// Récupérer la quantité disponible
|
||||
final availableQty = await _availabilityService.getAvailableQuantity(
|
||||
equipment: eq,
|
||||
startDate: widget.startDate,
|
||||
endDate: widget.endDate,
|
||||
excludeEventId: widget.excludeEventId,
|
||||
);
|
||||
final startTime = DateTime.now();
|
||||
|
||||
// Vérifier si un item de cet équipement est déjà sélectionné
|
||||
final selectedItem = _selectedItems[eq.id];
|
||||
final requestedQty = selectedItem?.quantity ?? 1;
|
||||
// UN SEUL appel API pour récupérer TOUS les équipements en conflit
|
||||
final result = await _dataService.getConflictingEquipmentIds(
|
||||
startDate: widget.startDate,
|
||||
endDate: widget.endDate,
|
||||
excludeEventId: widget.excludeEventId,
|
||||
installationTime: 0, // TODO: Récupérer depuis l'événement si nécessaire
|
||||
disassemblyTime: 0,
|
||||
);
|
||||
|
||||
// ✅ Ne créer un conflit QUE si la quantité demandée dépasse la quantité disponible
|
||||
if (requestedQty > availableQty) {
|
||||
final conflicts = await _availabilityService.checkEquipmentAvailabilityWithQuantity(
|
||||
equipment: eq,
|
||||
requestedQuantity: requestedQty,
|
||||
startDate: widget.startDate,
|
||||
endDate: widget.endDate,
|
||||
excludeEventId: widget.excludeEventId,
|
||||
);
|
||||
final endTime = DateTime.now();
|
||||
final duration = endTime.difference(startTime);
|
||||
|
||||
if (conflicts.isNotEmpty) {
|
||||
_equipmentConflicts[eq.id] = conflicts;
|
||||
}
|
||||
}
|
||||
// Sinon, pas de conflit à afficher dans la liste
|
||||
} else {
|
||||
// Pour les équipements non quantifiables, vérification classique
|
||||
final conflicts = await _availabilityService.checkEquipmentAvailability(
|
||||
equipmentId: eq.id,
|
||||
equipmentName: eq.id,
|
||||
startDate: widget.startDate,
|
||||
endDate: widget.endDate,
|
||||
excludeEventId: widget.excludeEventId,
|
||||
);
|
||||
print('[EquipmentSelectionDialog] Conflicts loaded in ${duration.inMilliseconds}ms');
|
||||
|
||||
if (conflicts.isNotEmpty) {
|
||||
_equipmentConflicts[eq.id] = conflicts;
|
||||
}
|
||||
}
|
||||
// Extraire les IDs en conflit
|
||||
final conflictingEquipmentIds = (result['conflictingEquipmentIds'] as List<dynamic>?)
|
||||
?.map((e) => e.toString())
|
||||
.toSet() ?? {};
|
||||
|
||||
final conflictingContainerIds = (result['conflictingContainerIds'] as List<dynamic>?)
|
||||
?.map((e) => e.toString())
|
||||
.toSet() ?? {};
|
||||
|
||||
final conflictDetails = result['conflictDetails'] as Map<String, dynamic>? ?? {};
|
||||
final equipmentQuantities = result['equipmentQuantities'] as Map<String, dynamic>? ?? {};
|
||||
|
||||
print('[EquipmentSelectionDialog] Found ${conflictingEquipmentIds.length} equipment(s) and ${conflictingContainerIds.length} container(s) in conflict');
|
||||
print('[EquipmentSelectionDialog] Quantity info for ${equipmentQuantities.length} equipment(s)');
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_conflictingEquipmentIds = conflictingEquipmentIds;
|
||||
_conflictingContainerIds = conflictingContainerIds;
|
||||
_conflictDetails = conflictDetails;
|
||||
_equipmentQuantities = equipmentQuantities;
|
||||
});
|
||||
}
|
||||
|
||||
// Mettre à jour les statuts de conteneurs
|
||||
await _updateContainerConflictStatus();
|
||||
|
||||
} catch (e) {
|
||||
print('[EquipmentSelectionDialog] Error loading conflicts: $e');
|
||||
} finally {
|
||||
@@ -284,90 +290,277 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Charge les conflits de disponibilité pour tous les conteneurs
|
||||
Future<void> _loadContainerConflicts() async {
|
||||
/// Met à jour le statut de conflit des conteneurs basé sur les IDs en conflit
|
||||
Future<void> _updateContainerConflictStatus() async {
|
||||
try {
|
||||
print('[EquipmentSelectionDialog] Loading container conflicts...');
|
||||
final containerProvider = context.read<ContainerProvider>();
|
||||
final equipmentProvider = context.read<EquipmentProvider>();
|
||||
final containers = await containerProvider.containersStream.first;
|
||||
final allEquipment = await equipmentProvider.equipmentStream.first;
|
||||
|
||||
print('[EquipmentSelectionDialog] Checking conflicts for ${containers.length} containers');
|
||||
|
||||
for (var container in containers) {
|
||||
// Vérifier d'abord si la boîte complète est utilisée ailleurs
|
||||
final containerEquipment = allEquipment
|
||||
.where((eq) => container.equipmentIds.contains(eq.id))
|
||||
// Vérifier si le conteneur lui-même est en conflit
|
||||
if (_conflictingContainerIds.contains(container.id)) {
|
||||
_containerConflicts[container.id] = ContainerConflictInfo(
|
||||
status: ContainerConflictStatus.complete,
|
||||
conflictingEquipmentIds: [],
|
||||
totalChildren: container.equipmentIds.length,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Vérifier si des équipements enfants sont en conflit
|
||||
final conflictingChildren = container.equipmentIds
|
||||
.where((eqId) => _conflictingEquipmentIds.contains(eqId))
|
||||
.toList();
|
||||
|
||||
final containerConflicts = await _availabilityService.checkContainerAvailability(
|
||||
container: container,
|
||||
containerEquipment: containerEquipment,
|
||||
startDate: widget.startDate,
|
||||
endDate: widget.endDate,
|
||||
excludeEventId: widget.excludeEventId,
|
||||
);
|
||||
if (conflictingChildren.isNotEmpty) {
|
||||
final status = conflictingChildren.length == container.equipmentIds.length
|
||||
? ContainerConflictStatus.complete
|
||||
: ContainerConflictStatus.partial;
|
||||
|
||||
if (containerConflicts.isNotEmpty) {
|
||||
// Déterminer le statut en fonction du type de conflit
|
||||
final hasFullConflict = containerConflicts.any(
|
||||
(c) => c.type == ConflictType.containerFullyUsed,
|
||||
_containerConflicts[container.id] = ContainerConflictInfo(
|
||||
status: status,
|
||||
conflictingEquipmentIds: conflictingChildren,
|
||||
totalChildren: container.equipmentIds.length,
|
||||
);
|
||||
|
||||
final conflictingChildren = containerConflicts
|
||||
.where((c) => c.type != ConflictType.containerFullyUsed &&
|
||||
c.type != ConflictType.containerPartiallyUsed)
|
||||
.map((c) => c.equipmentId)
|
||||
.toList();
|
||||
|
||||
final status = hasFullConflict
|
||||
? ContainerConflictStatus.complete
|
||||
: (conflictingChildren.isNotEmpty
|
||||
? ContainerConflictStatus.partial
|
||||
: ContainerConflictStatus.none);
|
||||
|
||||
if (status != ContainerConflictStatus.none) {
|
||||
_containerConflicts[container.id] = ContainerConflictInfo(
|
||||
status: status,
|
||||
conflictingEquipmentIds: conflictingChildren,
|
||||
totalChildren: container.equipmentIds.length,
|
||||
);
|
||||
|
||||
print('[EquipmentSelectionDialog] Container ${container.id}: ${status.name} conflict');
|
||||
}
|
||||
} else {
|
||||
// Vérifier chaque équipement enfant individuellement
|
||||
final conflictingChildren = <String>[];
|
||||
|
||||
for (var equipmentId in container.equipmentIds) {
|
||||
if (_equipmentConflicts.containsKey(equipmentId)) {
|
||||
conflictingChildren.add(equipmentId);
|
||||
}
|
||||
}
|
||||
|
||||
if (conflictingChildren.isNotEmpty) {
|
||||
final status = conflictingChildren.length == container.equipmentIds.length
|
||||
? ContainerConflictStatus.complete
|
||||
: ContainerConflictStatus.partial;
|
||||
|
||||
_containerConflicts[container.id] = ContainerConflictInfo(
|
||||
status: status,
|
||||
conflictingEquipmentIds: conflictingChildren,
|
||||
totalChildren: container.equipmentIds.length,
|
||||
);
|
||||
|
||||
print('[EquipmentSelectionDialog] Container ${container.id}: ${status.name} conflict (${conflictingChildren.length}/${container.equipmentIds.length} children)');
|
||||
}
|
||||
print('[EquipmentSelectionDialog] Container ${container.id}: ${status.name} conflict (${conflictingChildren.length}/${container.equipmentIds.length} children)');
|
||||
}
|
||||
}
|
||||
|
||||
print('[EquipmentSelectionDialog] Total containers with conflicts: ${_containerConflicts.length}');
|
||||
} catch (e) {
|
||||
print('[EquipmentSelectionDialog] Error loading container conflicts: $e');
|
||||
print('[EquipmentSelectionDialog] Error updating container conflicts: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Récupère les détails des conflits pour un équipement/conteneur donné
|
||||
List<Map<String, dynamic>> _getConflictDetailsFor(String id) {
|
||||
final details = _conflictDetails[id];
|
||||
if (details == null) return [];
|
||||
|
||||
if (details is List) {
|
||||
return details.cast<Map<String, dynamic>>();
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/// Construit l'affichage des quantités pour les câbles/consommables
|
||||
Widget _buildQuantityInfo(EquipmentModel equipment) {
|
||||
final quantityInfo = _equipmentQuantities[equipment.id] as Map<String, dynamic>?;
|
||||
|
||||
if (quantityInfo == null) {
|
||||
// Pas d'info de quantité, utiliser l'ancien système (availableQuantities)
|
||||
final availableQty = _availableQuantities[equipment.id];
|
||||
if (availableQty == null) return const SizedBox.shrink();
|
||||
|
||||
return Text(
|
||||
'Disponible : $availableQty ${equipment.category == EquipmentCategory.cable ? "m" : ""}',
|
||||
style: TextStyle(
|
||||
color: availableQty > 0 ? Colors.green : Colors.red,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 13,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final totalQuantity = quantityInfo['totalQuantity'] as int? ?? 0;
|
||||
final availableQuantity = quantityInfo['availableQuantity'] as int? ?? 0;
|
||||
final reservedQuantity = quantityInfo['reservedQuantity'] as int? ?? 0;
|
||||
final reservations = quantityInfo['reservations'] as List<dynamic>? ?? [];
|
||||
final unit = equipment.category == EquipmentCategory.cable ? "m" : "";
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
Text(
|
||||
'Disponible : $availableQuantity/$totalQuantity $unit',
|
||||
style: TextStyle(
|
||||
color: availableQuantity > 0 ? Colors.green.shade700 : Colors.red,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
if (reservations.isNotEmpty) ...[
|
||||
const SizedBox(width: 6),
|
||||
GestureDetector(
|
||||
onTap: () => _showQuantityDetailsDialog(equipment, quantityInfo),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.shade50,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.info_outline,
|
||||
size: 16,
|
||||
color: Colors.blue.shade700,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Affiche un dialog avec les détails des réservations de quantité
|
||||
Future<void> _showQuantityDetailsDialog(EquipmentModel equipment, Map<String, dynamic> quantityInfo) async {
|
||||
final reservations = quantityInfo['reservations'] as List<dynamic>? ?? [];
|
||||
final totalQuantity = quantityInfo['totalQuantity'] as int? ?? 0;
|
||||
final availableQuantity = quantityInfo['availableQuantity'] as int? ?? 0;
|
||||
final unit = equipment.category == EquipmentCategory.cable ? "m" : "";
|
||||
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(Icons.inventory_2, color: Colors.blue.shade700),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Quantités - ${equipment.name}',
|
||||
style: const TextStyle(fontSize: 18),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
content: SizedBox(
|
||||
width: 500,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Résumé
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.shade50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Quantité totale :',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey.shade800,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'$totalQuantity $unit',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.blue.shade900,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Disponible :',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey.shade800,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'$availableQuantity $unit',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: availableQuantity > 0 ? Colors.green.shade700 : Colors.red,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Liste des réservations
|
||||
if (reservations.isNotEmpty) ...[
|
||||
Text(
|
||||
'Utilisé sur ${reservations.length} événement(s) :',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
constraints: const BoxConstraints(maxHeight: 300),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: reservations.map((reservation) {
|
||||
final res = reservation as Map<String, dynamic>;
|
||||
final eventName = res['eventName'] as String? ?? 'Événement inconnu';
|
||||
final quantity = res['quantity'] as int? ?? 0;
|
||||
final viaContainer = res['viaContainer'] as String?;
|
||||
final viaContainerName = res['viaContainerName'] as String?;
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: Colors.orange.shade100,
|
||||
child: Text(
|
||||
'$quantity',
|
||||
style: TextStyle(
|
||||
color: Colors.orange.shade900,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
eventName,
|
||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||
),
|
||||
subtitle: viaContainer != null
|
||||
? Text(
|
||||
'Via ${viaContainerName ?? viaContainer}',
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
trailing: Text(
|
||||
'$quantity $unit',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Fermer'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Recherche les conteneurs recommandés pour un équipement
|
||||
Future<void> _findRecommendedContainers(String equipmentId) async {
|
||||
try {
|
||||
@@ -398,7 +591,7 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
||||
|
||||
void _toggleSelection(String id, String name, SelectionType type, {int? maxQuantity, bool force = false}) async {
|
||||
// Vérifier si l'équipement est en conflit
|
||||
if (!force && type == SelectionType.equipment && _equipmentConflicts.containsKey(id)) {
|
||||
if (!force && type == SelectionType.equipment && _conflictingEquipmentIds.contains(id)) {
|
||||
// Demander confirmation pour forcer
|
||||
final shouldForce = await _showForceConfirmationDialog(id);
|
||||
if (shouldForce == true) {
|
||||
@@ -822,6 +1015,14 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
||||
final allContainers = containerSnapshot.data ?? [];
|
||||
final allEquipment = equipmentSnapshot.data ?? [];
|
||||
|
||||
// Charger les conflits une seule fois après le chargement des données
|
||||
if (!_isLoadingConflicts && _conflictingEquipmentIds.isEmpty && allEquipment.isNotEmpty) {
|
||||
// Lancer le chargement des conflits en arrière-plan
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_loadEquipmentConflicts();
|
||||
});
|
||||
}
|
||||
|
||||
// Filtrage des boîtes
|
||||
final filteredContainers = allContainers.where((container) {
|
||||
if (_searchQuery.isNotEmpty) {
|
||||
@@ -948,8 +1149,8 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
||||
equipment.category == EquipmentCategory.cable;
|
||||
final availableQty = _availableQuantities[equipment.id];
|
||||
final selectedItem = _selectedItems[equipment.id];
|
||||
final hasConflict = _equipmentConflicts.containsKey(equipment.id);
|
||||
final conflicts = _equipmentConflicts[equipment.id] ?? [];
|
||||
final hasConflict = _conflictingEquipmentIds.contains(equipment.id); // CORRECTION ICI !
|
||||
final conflictDetails = _getConflictDetailsFor(equipment.id);
|
||||
|
||||
// Bloquer la sélection si en conflit et non forcé
|
||||
final canSelect = !hasConflict || isSelected;
|
||||
@@ -1094,17 +1295,10 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
if (isConsumable && availableQty != null)
|
||||
if (isConsumable)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
child: Text(
|
||||
'Disponible : $availableQty ${equipment.category == EquipmentCategory.cable ? "m" : ""}',
|
||||
style: TextStyle(
|
||||
color: availableQty > 0 ? Colors.green : Colors.red,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
child: _buildQuantityInfo(equipment),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -1134,7 +1328,7 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
||||
Icon(Icons.info_outline, size: 16, color: Colors.orange.shade900),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'Utilisé sur ${conflicts.length} événement(s) :',
|
||||
'Utilisé sur ${conflictDetails.length} événement(s) :',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
@@ -1144,21 +1338,44 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
...conflicts.take(2).map((conflict) => Padding(
|
||||
padding: const EdgeInsets.only(left: 22, top: 4),
|
||||
child: Text(
|
||||
'• ${conflict.conflictingEvent.name} (${conflict.overlapDays} jour(s))',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.orange.shade800,
|
||||
...conflictDetails.take(2).map((detail) {
|
||||
final eventName = detail['eventName'] as String? ?? 'Événement inconnu';
|
||||
final viaContainer = detail['viaContainer'] as String?;
|
||||
final viaContainerName = detail['viaContainerName'] as String?;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 22, top: 4),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'• $eventName',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.orange.shade800,
|
||||
),
|
||||
),
|
||||
if (viaContainer != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8),
|
||||
child: Text(
|
||||
'via ${viaContainerName ?? viaContainer}',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.grey.shade600,
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)),
|
||||
if (conflicts.length > 2)
|
||||
);
|
||||
}),
|
||||
if (conflictDetails.length > 2)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 22, top: 4),
|
||||
child: Text(
|
||||
'... et ${conflicts.length - 2} autre(s)',
|
||||
'... et ${conflictDetails.length - 2} autre(s)',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.orange.shade800,
|
||||
|
||||
Reference in New Issue
Block a user