refactor: Amélioration et stabilisation du sélecteur d'équipement

Cette mise à jour refactorise en profondeur la boîte de dialogue de sélection d'équipement pour améliorer ses performances, sa stabilité et son ergonomie. Le chargement des données a été entièrement revu pour être plus robuste et les interactions utilisateur sont désormais plus fluides et intuitives.

**Améliorations de performance et de stabilité :**
- **Chargement de données asynchrone et robuste :** Remplacement de la logique de chargement dans `initState` par une nouvelle méthode `_initializeData`. Cette méthode force le chargement des équipements et des conteneurs via `ensureLoaded()` et attend activement leur complétion avant de poursuivre, garantissant ainsi que toutes les données sont disponibles avant l'affichage.
- **Mise en cache locale :** Les données des équipements et conteneurs sont mises en cache (`_cachedEquipment`, `_cachedContainers`) après le chargement initial. Toute la boîte de dialogue utilise désormais ce cache, éliminant les appels répétés aux `Stream` et réduisant les rebuilds inutiles.
- **Fiabilisation des `setState` :** Les modifications de la sélection (`_selectedItems`) sont maintenant systématiquement wrappées dans des appels `setState` pour assurer que l'interface graphique se met à jour de manière fiable, corrigeant des bugs où la sélection n'était pas reflétée visuellement.

**Nouvelles fonctionnalités et améliorations de l'UX :**
- **Sections dépliables :** Les listes "Boîtes" et "Tous les équipements" sont désormais dans des sections qui peuvent être repliées, permettant à l'utilisateur de se concentrer sur l'une ou l'autre et d'améliorer la lisibilité sur les petits écrans.
- **Option d'affichage des conflits :** Ajout d'une checkbox "Afficher les équipements déjà utilisés". Lorsqu'elle est décochée, tous les équipements et boîtes en conflit avec les dates de l'événement sont masqués, simplifiant la recherche de matériel disponible.
- **Meilleure gestion des filtres :** Le filtre par catégorie s'applique désormais aussi aux boîtes, n'affichant que celles qui contiennent au moins un équipement de la catégorie sélectionnée.
- **Notifications de sélection affinées :** Le compteur dans le pied de page (`_selectedItems.length`) est maintenant mis à jour en temps réel à chaque modification de la sélection grâce à un `ValueListenableBuilder`.

**Refactorisation mineure :**
- **`EquipmentProvider` :** Ajout d'un getter `allEquipment` pour fournir un accès à la liste complète des équipements, non filtrée, utilisée par la boîte de dialogue pour sa logique de cache et de filtrage.
This commit is contained in:
ElPoyo
2026-01-15 23:59:25 +01:00
parent beaabceda4
commit 67b85d323c
2 changed files with 352 additions and 129 deletions

View File

@@ -23,6 +23,7 @@ class EquipmentProvider extends ChangeNotifier {
// Getters // Getters
List<EquipmentModel> get equipment => _filteredEquipment; List<EquipmentModel> get equipment => _filteredEquipment;
List<EquipmentModel> get allEquipment => _equipment; // Tous les équipements sans filtre
List<String> get models => _models; List<String> get models => _models;
List<String> get brands => _brands; List<String> get brands => _brands;
EquipmentCategory? get selectedCategory => _selectedCategory; EquipmentCategory? get selectedCategory => _selectedCategory;

View File

@@ -111,9 +111,13 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
bool _isLoadingQuantities = false; bool _isLoadingQuantities = false;
bool _isLoadingConflicts = false; bool _isLoadingConflicts = false;
bool _conflictsLoaded = false; // Flag pour éviter de recharger indéfiniment
String _searchQuery = ''; 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 // Cache pour éviter les rebuilds inutiles
List<ContainerModel> _cachedContainers = []; List<ContainerModel> _cachedContainers = [];
List<EquipmentModel> _cachedEquipment = []; List<EquipmentModel> _cachedEquipment = [];
@@ -123,31 +127,84 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
void initState() { void initState() {
super.initState(); super.initState();
// Charger après le premier frame pour éviter setState pendant build // Charger immédiatement les données de manière asynchrone
WidgetsBinding.instance.addPostFrameCallback((_) { _initializeData();
_ensureEquipmentsLoaded(); }
_initializeAlreadyAssigned();
_loadAvailableQuantities(); /// Initialise toutes les données nécessaires
_loadEquipmentConflicts(); Future<void> _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<EquipmentProvider>();
final containerProvider = context.read<ContainerProvider>();
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 /// S'assure que les équipements sont chargés avant d'utiliser le dialog
Future<void> _ensureEquipmentsLoaded() async { Future<void> _ensureEquipmentsLoaded() async {
final equipmentProvider = context.read<EquipmentProvider>(); final equipmentProvider = context.read<EquipmentProvider>();
final containerProvider = context.read<ContainerProvider>(); final containerProvider = context.read<ContainerProvider>();
DebugLog.info('[EquipmentSelectionDialog] Starting equipment loading...');
// Forcer le chargement et attendre qu'il soit terminé
await equipmentProvider.ensureLoaded(); await equipmentProvider.ensureLoaded();
// Charger aussi les conteneurs si nécessaire
if (!containerProvider.isLoading && containerProvider.containers.isEmpty) { // Attendre que le chargement soit vraiment terminé
await containerProvider.loadContainers(); 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é /// Initialise la sélection avec le matériel déjà assigné
Future<void> _initializeAlreadyAssigned() async { Future<void> _initializeAlreadyAssigned() async {
final Map<String, SelectedItem> initialSelection = {};
// Ajouter les équipements déjà assignés // Ajouter les équipements déjà assignés
for (var eq in widget.alreadyAssigned) { for (var eq in widget.alreadyAssigned) {
_selectedItems[eq.equipmentId] = SelectedItem( initialSelection[eq.equipmentId] = SelectedItem(
id: eq.equipmentId, id: eq.equipmentId,
name: eq.equipmentId, name: eq.equipmentId,
type: SelectionType.equipment, type: SelectionType.equipment,
@@ -159,7 +216,7 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
if (widget.alreadyAssignedContainers.isNotEmpty) { if (widget.alreadyAssignedContainers.isNotEmpty) {
try { try {
final containerProvider = context.read<ContainerProvider>(); final containerProvider = context.read<ContainerProvider>();
final containers = await containerProvider.containersStream.first; final containers = _cachedContainers.isNotEmpty ? _cachedContainers : containerProvider.containers;
for (var containerId in widget.alreadyAssignedContainers) { for (var containerId in widget.alreadyAssignedContainers) {
final container = containers.firstWhere( final container = containers.firstWhere(
@@ -175,7 +232,7 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
), ),
); );
_selectedItems[containerId] = SelectedItem( initialSelection[containerId] = SelectedItem(
id: containerId, id: containerId,
name: container.name, name: container.name,
type: SelectionType.container, type: SelectionType.container,
@@ -186,8 +243,8 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
// Ajouter les enfants comme sélectionnés aussi // Ajouter les enfants comme sélectionnés aussi
for (var equipmentId in container.equipmentIds) { for (var equipmentId in container.equipmentIds) {
if (!_selectedItems.containsKey(equipmentId)) { if (!initialSelection.containsKey(equipmentId)) {
_selectedItems[equipmentId] = SelectedItem( initialSelection[equipmentId] = SelectedItem(
id: equipmentId, id: equipmentId,
name: equipmentId, name: equipmentId,
type: SelectionType.equipment, type: SelectionType.equipment,
@@ -201,8 +258,15 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
} }
} }
// 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'); DebugLog.info('[EquipmentSelectionDialog] Initialized with ${_selectedItems.length} already assigned items');
} }
}
@override @override
void dispose() { void dispose() {
_searchController.dispose(); _searchController.dispose();
@@ -213,14 +277,14 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
/// Charge les quantités disponibles pour tous les consommables/câbles /// Charge les quantités disponibles pour tous les consommables/câbles
Future<void> _loadAvailableQuantities() async { Future<void> _loadAvailableQuantities() async {
if (!mounted) return;
setState(() => _isLoadingQuantities = true); setState(() => _isLoadingQuantities = true);
try { try {
final equipmentProvider = context.read<EquipmentProvider>(); final equipmentProvider = context.read<EquipmentProvider>();
// EquipmentProvider utilise un stream, récupérons les données // Utiliser directement allEquipment du provider (déjà chargé)
final equipmentStream = equipmentProvider.equipmentStream; final equipment = equipmentProvider.allEquipment;
final equipment = await equipmentStream.first;
final consumables = equipment.where((eq) => final consumables = equipment.where((eq) =>
eq.category == EquipmentCategory.consumable || eq.category == EquipmentCategory.consumable ||
@@ -287,7 +351,6 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
_conflictingContainerIds = conflictingContainerIds; _conflictingContainerIds = conflictingContainerIds;
_conflictDetails = conflictDetails; _conflictDetails = conflictDetails;
_equipmentQuantities = equipmentQuantities; _equipmentQuantities = equipmentQuantities;
_conflictsLoaded = true; // Marquer comme chargé
}); });
} }
@@ -381,7 +444,6 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
final totalQuantity = quantityInfo['totalQuantity'] as int? ?? 0; final totalQuantity = quantityInfo['totalQuantity'] as int? ?? 0;
final availableQuantity = quantityInfo['availableQuantity'] 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 reservations = quantityInfo['reservations'] as List<dynamic>? ?? [];
final unit = equipment.category == EquipmentCategory.cable ? "m" : ""; final unit = equipment.category == EquipmentCategory.cable ? "m" : "";
@@ -625,24 +687,33 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
await _deselectContainerChildren(id); await _deselectContainerChildren(id);
} }
// Mise à jour sans setState - utiliser ValueNotifier pour notifier uniquement les cards concernées // Mise à jour avec setState pour garantir le rebuild
if (mounted) {
setState(() {
_selectedItems.remove(id); _selectedItems.remove(id);
});
}
DebugLog.info('[EquipmentSelectionDialog] After deselection, _selectedItems count: ${_selectedItems.length}'); DebugLog.info('[EquipmentSelectionDialog] After deselection, _selectedItems count: ${_selectedItems.length}');
DebugLog.info('[EquipmentSelectionDialog] Remaining items: ${_selectedItems.keys.toList()}'); DebugLog.info('[EquipmentSelectionDialog] Remaining items: ${_selectedItems.keys.toList()}');
// Notifier le changement sans rebuilder toute la liste // Notifier le changement
_selectionChangeNotifier.value++; _selectionChangeNotifier.value++;
} else { } else {
// Sélectionner // Sélectionner
DebugLog.info('[EquipmentSelectionDialog] Selecting $type: $id'); DebugLog.info('[EquipmentSelectionDialog] Selecting $type: $id');
// Mise à jour sans setState - utiliser ValueNotifier // Mise à jour avec setState pour garantir le rebuild
if (mounted) {
setState(() {
_selectedItems[id] = SelectedItem( _selectedItems[id] = SelectedItem(
id: id, id: id,
name: name, name: name,
type: type, type: type,
quantity: 1, quantity: 1,
); );
});
}
// Si c'est un équipement, chercher les conteneurs recommandés // Si c'est un équipement, chercher les conteneurs recommandés
if (type == SelectionType.equipment) { if (type == SelectionType.equipment) {
@@ -654,7 +725,7 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
await _selectContainerChildren(id); await _selectContainerChildren(id);
} }
// Notifier le changement sans rebuilder toute la liste // Notifier le changement
_selectionChangeNotifier.value++; _selectionChangeNotifier.value++;
} }
} }
@@ -665,8 +736,9 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
final containerProvider = context.read<ContainerProvider>(); final containerProvider = context.read<ContainerProvider>();
final equipmentProvider = context.read<EquipmentProvider>(); final equipmentProvider = context.read<EquipmentProvider>();
final containers = await containerProvider.containersStream.first; // Utiliser le cache si disponible
final equipment = await equipmentProvider.equipmentStream.first; final containers = _cachedContainers.isNotEmpty ? _cachedContainers : containerProvider.containers;
final equipment = _cachedEquipment.isNotEmpty ? _cachedEquipment : equipmentProvider.allEquipment;
final container = containers.firstWhere( final container = containers.firstWhere(
(c) => c.id == containerId, (c) => c.id == containerId,
@@ -713,6 +785,8 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
} }
} }
} }
DebugLog.info('[EquipmentSelectionDialog] Selected container $containerId with ${container.equipmentIds.length} children');
} catch (e) { } catch (e) {
DebugLog.error('Error selecting container children', e); DebugLog.error('Error selecting container children', e);
} }
@@ -722,7 +796,9 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
Future<void> _deselectContainerChildren(String containerId) async { Future<void> _deselectContainerChildren(String containerId) async {
try { try {
final containerProvider = context.read<ContainerProvider>(); final containerProvider = context.read<ContainerProvider>();
final containers = await containerProvider.containersStream.first;
// Utiliser le cache si disponible
final containers = _cachedContainers.isNotEmpty ? _cachedContainers : containerProvider.containers;
final container = containers.firstWhere( final container = containers.firstWhere(
(c) => c.id == containerId, (c) => c.id == containerId,
@@ -737,6 +813,8 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
), ),
); );
if (mounted) {
setState(() {
// Retirer les enfants de _selectedItems // Retirer les enfants de _selectedItems
for (var equipmentId in container.equipmentIds) { for (var equipmentId in container.equipmentIds) {
_selectedItems.remove(equipmentId); _selectedItems.remove(equipmentId);
@@ -747,6 +825,8 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
// Retirer de la liste des conteneurs expandés // Retirer de la liste des conteneurs expandés
_expandedContainers.remove(containerId); _expandedContainers.remove(containerId);
});
}
DebugLog.info('[EquipmentSelectionDialog] Deselected container $containerId and ${container.equipmentIds.length} children'); DebugLog.info('[EquipmentSelectionDialog] Deselected container $containerId and ${container.equipmentIds.length} children');
} catch (e) { } catch (e) {
@@ -969,6 +1049,36 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
], ],
), ),
), ),
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<EquipmentSelectionDialog> {
Widget _buildHierarchicalList() { Widget _buildHierarchicalList() {
return Consumer2<ContainerProvider, EquipmentProvider>( return Consumer2<ContainerProvider, EquipmentProvider>(
builder: (context, containerProvider, equipmentProvider, child) { builder: (context, containerProvider, equipmentProvider, child) {
// Charger les données initiales dans le cache si pas encore fait // Utiliser les données du cache si disponibles, sinon utiliser allEquipment des providers
if (!_initialDataLoaded) { final allContainers = _initialDataLoaded ? _cachedContainers : containerProvider.containers;
WidgetsBinding.instance.addPostFrameCallback((_) { final allEquipment = _initialDataLoaded ? _cachedEquipment : equipmentProvider.allEquipment;
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 ValueListenableBuilder pour rebuild uniquement sur changement de sélection // Utiliser ValueListenableBuilder pour rebuild uniquement sur changement de sélection
return ValueListenableBuilder<int>( return ValueListenableBuilder<int>(
@@ -1052,16 +1141,52 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
builder: (context, _, __) { builder: (context, _, __) {
// Filtrage des boîtes // Filtrage des boîtes
final filteredContainers = allContainers.where((container) { 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) { if (_searchQuery.isNotEmpty) {
final searchLower = _searchQuery.toLowerCase(); final searchLower = _searchQuery.toLowerCase();
return container.id.toLowerCase().contains(searchLower) || return container.id.toLowerCase().contains(searchLower) ||
container.name.toLowerCase().contains(searchLower); container.name.toLowerCase().contains(searchLower);
} }
return true; return true;
}).toList(); }).toList();
// Filtrage des équipements (TOUS, pas seulement les orphelins) // Filtrage des équipements (TOUS, pas seulement les orphelins)
final filteredEquipment = allEquipment.where((eq) { 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 // Filtre par catégorie
if (_selectedCategory != null && eq.category != _selectedCategory) { if (_selectedCategory != null && eq.category != _selectedCategory) {
return false; return false;
@@ -1085,24 +1210,48 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
children: [ children: [
// SECTION 1 : BOÎTES // SECTION 1 : BOÎTES
if (filteredContainers.isNotEmpty) ...[ 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), const SizedBox(height: 12),
if (_containersExpanded) ...[
...filteredContainers.map((container) => _buildContainerCard( ...filteredContainers.map((container) => _buildContainerCard(
container, container,
key: ValueKey('container_${container.id}'), key: ValueKey('container_${container.id}'),
)), )),
const SizedBox(height: 24), const SizedBox(height: 24),
], ],
],
// SECTION 2 : TOUS LES ÉQUIPEMENTS // SECTION 2 : TOUS LES ÉQUIPEMENTS
if (filteredEquipment.isNotEmpty) ...[ 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), const SizedBox(height: 12),
if (_equipmentExpanded) ...[
...filteredEquipment.map((equipment) => _buildEquipmentCard( ...filteredEquipment.map((equipment) => _buildEquipmentCard(
equipment, equipment,
key: ValueKey('equipment_${equipment.id}'), key: ValueKey('equipment_${equipment.id}'),
)), )),
], ],
],
// Message si rien n'est trouvé // Message si rien n'est trouvé
if (filteredContainers.isEmpty && filteredEquipment.isEmpty) if (filteredContainers.isEmpty && filteredEquipment.isEmpty)
@@ -1129,7 +1278,7 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
); );
} }
/// Header de section /// Header de section (version simple, gardée pour compatibilité)
Widget _buildSectionHeader(String title, IconData icon, int count) { Widget _buildSectionHeader(String title, IconData icon, int count) {
return Container( return Container(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12), padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
@@ -1170,6 +1319,66 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
); );
} }
/// 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}) { Widget _buildEquipmentCard(EquipmentModel equipment, {Key? key}) {
final isSelected = _selectedItems.containsKey(equipment.id); final isSelected = _selectedItems.containsKey(equipment.id);
final isConsumable = equipment.category == EquipmentCategory.consumable || final isConsumable = equipment.category == EquipmentCategory.consumable ||
@@ -1469,8 +1678,12 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
icon: const Icon(Icons.remove_circle_outline), icon: const Icon(Icons.remove_circle_outline),
onPressed: isSelected && selectedItem.quantity > 1 onPressed: isSelected && selectedItem.quantity > 1
? () { ? () {
if (mounted) {
setState(() {
_selectedItems[equipmentId] = selectedItem.copyWith(quantity: selectedItem.quantity - 1); _selectedItems[equipmentId] = selectedItem.copyWith(quantity: selectedItem.quantity - 1);
_selectionChangeNotifier.value++; // Notifier sans rebuild complet });
_selectionChangeNotifier.value++; // Notifier le changement
}
} }
: null, : null,
iconSize: 20, iconSize: 20,
@@ -1500,8 +1713,12 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
); );
} else { } else {
// Item déjà sélectionné : incrémenter // Item déjà sélectionné : incrémenter
if (mounted) {
setState(() {
_selectedItems[equipmentId] = selectedItem.copyWith(quantity: selectedItem.quantity + 1); _selectedItems[equipmentId] = selectedItem.copyWith(quantity: selectedItem.quantity + 1);
_selectionChangeNotifier.value++; // Notifier sans rebuild complet });
_selectionChangeNotifier.value++; // Notifier le changement
}
} }
} }
: null, : null,
@@ -2195,6 +2412,9 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
} }
Widget _buildFooter() { Widget _buildFooter() {
return ValueListenableBuilder<int>(
valueListenable: _selectionChangeNotifier,
builder: (context, _, __) {
return Container( return Container(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -2235,6 +2455,8 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
], ],
), ),
); );
},
);
} }
} }