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:
@@ -23,6 +23,7 @@ class EquipmentProvider extends ChangeNotifier {
|
||||
|
||||
// Getters
|
||||
List<EquipmentModel> get equipment => _filteredEquipment;
|
||||
List<EquipmentModel> get allEquipment => _equipment; // Tous les équipements sans filtre
|
||||
List<String> get models => _models;
|
||||
List<String> get brands => _brands;
|
||||
EquipmentCategory? get selectedCategory => _selectedCategory;
|
||||
|
||||
@@ -111,9 +111,13 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
||||
|
||||
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<ContainerModel> _cachedContainers = [];
|
||||
List<EquipmentModel> _cachedEquipment = [];
|
||||
@@ -123,31 +127,84 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
||||
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<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
|
||||
Future<void> _ensureEquipmentsLoaded() async {
|
||||
final equipmentProvider = context.read<EquipmentProvider>();
|
||||
final containerProvider = context.read<ContainerProvider>();
|
||||
|
||||
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<void> _initializeAlreadyAssigned() async {
|
||||
final Map<String, SelectedItem> 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<EquipmentSelectionDialog> {
|
||||
if (widget.alreadyAssignedContainers.isNotEmpty) {
|
||||
try {
|
||||
final containerProvider = context.read<ContainerProvider>();
|
||||
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<EquipmentSelectionDialog> {
|
||||
),
|
||||
);
|
||||
|
||||
_selectedItems[containerId] = SelectedItem(
|
||||
initialSelection[containerId] = SelectedItem(
|
||||
id: containerId,
|
||||
name: container.name,
|
||||
type: SelectionType.container,
|
||||
@@ -186,8 +243,8 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
||||
|
||||
// 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<EquipmentSelectionDialog> {
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
DebugLog.error('[EquipmentSelectionDialog] Error loading already assigned containers', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
DebugLog.error('[EquipmentSelectionDialog] Error loading already assigned containers', e);
|
||||
}
|
||||
}
|
||||
|
||||
DebugLog.info('[EquipmentSelectionDialog] Initialized with ${_selectedItems.length} already assigned items');
|
||||
}
|
||||
// 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<EquipmentSelectionDialog> {
|
||||
|
||||
/// Charge les quantités disponibles pour tous les consommables/câbles
|
||||
Future<void> _loadAvailableQuantities() async {
|
||||
if (!mounted) return;
|
||||
setState(() => _isLoadingQuantities = true);
|
||||
|
||||
try {
|
||||
final equipmentProvider = context.read<EquipmentProvider>();
|
||||
|
||||
// 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<EquipmentSelectionDialog> {
|
||||
_conflictingContainerIds = conflictingContainerIds;
|
||||
_conflictDetails = conflictDetails;
|
||||
_equipmentQuantities = equipmentQuantities;
|
||||
_conflictsLoaded = true; // Marquer comme chargé
|
||||
});
|
||||
}
|
||||
|
||||
@@ -381,7 +444,6 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
||||
|
||||
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" : "";
|
||||
|
||||
@@ -625,24 +687,33 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
||||
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<EquipmentSelectionDialog> {
|
||||
await _selectContainerChildren(id);
|
||||
}
|
||||
|
||||
// Notifier le changement sans rebuilder toute la liste
|
||||
// Notifier le changement
|
||||
_selectionChangeNotifier.value++;
|
||||
}
|
||||
}
|
||||
@@ -665,8 +736,9 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
||||
final containerProvider = context.read<ContainerProvider>();
|
||||
final equipmentProvider = context.read<EquipmentProvider>();
|
||||
|
||||
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<EquipmentSelectionDialog> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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<EquipmentSelectionDialog> {
|
||||
Future<void> _deselectContainerChildren(String containerId) async {
|
||||
try {
|
||||
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(
|
||||
(c) => c.id == containerId,
|
||||
@@ -737,17 +813,21 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
||||
),
|
||||
);
|
||||
|
||||
// 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<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() {
|
||||
return Consumer2<ContainerProvider, EquipmentProvider>(
|
||||
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<int>(
|
||||
@@ -1052,16 +1141,52 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
||||
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<EquipmentSelectionDialog> {
|
||||
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<EquipmentSelectionDialog> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 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<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}) {
|
||||
final isSelected = _selectedItems.containsKey(equipment.id);
|
||||
final isConsumable = equipment.category == EquipmentCategory.consumable ||
|
||||
@@ -1469,8 +1678,12 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
||||
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<EquipmentSelectionDialog> {
|
||||
);
|
||||
} 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<EquipmentSelectionDialog> {
|
||||
}
|
||||
|
||||
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<int>(
|
||||
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'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user