diff --git a/em2rp/lib/providers/equipment_provider.dart b/em2rp/lib/providers/equipment_provider.dart index 90f15d9..3fe8217 100644 --- a/em2rp/lib/providers/equipment_provider.dart +++ b/em2rp/lib/providers/equipment_provider.dart @@ -23,6 +23,7 @@ class EquipmentProvider extends ChangeNotifier { // Getters List get equipment => _filteredEquipment; + List get allEquipment => _equipment; // Tous les équipements sans filtre List get models => _models; List get brands => _brands; EquipmentCategory? get selectedCategory => _selectedCategory; diff --git a/em2rp/lib/views/widgets/event/equipment_selection_dialog.dart b/em2rp/lib/views/widgets/event/equipment_selection_dialog.dart index 0d4e0f8..084752f 100644 --- a/em2rp/lib/views/widgets/event/equipment_selection_dialog.dart +++ b/em2rp/lib/views/widgets/event/equipment_selection_dialog.dart @@ -111,9 +111,13 @@ class _EquipmentSelectionDialogState extends State { bool _isLoadingQuantities = false; bool _isLoadingConflicts = false; - bool _conflictsLoaded = false; // Flag pour éviter de recharger indéfiniment String _searchQuery = ''; + // Nouvelles options d'affichage + bool _showConflictingItems = false; // Afficher les équipements/boîtes en conflit + bool _containersExpanded = true; // Section "Boîtes" dépliée + bool _equipmentExpanded = true; // Section "Tous les équipements" dépliée + // Cache pour éviter les rebuilds inutiles List _cachedContainers = []; List _cachedEquipment = []; @@ -123,31 +127,84 @@ class _EquipmentSelectionDialogState extends State { void initState() { super.initState(); - // Charger après le premier frame pour éviter setState pendant build - WidgetsBinding.instance.addPostFrameCallback((_) { - _ensureEquipmentsLoaded(); - _initializeAlreadyAssigned(); - _loadAvailableQuantities(); - _loadEquipmentConflicts(); - }); + // Charger immédiatement les données de manière asynchrone + _initializeData(); + } + + /// Initialise toutes les données nécessaires + Future _initializeData() async { + try { + // 1. S'assurer que les équipements et conteneurs sont chargés + await _ensureEquipmentsLoaded(); + + // 2. Mettre à jour le cache immédiatement après le chargement + if (mounted) { + final equipmentProvider = context.read(); + final containerProvider = context.read(); + + setState(() { + // Utiliser allEquipment pour avoir TOUS les équipements sans filtres + _cachedEquipment = equipmentProvider.allEquipment; + _cachedContainers = containerProvider.containers; + _initialDataLoaded = true; + }); + + DebugLog.info('[EquipmentSelectionDialog] Cache updated: ${_cachedEquipment.length} equipment(s), ${_cachedContainers.length} container(s)'); + } + + // 3. Initialiser la sélection avec le matériel déjà assigné + await _initializeAlreadyAssigned(); + + // 4. Charger les quantités et conflits en parallèle + await Future.wait([ + _loadAvailableQuantities(), + _loadEquipmentConflicts(), + ]); + } catch (e) { + DebugLog.error('[EquipmentSelectionDialog] Error during initialization', e); + } } /// S'assure que les équipements sont chargés avant d'utiliser le dialog Future _ensureEquipmentsLoaded() async { final equipmentProvider = context.read(); final containerProvider = context.read(); + + DebugLog.info('[EquipmentSelectionDialog] Starting equipment loading...'); + + // Forcer le chargement et attendre qu'il soit terminé await equipmentProvider.ensureLoaded(); - // Charger aussi les conteneurs si nécessaire - if (!containerProvider.isLoading && containerProvider.containers.isEmpty) { - await containerProvider.loadContainers(); + + // Attendre que le chargement soit vraiment terminé + while (equipmentProvider.isLoading) { + await Future.delayed(const Duration(milliseconds: 100)); } + + // Vérifier qu'on a bien des équipements chargés + if (equipmentProvider.allEquipment.isEmpty) { + DebugLog.warning('[EquipmentSelectionDialog] No equipment loaded after ensureLoaded!'); + } + + // Charger aussi les conteneurs si nécessaire + if (containerProvider.containers.isEmpty) { + await containerProvider.loadContainers(); + + // Attendre que le chargement des conteneurs soit terminé + while (containerProvider.isLoading) { + await Future.delayed(const Duration(milliseconds: 100)); + } + } + + DebugLog.info('[EquipmentSelectionDialog] Data loaded: ${equipmentProvider.allEquipment.length} equipment(s), ${containerProvider.containers.length} container(s)'); } /// Initialise la sélection avec le matériel déjà assigné Future _initializeAlreadyAssigned() async { + final Map initialSelection = {}; + // Ajouter les équipements déjà assignés for (var eq in widget.alreadyAssigned) { - _selectedItems[eq.equipmentId] = SelectedItem( + initialSelection[eq.equipmentId] = SelectedItem( id: eq.equipmentId, name: eq.equipmentId, type: SelectionType.equipment, @@ -159,7 +216,7 @@ class _EquipmentSelectionDialogState extends State { if (widget.alreadyAssignedContainers.isNotEmpty) { try { final containerProvider = context.read(); - final containers = await containerProvider.containersStream.first; + final containers = _cachedContainers.isNotEmpty ? _cachedContainers : containerProvider.containers; for (var containerId in widget.alreadyAssignedContainers) { final container = containers.firstWhere( @@ -175,7 +232,7 @@ class _EquipmentSelectionDialogState extends State { ), ); - _selectedItems[containerId] = SelectedItem( + initialSelection[containerId] = SelectedItem( id: containerId, name: container.name, type: SelectionType.container, @@ -186,8 +243,8 @@ class _EquipmentSelectionDialogState extends State { // Ajouter les enfants comme sélectionnés aussi for (var equipmentId in container.equipmentIds) { - if (!_selectedItems.containsKey(equipmentId)) { - _selectedItems[equipmentId] = SelectedItem( + if (!initialSelection.containsKey(equipmentId)) { + initialSelection[equipmentId] = SelectedItem( id: equipmentId, name: equipmentId, type: SelectionType.equipment, @@ -195,14 +252,21 @@ class _EquipmentSelectionDialogState extends State { ); } } - } - } catch (e) { - DebugLog.error('[EquipmentSelectionDialog] Error loading already assigned containers', e); - } - } - - DebugLog.info('[EquipmentSelectionDialog] Initialized with ${_selectedItems.length} already assigned items'); - } + } + } catch (e) { + DebugLog.error('[EquipmentSelectionDialog] Error loading already assigned containers', e); + } + } + + // Mettre à jour la sélection et notifier + if (mounted && initialSelection.isNotEmpty) { + setState(() { + _selectedItems = initialSelection; + }); + _selectionChangeNotifier.value++; + DebugLog.info('[EquipmentSelectionDialog] Initialized with ${_selectedItems.length} already assigned items'); + } + } @override void dispose() { _searchController.dispose(); @@ -213,14 +277,14 @@ class _EquipmentSelectionDialogState extends State { /// Charge les quantités disponibles pour tous les consommables/câbles Future _loadAvailableQuantities() async { + if (!mounted) return; setState(() => _isLoadingQuantities = true); try { final equipmentProvider = context.read(); - // EquipmentProvider utilise un stream, récupérons les données - final equipmentStream = equipmentProvider.equipmentStream; - final equipment = await equipmentStream.first; + // Utiliser directement allEquipment du provider (déjà chargé) + final equipment = equipmentProvider.allEquipment; final consumables = equipment.where((eq) => eq.category == EquipmentCategory.consumable || @@ -287,7 +351,6 @@ class _EquipmentSelectionDialogState extends State { _conflictingContainerIds = conflictingContainerIds; _conflictDetails = conflictDetails; _equipmentQuantities = equipmentQuantities; - _conflictsLoaded = true; // Marquer comme chargé }); } @@ -381,7 +444,6 @@ class _EquipmentSelectionDialogState extends State { 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? ?? []; final unit = equipment.category == EquipmentCategory.cable ? "m" : ""; @@ -625,24 +687,33 @@ class _EquipmentSelectionDialogState extends State { await _deselectContainerChildren(id); } - // Mise à jour sans setState - utiliser ValueNotifier pour notifier uniquement les cards concernées - _selectedItems.remove(id); + // Mise à jour avec setState pour garantir le rebuild + if (mounted) { + setState(() { + _selectedItems.remove(id); + }); + } + DebugLog.info('[EquipmentSelectionDialog] After deselection, _selectedItems count: ${_selectedItems.length}'); DebugLog.info('[EquipmentSelectionDialog] Remaining items: ${_selectedItems.keys.toList()}'); - // Notifier le changement sans rebuilder toute la liste + // Notifier le changement _selectionChangeNotifier.value++; } else { // Sélectionner DebugLog.info('[EquipmentSelectionDialog] Selecting $type: $id'); - // Mise à jour sans setState - utiliser ValueNotifier - _selectedItems[id] = SelectedItem( - id: id, - name: name, - type: type, - quantity: 1, - ); + // Mise à jour avec setState pour garantir le rebuild + if (mounted) { + setState(() { + _selectedItems[id] = SelectedItem( + id: id, + name: name, + type: type, + quantity: 1, + ); + }); + } // Si c'est un équipement, chercher les conteneurs recommandés if (type == SelectionType.equipment) { @@ -654,7 +725,7 @@ class _EquipmentSelectionDialogState extends State { await _selectContainerChildren(id); } - // Notifier le changement sans rebuilder toute la liste + // Notifier le changement _selectionChangeNotifier.value++; } } @@ -665,8 +736,9 @@ class _EquipmentSelectionDialogState extends State { final containerProvider = context.read(); final equipmentProvider = context.read(); - final containers = await containerProvider.containersStream.first; - final equipment = await equipmentProvider.equipmentStream.first; + // Utiliser le cache si disponible + final containers = _cachedContainers.isNotEmpty ? _cachedContainers : containerProvider.containers; + final equipment = _cachedEquipment.isNotEmpty ? _cachedEquipment : equipmentProvider.allEquipment; final container = containers.firstWhere( (c) => c.id == containerId, @@ -713,6 +785,8 @@ class _EquipmentSelectionDialogState extends State { } } } + + DebugLog.info('[EquipmentSelectionDialog] Selected container $containerId with ${container.equipmentIds.length} children'); } catch (e) { DebugLog.error('Error selecting container children', e); } @@ -722,7 +796,9 @@ class _EquipmentSelectionDialogState extends State { Future _deselectContainerChildren(String containerId) async { try { final containerProvider = context.read(); - final containers = await containerProvider.containersStream.first; + + // Utiliser le cache si disponible + final containers = _cachedContainers.isNotEmpty ? _cachedContainers : containerProvider.containers; final container = containers.firstWhere( (c) => c.id == containerId, @@ -737,17 +813,21 @@ class _EquipmentSelectionDialogState extends State { ), ); - // Retirer les enfants de _selectedItems - for (var equipmentId in container.equipmentIds) { - _selectedItems.remove(equipmentId); + if (mounted) { + setState(() { + // Retirer les enfants de _selectedItems + for (var equipmentId in container.equipmentIds) { + _selectedItems.remove(equipmentId); + } + + // Nettoyer le cache + _containerEquipmentCache.remove(containerId); + + // Retirer de la liste des conteneurs expandés + _expandedContainers.remove(containerId); + }); } - // Nettoyer le cache - _containerEquipmentCache.remove(containerId); - - // Retirer de la liste des conteneurs expandés - _expandedContainers.remove(containerId); - DebugLog.info('[EquipmentSelectionDialog] Deselected container $containerId and ${container.equipmentIds.length} children'); } catch (e) { DebugLog.error('Error deselecting container children', e); @@ -969,6 +1049,36 @@ class _EquipmentSelectionDialogState extends State { ], ), ), + + const SizedBox(height: 12), + + // Checkbox pour afficher les équipements en conflit + Row( + children: [ + Checkbox( + value: _showConflictingItems, + onChanged: (value) { + setState(() { + _showConflictingItems = value ?? false; + }); + }, + activeColor: AppColors.rouge, + ), + const Text( + 'Afficher les équipements déjà utilisés', + style: TextStyle(fontSize: 14), + ), + const SizedBox(width: 8), + Tooltip( + message: 'Afficher les équipements et boîtes qui sont déjà utilisés durant ces dates', + child: Icon( + Icons.info_outline, + size: 18, + color: Colors.grey.shade600, + ), + ), + ], + ), ], ), ); @@ -1021,30 +1131,9 @@ class _EquipmentSelectionDialogState extends State { Widget _buildHierarchicalList() { return Consumer2( builder: (context, containerProvider, equipmentProvider, child) { - // Charger les données initiales dans le cache si pas encore fait - if (!_initialDataLoaded) { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) { - setState(() { - _cachedContainers = containerProvider.containers; - _cachedEquipment = equipmentProvider.equipment; - _initialDataLoaded = true; - }); - } - }); - } - - // Utiliser les données du cache au lieu des streams - final allContainers = _cachedContainers.isNotEmpty ? _cachedContainers : containerProvider.containers; - final allEquipment = _cachedEquipment.isNotEmpty ? _cachedEquipment : equipmentProvider.equipment; - - // Charger les conflits une seule fois après le chargement des données - if (!_isLoadingConflicts && !_conflictsLoaded && allEquipment.isNotEmpty) { - // Lancer le chargement des conflits en arrière-plan - WidgetsBinding.instance.addPostFrameCallback((_) { - _loadEquipmentConflicts(); - }); - } + // Utiliser les données du cache si disponibles, sinon utiliser allEquipment des providers + final allContainers = _initialDataLoaded ? _cachedContainers : containerProvider.containers; + final allEquipment = _initialDataLoaded ? _cachedEquipment : equipmentProvider.allEquipment; // Utiliser ValueListenableBuilder pour rebuild uniquement sur changement de sélection return ValueListenableBuilder( @@ -1052,16 +1141,52 @@ class _EquipmentSelectionDialogState extends State { builder: (context, _, __) { // Filtrage des boîtes final filteredContainers = allContainers.where((container) { + // Filtre par conflit (masquer si non cochée et en conflit) + if (!_showConflictingItems && _conflictingContainerIds.contains(container.id)) { + return false; + } + + // Filtre par catégorie : afficher uniquement les boîtes contenant au moins 1 équipement de la catégorie + if (_selectedCategory != null) { + final hasEquipmentOfCategory = container.equipmentIds.any((eqId) { + final equipment = allEquipment.firstWhere( + (e) => e.id == eqId, + orElse: () => EquipmentModel( + id: '', + name: '', + category: EquipmentCategory.other, + status: EquipmentStatus.available, + parentBoxIds: [], + maintenanceIds: [], + createdAt: DateTime.now(), + updatedAt: DateTime.now(), + ), + ); + return equipment.id.isNotEmpty && equipment.category == _selectedCategory; + }); + + if (!hasEquipmentOfCategory) { + return false; + } + } + + // Filtre par recherche if (_searchQuery.isNotEmpty) { final searchLower = _searchQuery.toLowerCase(); return container.id.toLowerCase().contains(searchLower) || container.name.toLowerCase().contains(searchLower); } + return true; }).toList(); // Filtrage des équipements (TOUS, pas seulement les orphelins) final filteredEquipment = allEquipment.where((eq) { + // Filtre par conflit (masquer si non cochée et en conflit) + if (!_showConflictingItems && _conflictingEquipmentIds.contains(eq.id)) { + return false; + } + // Filtre par catégorie if (_selectedCategory != null && eq.category != _selectedCategory) { return false; @@ -1085,23 +1210,47 @@ class _EquipmentSelectionDialogState extends State { children: [ // SECTION 1 : BOÎTES if (filteredContainers.isNotEmpty) ...[ - _buildSectionHeader('Boîtes', Icons.inventory, filteredContainers.length), + _buildCollapsibleSectionHeader( + 'Boîtes', + Icons.inventory, + filteredContainers.length, + _containersExpanded, + (expanded) { + setState(() { + _containersExpanded = expanded; + }); + }, + ), const SizedBox(height: 12), - ...filteredContainers.map((container) => _buildContainerCard( - container, - key: ValueKey('container_${container.id}'), - )), - const SizedBox(height: 24), + if (_containersExpanded) ...[ + ...filteredContainers.map((container) => _buildContainerCard( + container, + key: ValueKey('container_${container.id}'), + )), + const SizedBox(height: 24), + ], ], // SECTION 2 : TOUS LES ÉQUIPEMENTS if (filteredEquipment.isNotEmpty) ...[ - _buildSectionHeader('Tous les équipements', Icons.inventory_2, filteredEquipment.length), + _buildCollapsibleSectionHeader( + 'Tous les équipements', + Icons.inventory_2, + filteredEquipment.length, + _equipmentExpanded, + (expanded) { + setState(() { + _equipmentExpanded = expanded; + }); + }, + ), const SizedBox(height: 12), - ...filteredEquipment.map((equipment) => _buildEquipmentCard( - equipment, - key: ValueKey('equipment_${equipment.id}'), - )), + if (_equipmentExpanded) ...[ + ...filteredEquipment.map((equipment) => _buildEquipmentCard( + equipment, + key: ValueKey('equipment_${equipment.id}'), + )), + ], ], // Message si rien n'est trouvé @@ -1129,7 +1278,7 @@ class _EquipmentSelectionDialogState extends State { ); } - /// Header de section + /// Header de section (version simple, gardée pour compatibilité) Widget _buildSectionHeader(String title, IconData icon, int count) { return Container( padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12), @@ -1170,6 +1319,66 @@ class _EquipmentSelectionDialogState extends State { ); } + /// Header de section repliable + Widget _buildCollapsibleSectionHeader( + String title, + IconData icon, + int count, + bool isExpanded, + Function(bool) onToggle, + ) { + return InkWell( + onTap: () => onToggle(!isExpanded), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 12), + decoration: BoxDecoration( + color: AppColors.rouge.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: AppColors.rouge.withValues(alpha: 0.3), + width: 1, + ), + ), + child: Row( + children: [ + Icon( + isExpanded ? Icons.keyboard_arrow_down : Icons.keyboard_arrow_right, + color: AppColors.rouge, + size: 24, + ), + const SizedBox(width: 8), + Icon(icon, color: AppColors.rouge, size: 20), + const SizedBox(width: 8), + Text( + title, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: AppColors.rouge, + ), + ), + const Spacer(), + Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + color: AppColors.rouge, + borderRadius: BorderRadius.circular(12), + ), + child: Text( + '$count', + style: const TextStyle( + color: Colors.white, + fontSize: 13, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ), + ); + } + Widget _buildEquipmentCard(EquipmentModel equipment, {Key? key}) { final isSelected = _selectedItems.containsKey(equipment.id); final isConsumable = equipment.category == EquipmentCategory.consumable || @@ -1469,8 +1678,12 @@ class _EquipmentSelectionDialogState extends State { icon: const Icon(Icons.remove_circle_outline), onPressed: isSelected && selectedItem.quantity > 1 ? () { - _selectedItems[equipmentId] = selectedItem.copyWith(quantity: selectedItem.quantity - 1); - _selectionChangeNotifier.value++; // Notifier sans rebuild complet + if (mounted) { + setState(() { + _selectedItems[equipmentId] = selectedItem.copyWith(quantity: selectedItem.quantity - 1); + }); + _selectionChangeNotifier.value++; // Notifier le changement + } } : null, iconSize: 20, @@ -1500,8 +1713,12 @@ class _EquipmentSelectionDialogState extends State { ); } else { // Item déjà sélectionné : incrémenter - _selectedItems[equipmentId] = selectedItem.copyWith(quantity: selectedItem.quantity + 1); - _selectionChangeNotifier.value++; // Notifier sans rebuild complet + if (mounted) { + setState(() { + _selectedItems[equipmentId] = selectedItem.copyWith(quantity: selectedItem.quantity + 1); + }); + _selectionChangeNotifier.value++; // Notifier le changement + } } } : null, @@ -2195,45 +2412,50 @@ class _EquipmentSelectionDialogState extends State { } Widget _buildFooter() { - return Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.white, - border: Border(top: BorderSide(color: Colors.grey.shade300)), - boxShadow: [ - BoxShadow( - color: Colors.grey.withValues(alpha: 0.2), - spreadRadius: 1, - blurRadius: 5, - offset: const Offset(0, -2), + return ValueListenableBuilder( + valueListenable: _selectionChangeNotifier, + builder: (context, _, __) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + border: Border(top: BorderSide(color: Colors.grey.shade300)), + boxShadow: [ + BoxShadow( + color: Colors.grey.withValues(alpha: 0.2), + spreadRadius: 1, + blurRadius: 5, + offset: const Offset(0, -2), + ), + ], ), - ], - ), - child: Row( - children: [ - Text( - '${_selectedItems.length} élément(s) sélectionné(s)', - style: const TextStyle(fontWeight: FontWeight.w500), + child: Row( + children: [ + Text( + '${_selectedItems.length} élément(s) sélectionné(s)', + style: const TextStyle(fontWeight: FontWeight.w500), + ), + const Spacer(), + OutlinedButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Annuler'), + ), + const SizedBox(width: 12), + ElevatedButton( + onPressed: _selectedItems.isEmpty + ? null + : () => Navigator.of(context).pop(_selectedItems), + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.rouge, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 14), + ), + child: const Text('Valider la sélection'), + ), + ], ), - const Spacer(), - OutlinedButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('Annuler'), - ), - const SizedBox(width: 12), - ElevatedButton( - onPressed: _selectedItems.isEmpty - ? null - : () => Navigator.of(context).pop(_selectedItems), - style: ElevatedButton.styleFrom( - backgroundColor: AppColors.rouge, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 14), - ), - child: const Text('Valider la sélection'), - ), - ], - ), + ); + }, ); } }