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`);
|
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
|
// Maps pour stocker les conflits
|
||||||
const conflictingEquipmentIds = new Set();
|
const conflictingEquipmentIds = new Set();
|
||||||
const conflictingContainerIds = 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) {
|
for (const eventDoc of eventsSnapshot.docs) {
|
||||||
// Exclure l'événement en cours d'édition
|
// 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
|
// Ajouter les équipements directement assignés
|
||||||
for (const eq of assignedEquipment) {
|
for (const eq of assignedEquipment) {
|
||||||
const equipmentId = eq.equipmentId;
|
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]) {
|
if (!conflictDetails[equipmentId]) {
|
||||||
conflictDetails[equipmentId] = [];
|
conflictDetails[equipmentId] = [];
|
||||||
}
|
}
|
||||||
conflictDetails[equipmentId].push(conflictInfo);
|
conflictDetails[equipmentId].push({
|
||||||
|
...conflictInfo,
|
||||||
|
quantity: quantity
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ajouter les conteneurs assignés
|
// Ajouter les conteneurs assignés
|
||||||
@@ -2125,6 +2172,7 @@ exports.getConflictingEquipmentIds = onRequest(httpOptions, withCors(async (req,
|
|||||||
conflictingEquipmentIds: Array.from(conflictingEquipmentIds),
|
conflictingEquipmentIds: Array.from(conflictingEquipmentIds),
|
||||||
conflictingContainerIds: Array.from(conflictingContainerIds),
|
conflictingContainerIds: Array.from(conflictingContainerIds),
|
||||||
conflictDetails: conflictDetails,
|
conflictDetails: conflictDetails,
|
||||||
|
equipmentQuantities: equipmentQuantities, // NOUVEAU : Informations sur les quantités
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error getting conflicting equipment IDs:", 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/equipment_provider.dart';
|
||||||
import 'package:em2rp/providers/container_provider.dart';
|
import 'package:em2rp/providers/container_provider.dart';
|
||||||
import 'package:em2rp/services/event_availability_service.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';
|
import 'package:em2rp/utils/colors.dart';
|
||||||
|
|
||||||
/// Type de sélection dans le dialog
|
/// Type de sélection dans le dialog
|
||||||
@@ -87,16 +89,23 @@ class EquipmentSelectionDialog extends StatefulWidget {
|
|||||||
class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
||||||
final TextEditingController _searchController = TextEditingController();
|
final TextEditingController _searchController = TextEditingController();
|
||||||
final EventAvailabilityService _availabilityService = EventAvailabilityService();
|
final EventAvailabilityService _availabilityService = EventAvailabilityService();
|
||||||
|
final DataService _dataService = DataService(apiService);
|
||||||
|
|
||||||
EquipmentCategory? _selectedCategory;
|
EquipmentCategory? _selectedCategory;
|
||||||
|
|
||||||
Map<String, SelectedItem> _selectedItems = {};
|
Map<String, SelectedItem> _selectedItems = {};
|
||||||
Map<String, int> _availableQuantities = {}; // Pour consommables
|
Map<String, int> _availableQuantities = {}; // Pour consommables
|
||||||
Map<String, List<ContainerModel>> _recommendedContainers = {}; // Recommandations
|
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
|
Map<String, ContainerConflictInfo> _containerConflicts = {}; // Conflits des conteneurs
|
||||||
Set<String> _expandedContainers = {}; // Conteneurs dépliés dans la liste
|
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 _isLoadingQuantities = false;
|
||||||
bool _isLoadingConflicts = false;
|
bool _isLoadingConflicts = false;
|
||||||
String _searchQuery = '';
|
String _searchQuery = '';
|
||||||
@@ -111,7 +120,6 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
|||||||
_initializeAlreadyAssigned();
|
_initializeAlreadyAssigned();
|
||||||
_loadAvailableQuantities();
|
_loadAvailableQuantities();
|
||||||
_loadEquipmentConflicts();
|
_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 {
|
Future<void> _loadEquipmentConflicts() async {
|
||||||
setState(() => _isLoadingConflicts = true);
|
setState(() => _isLoadingConflicts = true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final equipmentProvider = context.read<EquipmentProvider>();
|
print('[EquipmentSelectionDialog] Loading conflicts (optimized batch method)...');
|
||||||
final equipment = await equipmentProvider.equipmentStream.first;
|
|
||||||
|
|
||||||
for (var eq in equipment) {
|
final startTime = DateTime.now();
|
||||||
// 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,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Vérifier si un item de cet équipement est déjà sélectionné
|
// UN SEUL appel API pour récupérer TOUS les équipements en conflit
|
||||||
final selectedItem = _selectedItems[eq.id];
|
final result = await _dataService.getConflictingEquipmentIds(
|
||||||
final requestedQty = selectedItem?.quantity ?? 1;
|
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
|
final endTime = DateTime.now();
|
||||||
if (requestedQty > availableQty) {
|
final duration = endTime.difference(startTime);
|
||||||
final conflicts = await _availabilityService.checkEquipmentAvailabilityWithQuantity(
|
|
||||||
equipment: eq,
|
|
||||||
requestedQuantity: requestedQty,
|
|
||||||
startDate: widget.startDate,
|
|
||||||
endDate: widget.endDate,
|
|
||||||
excludeEventId: widget.excludeEventId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (conflicts.isNotEmpty) {
|
print('[EquipmentSelectionDialog] Conflicts loaded in ${duration.inMilliseconds}ms');
|
||||||
_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,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (conflicts.isNotEmpty) {
|
// Extraire les IDs en conflit
|
||||||
_equipmentConflicts[eq.id] = conflicts;
|
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) {
|
} catch (e) {
|
||||||
print('[EquipmentSelectionDialog] Error loading conflicts: $e');
|
print('[EquipmentSelectionDialog] Error loading conflicts: $e');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -284,90 +290,277 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Charge les conflits de disponibilité pour tous les conteneurs
|
/// Met à jour le statut de conflit des conteneurs basé sur les IDs en conflit
|
||||||
Future<void> _loadContainerConflicts() async {
|
Future<void> _updateContainerConflictStatus() async {
|
||||||
try {
|
try {
|
||||||
print('[EquipmentSelectionDialog] Loading container conflicts...');
|
|
||||||
final containerProvider = context.read<ContainerProvider>();
|
final containerProvider = context.read<ContainerProvider>();
|
||||||
final equipmentProvider = context.read<EquipmentProvider>();
|
|
||||||
final containers = await containerProvider.containersStream.first;
|
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) {
|
for (var container in containers) {
|
||||||
// Vérifier d'abord si la boîte complète est utilisée ailleurs
|
// Vérifier si le conteneur lui-même est en conflit
|
||||||
final containerEquipment = allEquipment
|
if (_conflictingContainerIds.contains(container.id)) {
|
||||||
.where((eq) => container.equipmentIds.contains(eq.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();
|
.toList();
|
||||||
|
|
||||||
final containerConflicts = await _availabilityService.checkContainerAvailability(
|
if (conflictingChildren.isNotEmpty) {
|
||||||
container: container,
|
final status = conflictingChildren.length == container.equipmentIds.length
|
||||||
containerEquipment: containerEquipment,
|
? ContainerConflictStatus.complete
|
||||||
startDate: widget.startDate,
|
: ContainerConflictStatus.partial;
|
||||||
endDate: widget.endDate,
|
|
||||||
excludeEventId: widget.excludeEventId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (containerConflicts.isNotEmpty) {
|
_containerConflicts[container.id] = ContainerConflictInfo(
|
||||||
// Déterminer le statut en fonction du type de conflit
|
status: status,
|
||||||
final hasFullConflict = containerConflicts.any(
|
conflictingEquipmentIds: conflictingChildren,
|
||||||
(c) => c.type == ConflictType.containerFullyUsed,
|
totalChildren: container.equipmentIds.length,
|
||||||
);
|
);
|
||||||
|
|
||||||
final conflictingChildren = containerConflicts
|
print('[EquipmentSelectionDialog] Container ${container.id}: ${status.name} conflict (${conflictingChildren.length}/${container.equipmentIds.length} children)');
|
||||||
.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] Total containers with conflicts: ${_containerConflicts.length}');
|
print('[EquipmentSelectionDialog] Total containers with conflicts: ${_containerConflicts.length}');
|
||||||
} catch (e) {
|
} 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
|
/// Recherche les conteneurs recommandés pour un équipement
|
||||||
Future<void> _findRecommendedContainers(String equipmentId) async {
|
Future<void> _findRecommendedContainers(String equipmentId) async {
|
||||||
try {
|
try {
|
||||||
@@ -398,7 +591,7 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
|||||||
|
|
||||||
void _toggleSelection(String id, String name, SelectionType type, {int? maxQuantity, bool force = false}) async {
|
void _toggleSelection(String id, String name, SelectionType type, {int? maxQuantity, bool force = false}) async {
|
||||||
// Vérifier si l'équipement est en conflit
|
// 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
|
// Demander confirmation pour forcer
|
||||||
final shouldForce = await _showForceConfirmationDialog(id);
|
final shouldForce = await _showForceConfirmationDialog(id);
|
||||||
if (shouldForce == true) {
|
if (shouldForce == true) {
|
||||||
@@ -822,6 +1015,14 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
|||||||
final allContainers = containerSnapshot.data ?? [];
|
final allContainers = containerSnapshot.data ?? [];
|
||||||
final allEquipment = equipmentSnapshot.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
|
// Filtrage des boîtes
|
||||||
final filteredContainers = allContainers.where((container) {
|
final filteredContainers = allContainers.where((container) {
|
||||||
if (_searchQuery.isNotEmpty) {
|
if (_searchQuery.isNotEmpty) {
|
||||||
@@ -948,8 +1149,8 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
|||||||
equipment.category == EquipmentCategory.cable;
|
equipment.category == EquipmentCategory.cable;
|
||||||
final availableQty = _availableQuantities[equipment.id];
|
final availableQty = _availableQuantities[equipment.id];
|
||||||
final selectedItem = _selectedItems[equipment.id];
|
final selectedItem = _selectedItems[equipment.id];
|
||||||
final hasConflict = _equipmentConflicts.containsKey(equipment.id);
|
final hasConflict = _conflictingEquipmentIds.contains(equipment.id); // CORRECTION ICI !
|
||||||
final conflicts = _equipmentConflicts[equipment.id] ?? [];
|
final conflictDetails = _getConflictDetailsFor(equipment.id);
|
||||||
|
|
||||||
// Bloquer la sélection si en conflit et non forcé
|
// Bloquer la sélection si en conflit et non forcé
|
||||||
final canSelect = !hasConflict || isSelected;
|
final canSelect = !hasConflict || isSelected;
|
||||||
@@ -1094,17 +1295,10 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
|||||||
}).toList(),
|
}).toList(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (isConsumable && availableQty != null)
|
if (isConsumable)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 4),
|
padding: const EdgeInsets.only(top: 4),
|
||||||
child: Text(
|
child: _buildQuantityInfo(equipment),
|
||||||
'Disponible : $availableQty ${equipment.category == EquipmentCategory.cable ? "m" : ""}',
|
|
||||||
style: TextStyle(
|
|
||||||
color: availableQty > 0 ? Colors.green : Colors.red,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
fontSize: 13,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -1134,7 +1328,7 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
|||||||
Icon(Icons.info_outline, size: 16, color: Colors.orange.shade900),
|
Icon(Icons.info_outline, size: 16, color: Colors.orange.shade900),
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
Text(
|
Text(
|
||||||
'Utilisé sur ${conflicts.length} événement(s) :',
|
'Utilisé sur ${conflictDetails.length} événement(s) :',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
@@ -1144,21 +1338,44 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
...conflicts.take(2).map((conflict) => Padding(
|
...conflictDetails.take(2).map((detail) {
|
||||||
padding: const EdgeInsets.only(left: 22, top: 4),
|
final eventName = detail['eventName'] as String? ?? 'Événement inconnu';
|
||||||
child: Text(
|
final viaContainer = detail['viaContainer'] as String?;
|
||||||
'• ${conflict.conflictingEvent.name} (${conflict.overlapDays} jour(s))',
|
final viaContainerName = detail['viaContainerName'] as String?;
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 11,
|
return Padding(
|
||||||
color: Colors.orange.shade800,
|
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(
|
||||||
padding: const EdgeInsets.only(left: 22, top: 4),
|
padding: const EdgeInsets.only(left: 22, top: 4),
|
||||||
child: Text(
|
child: Text(
|
||||||
'... et ${conflicts.length - 2} autre(s)',
|
'... et ${conflictDetails.length - 2} autre(s)',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: Colors.orange.shade800,
|
color: Colors.orange.shade800,
|
||||||
|
|||||||
Reference in New Issue
Block a user