diff --git a/em2rp/CHANGELOG.md b/em2rp/CHANGELOG.md index 997754d..a0b12ee 100644 --- a/em2rp/CHANGELOG.md +++ b/em2rp/CHANGELOG.md @@ -1,6 +1,10 @@ # Changelog - EM2RP Toutes les modifications notables de ce projet seront documentées dans ce fichier. + +## 24/03/2026 +Fix BUG : Problème de cache avec les équipements non affichés dans le dialog de sélection d'équipement. Amélioration de la gestion du cache pour éviter les problèmes d'affichage. + ## 12/03/2026bis Fix BUG : Ajout equipement à un evenement existant, boutons de modification de statut d'un evenement ne fonctionnaient pas. Refonte legere de la page calendrier. diff --git a/em2rp/lib/config/app_version.dart b/em2rp/lib/config/app_version.dart index 4c2febe..c0fc0ae 100644 --- a/em2rp/lib/config/app_version.dart +++ b/em2rp/lib/config/app_version.dart @@ -1,6 +1,6 @@ /// Configuration de la version de l'application class AppVersion { - static const String version = '1.1.18'; + static const String version = '1.1.19'; /// Retourne la version complète de l'application static String get fullVersion => 'v$version'; diff --git a/em2rp/lib/views/container_form_page.dart b/em2rp/lib/views/container_form_page.dart index 443b1f1..9e4845c 100644 --- a/em2rp/lib/views/container_form_page.dart +++ b/em2rp/lib/views/container_form_page.dart @@ -100,7 +100,6 @@ class _ContainerFormPageState extends State { child: ListView( padding: const EdgeInsets.all(24), children: [ - // Nom TextFormField( controller: _nameController, @@ -257,7 +256,8 @@ class _ContainerFormPageState extends State { border: OutlineInputBorder(), prefixIcon: Icon(Icons.scale), ), - keyboardType: const TextInputType.numberWithOptions(decimal: true), + keyboardType: + const TextInputType.numberWithOptions(decimal: true), validator: (value) { if (value != null && value.isNotEmpty) { if (double.tryParse(value) == null) { @@ -279,7 +279,8 @@ class _ContainerFormPageState extends State { labelText: 'Longueur (cm)', border: OutlineInputBorder(), ), - keyboardType: TextInputType.numberWithOptions(decimal: true), + keyboardType: + TextInputType.numberWithOptions(decimal: true), validator: (value) { if (value != null && value.isNotEmpty) { if (double.tryParse(value) == null) { @@ -298,7 +299,8 @@ class _ContainerFormPageState extends State { labelText: 'Largeur (cm)', border: OutlineInputBorder(), ), - keyboardType: TextInputType.numberWithOptions(decimal: true), + keyboardType: + TextInputType.numberWithOptions(decimal: true), validator: (value) { if (value != null && value.isNotEmpty) { if (double.tryParse(value) == null) { @@ -317,7 +319,8 @@ class _ContainerFormPageState extends State { labelText: 'Hauteur (cm)', border: OutlineInputBorder(), ), - keyboardType: TextInputType.numberWithOptions(decimal: true), + keyboardType: + TextInputType.numberWithOptions(decimal: true), validator: (value) { if (value != null && value.isNotEmpty) { if (double.tryParse(value) == null) { @@ -452,6 +455,11 @@ class _ContainerFormPageState extends State { Future _selectEquipment() async { final equipmentProvider = context.read(); + // Toujours charger la liste complète pour éviter d'afficher uniquement + // la page paginée active d'un autre écran. + await equipmentProvider.loadEquipments(); + if (!mounted) return; + await showDialog( context: context, builder: (context) => _EquipmentSelectorDialog( @@ -460,6 +468,7 @@ class _ContainerFormPageState extends State { ), ); + if (!mounted) return; setState(() {}); } @@ -535,7 +544,8 @@ class _ContainerFormPageState extends State { equipmentId: equipmentId, ); } catch (e) { - DebugLog.error('Erreur lors de l\'ajout de l\'équipement $equipmentId', e); + DebugLog.error( + 'Erreur lors de l\'ajout de l\'équipement $equipmentId', e); } } @@ -573,7 +583,8 @@ class _ContainerFormPageState extends State { }); // Gérer les équipements ajoutés - final addedEquipment = _selectedEquipmentIds.difference(container.equipmentIds.toSet()); + final addedEquipment = + _selectedEquipmentIds.difference(container.equipmentIds.toSet()); for (final equipmentId in addedEquipment) { try { await provider.addEquipmentToContainer( @@ -581,12 +592,14 @@ class _ContainerFormPageState extends State { equipmentId: equipmentId, ); } catch (e) { - DebugLog.error('Erreur lors de l\'ajout de l\'équipement $equipmentId', e); + DebugLog.error( + 'Erreur lors de l\'ajout de l\'équipement $equipmentId', e); } } // Gérer les équipements retirés - final removedEquipment = container.equipmentIds.toSet().difference(_selectedEquipmentIds); + final removedEquipment = + container.equipmentIds.toSet().difference(_selectedEquipmentIds); for (final equipmentId in removedEquipment) { try { await provider.removeEquipmentFromContainer( @@ -594,7 +607,8 @@ class _ContainerFormPageState extends State { equipmentId: equipmentId, ); } catch (e) { - DebugLog.error('Erreur lors du retrait de l\'équipement $equipmentId', e); + DebugLog.error( + 'Erreur lors du retrait de l\'équipement $equipmentId', e); } } @@ -630,7 +644,8 @@ class _EquipmentSelectorDialog extends StatefulWidget { }); @override - State<_EquipmentSelectorDialog> createState() => _EquipmentSelectorDialogState(); + State<_EquipmentSelectorDialog> createState() => + _EquipmentSelectorDialogState(); } class _EquipmentSelectorDialogState extends State<_EquipmentSelectorDialog> { @@ -638,12 +653,14 @@ class _EquipmentSelectorDialogState extends State<_EquipmentSelectorDialog> { EquipmentCategory? _filterCategory; String _searchQuery = ''; late Set _tempSelectedIds; + late final Future _loadingFuture; @override void initState() { super.initState(); // Créer une copie temporaire des IDs sélectionnés _tempSelectedIds = Set.from(widget.selectedIds); + _loadingFuture = widget.equipmentProvider.loadEquipments(); } @override @@ -729,7 +746,8 @@ class _EquipmentSelectorDialogState extends State<_EquipmentSelectorDialog> { }, selectedColor: AppColors.rouge, labelStyle: TextStyle( - color: _filterCategory == null ? Colors.white : Colors.black, + color: + _filterCategory == null ? Colors.white : Colors.black, ), ), const SizedBox(width: 8), @@ -746,7 +764,9 @@ class _EquipmentSelectorDialogState extends State<_EquipmentSelectorDialog> { }, selectedColor: AppColors.rouge, labelStyle: TextStyle( - color: _filterCategory == category ? Colors.white : Colors.black, + color: _filterCategory == category + ? Colors.white + : Colors.black, ), ), ); @@ -778,8 +798,8 @@ class _EquipmentSelectorDialogState extends State<_EquipmentSelectorDialog> { // Liste des équipements Expanded( - child: StreamBuilder>( - stream: widget.equipmentProvider.equipmentStream, + child: FutureBuilder( + future: _loadingFuture, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); @@ -789,11 +809,15 @@ class _EquipmentSelectorDialogState extends State<_EquipmentSelectorDialog> { return Center(child: Text('Erreur: ${snapshot.error}')); } - var equipment = snapshot.data ?? []; + var equipment = List.from( + widget.equipmentProvider.allEquipment, + ); // Filtrer par catégorie if (_filterCategory != null) { - equipment = equipment.where((e) => e.category == _filterCategory).toList(); + equipment = equipment + .where((e) => e.category == _filterCategory) + .toList(); } // Filtrer par recherche @@ -801,8 +825,8 @@ class _EquipmentSelectorDialogState extends State<_EquipmentSelectorDialog> { final query = _searchQuery.toLowerCase(); equipment = equipment.where((e) { return e.id.toLowerCase().contains(query) || - (e.brand?.toLowerCase().contains(query) ?? false) || - (e.model?.toLowerCase().contains(query) ?? false); + (e.brand?.toLowerCase().contains(query) ?? false) || + (e.model?.toLowerCase().contains(query) ?? false); }).toList(); } @@ -945,4 +969,4 @@ class _EquipmentSelectorDialogState extends State<_EquipmentSelectorDialog> { return Icons.category; } } -} \ No newline at end of file +} diff --git a/em2rp/lib/views/widgets/event/equipment_selection_dialog.dart b/em2rp/lib/views/widgets/event/equipment_selection_dialog.dart index b6dcf63..26501ba 100644 --- a/em2rp/lib/views/widgets/event/equipment_selection_dialog.dart +++ b/em2rp/lib/views/widgets/event/equipment_selection_dialog.dart @@ -7,15 +7,16 @@ 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/views/widgets/event/equipment_selection_pagination.dart'; /// Type de sélection dans le dialog enum SelectionType { equipment, container } /// Statut de conflit pour un conteneur enum ContainerConflictStatus { - none, // Aucun conflit - partial, // Au moins un enfant en conflit - complete, // Tous les enfants en conflit + none, // Aucun conflit + partial, // Au moins un enfant en conflit + complete, // Tous les enfants en conflit } /// Informations sur les conflits d'un conteneur @@ -81,39 +82,50 @@ class EquipmentSelectionDialog extends StatefulWidget { }); @override - State createState() => _EquipmentSelectionDialogState(); + State createState() => + _EquipmentSelectionDialogState(); } class _EquipmentSelectionDialogState extends State { final TextEditingController _searchController = TextEditingController(); - final ScrollController _scrollController = ScrollController(); // Préserve la position de scroll - final EventAvailabilityService _availabilityService = EventAvailabilityService(); + final ScrollController _scrollController = + ScrollController(); // Préserve la position de scroll + final EventAvailabilityService _availabilityService = + EventAvailabilityService(); final DataService _dataService = DataService(apiService); EquipmentCategory? _selectedCategory; Map _selectedItems = {}; - final ValueNotifier _selectionChangeNotifier = ValueNotifier(0); // Pour notifier les changements de sélection sans setState + final ValueNotifier _selectionChangeNotifier = ValueNotifier( + 0); // Pour notifier les changements de sélection sans setState final Map _availableQuantities = {}; // Pour consommables - final Map> _recommendedContainers = {}; // Recommandations - final Map> _equipmentConflicts = {}; // Conflits de disponibilité (détaillés) - final Map _containerConflicts = {}; // Conflits des conteneurs - final Set _expandedContainers = {}; // Conteneurs dépliés dans la liste + final Map> _recommendedContainers = + {}; // Recommandations + final Map> _equipmentConflicts = + {}; // Conflits de disponibilité (détaillés) + final Map _containerConflicts = + {}; // Conflits des conteneurs + final Set _expandedContainers = + {}; // Conteneurs dépliés dans la liste // NOUVEAU : IDs en conflit récupérés en batch Set _conflictingEquipmentIds = {}; Set _conflictingContainerIds = {}; Map _conflictDetails = {}; // Détails des conflits par ID - Map _equipmentQuantities = {}; // Infos de quantités pour câbles/consommables + Map _equipmentQuantities = + {}; // Infos de quantités pour câbles/consommables bool _isLoadingConflicts = false; String _searchQuery = ''; // Nouvelles options d'affichage - bool _showConflictingItems = false; // Afficher les équipements/boîtes en conflit + bool _showConflictingItems = + false; // Afficher les équipements/boîtes en conflit // NOUVEAU : Lazy loading et pagination - SelectionType _displayType = SelectionType.equipment; // Type affiché (équipements OU containers) + SelectionType _displayType = + SelectionType.equipment; // Type affiché (équipements OU containers) bool _isLoadingMore = false; bool _hasMoreEquipments = true; bool _hasMoreContainers = true; @@ -142,16 +154,54 @@ class _EquipmentSelectionDialogState extends State { if (_isLoadingMore) return; if (_scrollController.hasClients && - _scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 300) { + _scrollController.position.pixels >= + _scrollController.position.maxScrollExtent - 300) { // Charger la page suivante selon le type affiché if (_displayType == SelectionType.equipment && _hasMoreEquipments) { _loadNextEquipmentPage(); - } else if (_displayType == SelectionType.container && _hasMoreContainers) { + } else if (_displayType == SelectionType.container && + _hasMoreContainers) { _loadNextContainerPage(); } } } + bool get _currentDisplayHasMore { + return _displayType == SelectionType.equipment + ? _hasMoreEquipments + : _hasMoreContainers; + } + + Future _loadNextPageForCurrentDisplayType() { + if (_displayType == SelectionType.equipment) { + return _loadNextEquipmentPage(); + } + return _loadNextContainerPage(); + } + + void _scheduleAutoLoadIfListNotScrollable() { + WidgetsBinding.instance.addPostFrameCallback((_) async { + if (!mounted) return; + + final hasClients = _scrollController.hasClients; + final maxScrollExtent = + hasClients ? _scrollController.position.maxScrollExtent : 0.0; + + final shouldAutoLoad = shouldAutoLoadNextPage( + hasMoreData: _currentDisplayHasMore, + isLoadingMore: _isLoadingMore, + hasClients: hasClients, + maxScrollExtent: maxScrollExtent, + ); + + if (!shouldAutoLoad) { + return; + } + + await _loadNextPageForCurrentDisplayType(); + }); + } + /// Initialise toutes les données nécessaires Future _initializeData() async { try { @@ -213,7 +263,8 @@ class _EquipmentSelectionDialogState extends State { ); // Charger le cache des enfants - _containerEquipmentCache[containerId] = List.from(container.equipmentIds); + _containerEquipmentCache[containerId] = + List.from(container.equipmentIds); // Ajouter les enfants comme sélectionnés aussi for (var equipmentId in container.equipmentIds) { @@ -228,7 +279,9 @@ class _EquipmentSelectionDialogState extends State { } } } catch (e) { - DebugLog.error('[EquipmentSelectionDialog] Error loading already assigned containers', e); + DebugLog.error( + '[EquipmentSelectionDialog] Error loading already assigned containers', + e); } } @@ -238,7 +291,8 @@ class _EquipmentSelectionDialogState extends State { _selectedItems = initialSelection; }); _selectionChangeNotifier.value++; - DebugLog.info('[EquipmentSelectionDialog] Initialized with ${_selectedItems.length} already assigned items'); + DebugLog.info( + '[EquipmentSelectionDialog] Initialized with ${_selectedItems.length} already assigned items'); } } @@ -253,18 +307,18 @@ class _EquipmentSelectionDialogState extends State { limit: 25, startAfter: _lastEquipmentId, searchQuery: _searchQuery.isNotEmpty ? _searchQuery : null, - category: _selectedCategory != null ? equipmentCategoryToString(_selectedCategory!) : null, + category: _selectedCategory != null + ? equipmentCategoryToString(_selectedCategory!) + : null, sortBy: 'id', sortOrder: 'asc', ); - final newEquipments = (result['equipments'] as List) - .map((data) { - final map = data as Map; - final id = map['id'] as String; - return EquipmentModel.fromMap(map, id); - }) - .toList(); + final newEquipments = (result['equipments'] as List).map((data) { + final map = data as Map; + final id = map['id'] as String; + return EquipmentModel.fromMap(map, id); + }).toList(); if (mounted) { setState(() { @@ -274,13 +328,18 @@ class _EquipmentSelectionDialogState extends State { _isLoadingMore = false; }); - DebugLog.info('[EquipmentSelectionDialog] Loaded ${newEquipments.length} equipments, total: ${_paginatedEquipments.length}, hasMore: $_hasMoreEquipments'); + DebugLog.info( + '[EquipmentSelectionDialog] Loaded ${newEquipments.length} equipments, total: ${_paginatedEquipments.length}, hasMore: $_hasMoreEquipments'); // Charger les quantités pour les consommables/câbles de cette page await _loadAvailableQuantities(newEquipments); + + // Si la liste ne peut pas scroller, précharger la page suivante. + _scheduleAutoLoadIfListNotScrollable(); } } catch (e) { - DebugLog.error('[EquipmentSelectionDialog] Error loading equipment page', e); + DebugLog.error( + '[EquipmentSelectionDialog] Error loading equipment page', e); if (mounted) { setState(() => _isLoadingMore = false); } @@ -305,7 +364,8 @@ class _EquipmentSelectionDialogState extends State { final containersData = result['containers'] as List; - DebugLog.info('[EquipmentSelectionDialog] Raw containers data received: ${containersData.length} containers'); + DebugLog.info( + '[EquipmentSelectionDialog] Raw containers data received: ${containersData.length} containers'); // D'abord, extraire TOUS les équipements final List allEquipmentsToCache = []; @@ -316,29 +376,30 @@ class _EquipmentSelectionDialogState extends State { // Debug: vérifier si le champ 'equipment' existe final hasEquipmentField = map.containsKey('equipment'); final equipmentData = map['equipment']; - DebugLog.info('[EquipmentSelectionDialog] Container $containerId: hasEquipmentField=$hasEquipmentField, equipmentData type=${equipmentData?.runtimeType}, count=${equipmentData is List ? equipmentData.length : 0}'); + DebugLog.info( + '[EquipmentSelectionDialog] Container $containerId: hasEquipmentField=$hasEquipmentField, equipmentData type=${equipmentData?.runtimeType}, count=${equipmentData is List ? equipmentData.length : 0}'); - final equipmentList = (map['equipment'] as List?) - ?.map((eqData) { - final eqMap = eqData as Map; - final eqId = eqMap['id'] as String; - DebugLog.info('[EquipmentSelectionDialog] - Equipment found: $eqId'); - return EquipmentModel.fromMap(eqMap, eqId); - }) - .toList() ?? []; + final equipmentList = + (map['equipment'] as List?)?.map((eqData) { + final eqMap = eqData as Map; + final eqId = eqMap['id'] as String; + DebugLog.info( + '[EquipmentSelectionDialog] - Equipment found: $eqId'); + return EquipmentModel.fromMap(eqMap, eqId); + }).toList() ?? + []; allEquipmentsToCache.addAll(equipmentList); } - DebugLog.info('[EquipmentSelectionDialog] Total equipments extracted from containers: ${allEquipmentsToCache.length}'); + DebugLog.info( + '[EquipmentSelectionDialog] Total equipments extracted from containers: ${allEquipmentsToCache.length}'); // Créer les containers - final newContainers = containersData - .map((data) { - final map = data as Map; - final id = map['id'] as String; - return ContainerModel.fromMap(map, id); - }) - .toList(); + final newContainers = containersData.map((data) { + final map = data as Map; + final id = map['id'] as String; + return ContainerModel.fromMap(map, id); + }).toList(); if (mounted) { setState(() { @@ -355,14 +416,20 @@ class _EquipmentSelectionDialogState extends State { _isLoadingMore = false; }); - DebugLog.info('[EquipmentSelectionDialog] Loaded ${newContainers.length} containers, total: ${_paginatedContainers.length}, hasMore: $_hasMoreContainers'); - DebugLog.info('[EquipmentSelectionDialog] Cached ${allEquipmentsToCache.length} equipment(s) from containers, total cache: ${_cachedEquipment.length}'); + DebugLog.info( + '[EquipmentSelectionDialog] Loaded ${newContainers.length} containers, total: ${_paginatedContainers.length}, hasMore: $_hasMoreContainers'); + DebugLog.info( + '[EquipmentSelectionDialog] Cached ${allEquipmentsToCache.length} equipment(s) from containers, total cache: ${_cachedEquipment.length}'); // Mettre à jour les statuts de conflit pour les nouveaux containers await _updateContainerConflictStatus(); + + // Si la liste ne peut pas scroller, précharger la page suivante. + _scheduleAutoLoadIfListNotScrollable(); } } catch (e) { - DebugLog.error('[EquipmentSelectionDialog] Error loading container page', e); + DebugLog.error( + '[EquipmentSelectionDialog] Error loading container page', e); if (mounted) { setState(() => _isLoadingMore = false); } @@ -427,7 +494,8 @@ class _EquipmentSelectionDialogState extends State { setState(() => _isLoadingConflicts = true); try { - DebugLog.info('[EquipmentSelectionDialog] Loading conflicts (optimized batch method)...'); + DebugLog.info( + '[EquipmentSelectionDialog] Loading conflicts (optimized batch method)...'); final startTime = DateTime.now(); @@ -443,22 +511,31 @@ class _EquipmentSelectionDialogState extends State { final endTime = DateTime.now(); final duration = endTime.difference(startTime); - DebugLog.info('[EquipmentSelectionDialog] Conflicts loaded in ${duration.inMilliseconds}ms'); + DebugLog.info( + '[EquipmentSelectionDialog] Conflicts loaded in ${duration.inMilliseconds}ms'); // Extraire les IDs en conflit - final conflictingEquipmentIds = (result['conflictingEquipmentIds'] as List?) - ?.map((e) => e.toString()) - .toSet() ?? {}; + final conflictingEquipmentIds = + (result['conflictingEquipmentIds'] as List?) + ?.map((e) => e.toString()) + .toSet() ?? + {}; - final conflictingContainerIds = (result['conflictingContainerIds'] as List?) - ?.map((e) => e.toString()) - .toSet() ?? {}; + final conflictingContainerIds = + (result['conflictingContainerIds'] as List?) + ?.map((e) => e.toString()) + .toSet() ?? + {}; - final conflictDetails = result['conflictDetails'] as Map? ?? {}; - final equipmentQuantities = result['equipmentQuantities'] as Map? ?? {}; + final conflictDetails = + result['conflictDetails'] as Map? ?? {}; + final equipmentQuantities = + result['equipmentQuantities'] as Map? ?? {}; - DebugLog.info('[EquipmentSelectionDialog] Found ${conflictingEquipmentIds.length} equipment(s) and ${conflictingContainerIds.length} container(s) in conflict'); - DebugLog.info('[EquipmentSelectionDialog] Quantity info for ${equipmentQuantities.length} equipment(s)'); + DebugLog.info( + '[EquipmentSelectionDialog] Found ${conflictingEquipmentIds.length} equipment(s) and ${conflictingContainerIds.length} container(s) in conflict'); + DebugLog.info( + '[EquipmentSelectionDialog] Quantity info for ${equipmentQuantities.length} equipment(s)'); if (mounted) { setState(() { @@ -478,7 +555,8 @@ class _EquipmentSelectionDialogState extends State { id: conflictMap['eventId'] as String, name: conflictMap['eventName'] as String, description: '', - startDateTime: DateTime.parse(conflictMap['startDate'] as String), + startDateTime: + DateTime.parse(conflictMap['startDate'] as String), endDateTime: DateTime.parse(conflictMap['endDate'] as String), basePrice: 0.0, installationTime: 0, @@ -497,11 +575,18 @@ class _EquipmentSelectionDialogState extends State { ); // Calculer les jours de chevauchement - final conflictStart = DateTime.parse(conflictMap['startDate'] as String); - final conflictEnd = DateTime.parse(conflictMap['endDate'] as String); - final overlapStart = widget.startDate.isAfter(conflictStart) ? widget.startDate : conflictStart; - final overlapEnd = widget.endDate.isBefore(conflictEnd) ? widget.endDate : conflictEnd; - final overlapDays = overlapEnd.difference(overlapStart).inDays + 1; + final conflictStart = + DateTime.parse(conflictMap['startDate'] as String); + final conflictEnd = + DateTime.parse(conflictMap['endDate'] as String); + final overlapStart = widget.startDate.isAfter(conflictStart) + ? widget.startDate + : conflictStart; + final overlapEnd = widget.endDate.isBefore(conflictEnd) + ? widget.endDate + : conflictEnd; + final overlapDays = + overlapEnd.difference(overlapStart).inDays + 1; return AvailabilityConflict( equipmentId: itemId, @@ -517,7 +602,6 @@ class _EquipmentSelectionDialogState extends State { // Mettre à jour les statuts de conteneurs await _updateContainerConflictStatus(); - } catch (e) { DebugLog.error('[EquipmentSelectionDialog] Error loading conflicts', e); } finally { @@ -548,9 +632,10 @@ class _EquipmentSelectionDialogState extends State { .toList(); if (conflictingChildren.isNotEmpty) { - final status = conflictingChildren.length == container.equipmentIds.length - ? ContainerConflictStatus.complete - : ContainerConflictStatus.partial; + final status = + conflictingChildren.length == container.equipmentIds.length + ? ContainerConflictStatus.complete + : ContainerConflictStatus.partial; _containerConflicts[container.id] = ContainerConflictInfo( status: status, @@ -558,18 +643,21 @@ class _EquipmentSelectionDialogState extends State { totalChildren: container.equipmentIds.length, ); - DebugLog.info('[EquipmentSelectionDialog] Container ${container.id}: ${status.name} conflict (${conflictingChildren.length}/${container.equipmentIds.length} children)'); + DebugLog.info( + '[EquipmentSelectionDialog] Container ${container.id}: ${status.name} conflict (${conflictingChildren.length}/${container.equipmentIds.length} children)'); } } - DebugLog.info('[EquipmentSelectionDialog] Total containers with conflicts: ${_containerConflicts.length}'); + DebugLog.info( + '[EquipmentSelectionDialog] Total containers with conflicts: ${_containerConflicts.length}'); // Déclencher un rebuild pour afficher les changements visuels if (mounted) { setState(() {}); } } catch (e) { - DebugLog.error('[EquipmentSelectionDialog] Error updating container conflicts', e); + DebugLog.error( + '[EquipmentSelectionDialog] Error updating container conflicts', e); } } @@ -587,7 +675,8 @@ class _EquipmentSelectionDialogState extends State { /// Construit l'affichage des quantités pour les câbles/consommables Widget _buildQuantityInfo(EquipmentModel equipment) { - final quantityInfo = _equipmentQuantities[equipment.id] as Map?; + final quantityInfo = + _equipmentQuantities[equipment.id] as Map?; if (quantityInfo == null) { // Pas d'info de quantité, utiliser l'ancien système (availableQuantities) @@ -642,7 +731,8 @@ class _EquipmentSelectionDialogState extends State { } /// Affiche un dialog avec les détails des réservations de quantité - Future _showQuantityDetailsDialog(EquipmentModel equipment, Map quantityInfo) async { + Future _showQuantityDetailsDialog( + EquipmentModel equipment, Map quantityInfo) async { final reservations = quantityInfo['reservations'] as List? ?? []; final totalQuantity = quantityInfo['totalQuantity'] as int? ?? 0; final availableQuantity = quantityInfo['availableQuantity'] as int? ?? 0; @@ -713,7 +803,9 @@ class _EquipmentSelectionDialogState extends State { '$availableQuantity $unit', style: TextStyle( fontWeight: FontWeight.bold, - color: availableQuantity > 0 ? Colors.green.shade700 : Colors.red, + color: availableQuantity > 0 + ? Colors.green.shade700 + : Colors.red, fontSize: 16, ), ), @@ -740,10 +832,12 @@ class _EquipmentSelectionDialogState extends State { child: Column( children: reservations.map((reservation) { final res = reservation as Map; - final eventName = res['eventName'] as String? ?? 'Événement inconnu'; + 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?; + final viaContainerName = + res['viaContainerName'] as String?; return Card( margin: const EdgeInsets.only(bottom: 8), @@ -762,7 +856,8 @@ class _EquipmentSelectionDialogState extends State { ), title: Text( eventName, - style: const TextStyle(fontWeight: FontWeight.w500), + style: + const TextStyle(fontWeight: FontWeight.w500), ), subtitle: viaContainer != null ? Text( @@ -813,9 +908,12 @@ class _EquipmentSelectionDialogState extends State { return _recommendedContainers[equipmentId] ?? []; } - 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 - if (!force && type == SelectionType.equipment && _conflictingEquipmentIds.contains(id)) { + if (!force && + type == SelectionType.equipment && + _conflictingEquipmentIds.contains(id)) { // Demander confirmation pour forcer final shouldForce = await _showForceConfirmationDialog(id); if (shouldForce == true) { @@ -827,7 +925,8 @@ class _EquipmentSelectionDialogState extends State { if (_selectedItems.containsKey(id)) { // Désélectionner DebugLog.info('[EquipmentSelectionDialog] Deselecting $type: $id'); - DebugLog.info('[EquipmentSelectionDialog] Before deselection, _selectedItems count: ${_selectedItems.length}'); + DebugLog.info( + '[EquipmentSelectionDialog] Before deselection, _selectedItems count: ${_selectedItems.length}'); if (type == SelectionType.container) { // Si c'est un conteneur, désélectionner d'abord ses enfants de manière asynchrone @@ -841,8 +940,10 @@ class _EquipmentSelectionDialogState extends State { }); } - DebugLog.info('[EquipmentSelectionDialog] After deselection, _selectedItems count: ${_selectedItems.length}'); - DebugLog.info('[EquipmentSelectionDialog] Remaining items: ${_selectedItems.keys.toList()}'); + DebugLog.info( + '[EquipmentSelectionDialog] After deselection, _selectedItems count: ${_selectedItems.length}'); + DebugLog.info( + '[EquipmentSelectionDialog] Remaining items: ${_selectedItems.keys.toList()}'); // Notifier le changement _selectionChangeNotifier.value++; @@ -881,7 +982,8 @@ class _EquipmentSelectionDialogState extends State { Future _selectContainerChildren(String containerId) async { try { // Chercher le container dans les données paginées ou le cache - final container = [..._paginatedContainers, ..._cachedContainers].firstWhere( + final container = + [..._paginatedContainers, ..._cachedContainers].firstWhere( (c) => c.id == containerId, orElse: () => ContainerModel( id: containerId, @@ -927,7 +1029,8 @@ class _EquipmentSelectionDialogState extends State { } } - DebugLog.info('[EquipmentSelectionDialog] Selected container $containerId with ${container.equipmentIds.length} children'); + DebugLog.info( + '[EquipmentSelectionDialog] Selected container $containerId with ${container.equipmentIds.length} children'); } catch (e) { DebugLog.error('Error selecting container children', e); } @@ -937,7 +1040,8 @@ class _EquipmentSelectionDialogState extends State { Future _deselectContainerChildren(String containerId) async { try { // Chercher le container dans les données paginées ou le cache - final container = [..._paginatedContainers, ..._cachedContainers].firstWhere( + final container = + [..._paginatedContainers, ..._cachedContainers].firstWhere( (c) => c.id == containerId, orElse: () => ContainerModel( id: containerId, @@ -965,7 +1069,8 @@ class _EquipmentSelectionDialogState extends State { }); } - DebugLog.info('[EquipmentSelectionDialog] Deselected container $containerId and ${container.equipmentIds.length} children'); + DebugLog.info( + '[EquipmentSelectionDialog] Deselected container $containerId and ${container.equipmentIds.length} children'); } catch (e) { DebugLog.error('Error deselecting container children', e); } @@ -989,33 +1094,36 @@ class _EquipmentSelectionDialogState extends State { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('Cet équipement est déjà utilisé sur ${conflicts.length} événement(s) :'), + Text( + 'Cet équipement est déjà utilisé sur ${conflicts.length} événement(s) :'), const SizedBox(height: 12), ...conflicts.map((conflict) => Padding( - padding: const EdgeInsets.only(bottom: 8), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Icon(Icons.event, size: 16, color: Colors.orange), - const SizedBox(width: 8), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - conflict.conflictingEvent.name, - style: const TextStyle(fontWeight: FontWeight.bold), + padding: const EdgeInsets.only(bottom: 8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Icon(Icons.event, size: 16, color: Colors.orange), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + conflict.conflictingEvent.name, + style: + const TextStyle(fontWeight: FontWeight.bold), + ), + Text( + 'Chevauchement : ${conflict.overlapDays} jour(s)', + style: const TextStyle( + fontSize: 12, color: Colors.grey), + ), + ], ), - Text( - 'Chevauchement : ${conflict.overlapDays} jour(s)', - style: const TextStyle(fontSize: 12, color: Colors.grey), - ), - ], - ), + ), + ], ), - ], - ), - )), + )), ], ), actions: [ @@ -1036,7 +1144,6 @@ class _EquipmentSelectionDialogState extends State { ); } - @override Widget build(BuildContext context) { final screenSize = MediaQuery.of(context).size; @@ -1161,7 +1268,8 @@ class _EquipmentSelectionDialogState extends State { border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), ), - contentPadding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), + contentPadding: + const EdgeInsets.symmetric(vertical: 12, horizontal: 16), ), onChanged: (value) { setState(() => _searchQuery = value.toLowerCase()); @@ -1200,6 +1308,9 @@ class _EquipmentSelectionDialogState extends State { setState(() { _showConflictingItems = value ?? false; }); + + // Des filtres plus stricts peuvent masquer la page courante. + _scheduleAutoLoadIfListNotScrollable(); }, activeColor: AppColors.rouge, ), @@ -1209,7 +1320,8 @@ class _EquipmentSelectionDialogState extends State { ), const SizedBox(width: 8), Tooltip( - message: 'Afficher les équipements et boîtes qui sont déjà utilisés durant ces dates', + message: + 'Afficher les équipements et boîtes qui sont déjà utilisés durant ces dates', child: Icon( Icons.info_outline, size: 18, @@ -1242,7 +1354,9 @@ class _EquipmentSelectionDialogState extends State { }, selectedColor: AppColors.rouge, labelStyle: TextStyle( - color: _displayType == SelectionType.equipment ? Colors.white : Colors.black87, + color: _displayType == SelectionType.equipment + ? Colors.white + : Colors.black87, ), ), const SizedBox(width: 8), @@ -1259,7 +1373,9 @@ class _EquipmentSelectionDialogState extends State { }, selectedColor: AppColors.rouge, labelStyle: TextStyle( - color: _displayType == SelectionType.container ? Colors.white : Colors.black87, + color: _displayType == SelectionType.container + ? Colors.white + : Colors.black87, ), ), ], @@ -1323,14 +1439,16 @@ class _EquipmentSelectionDialogState extends State { if (_displayType == SelectionType.equipment) { // Filtrer côté client pour "Afficher équipements déjà utilisés" final filteredEquipments = _paginatedEquipments.where((eq) { - if (!_showConflictingItems && _conflictingEquipmentIds.contains(eq.id)) { + if (!_showConflictingItems && + _conflictingEquipmentIds.contains(eq.id)) { return false; } return true; }).toList(); itemWidgets = filteredEquipments.map((equipment) { - return _buildEquipmentCard(equipment, key: ValueKey('equipment_${equipment.id}')); + return _buildEquipmentCard(equipment, + key: ValueKey('equipment_${equipment.id}')); }).toList(); } else { // Containers @@ -1354,7 +1472,8 @@ class _EquipmentSelectionDialogState extends State { }).toList(); itemWidgets = filteredContainers.map((container) { - return _buildContainerCard(container, key: ValueKey('container_${container.id}')); + return _buildContainerCard(container, + key: ValueKey('container_${container.id}')); }).toList(); } @@ -1364,8 +1483,12 @@ class _EquipmentSelectionDialogState extends State { children: [ // Header _buildSectionHeader( - _displayType == SelectionType.equipment ? 'Équipements' : 'Containers', - _displayType == SelectionType.equipment ? Icons.inventory_2 : Icons.inventory, + _displayType == SelectionType.equipment + ? 'Équipements' + : 'Containers', + _displayType == SelectionType.equipment + ? Icons.inventory_2 + : Icons.inventory, itemWidgets.length, ), const SizedBox(height: 12), @@ -1383,7 +1506,10 @@ class _EquipmentSelectionDialogState extends State { ), // Message si fin de liste - if (!_isLoadingMore && !(_displayType == SelectionType.equipment ? _hasMoreEquipments : _hasMoreContainers)) + if (!_isLoadingMore && + !(_displayType == SelectionType.equipment + ? _hasMoreEquipments + : _hasMoreContainers)) Padding( padding: const EdgeInsets.all(16), child: Center( @@ -1401,11 +1527,13 @@ class _EquipmentSelectionDialogState extends State { padding: const EdgeInsets.all(32), child: Column( children: [ - Icon(Icons.search_off, size: 64, color: Colors.grey.shade400), + Icon(Icons.search_off, + size: 64, color: Colors.grey.shade400), const SizedBox(height: 16), Text( 'Aucun résultat trouvé', - style: TextStyle(fontSize: 16, color: Colors.grey.shade600), + style: TextStyle( + fontSize: 16, color: Colors.grey.shade600), ), ], ), @@ -1461,10 +1589,11 @@ class _EquipmentSelectionDialogState extends State { Widget _buildEquipmentCard(EquipmentModel equipment, {Key? key}) { final isSelected = _selectedItems.containsKey(equipment.id); final isConsumable = equipment.category == EquipmentCategory.consumable || - equipment.category == EquipmentCategory.cable; + equipment.category == EquipmentCategory.cable; final availableQty = _availableQuantities[equipment.id]; final selectedItem = _selectedItems[equipment.id]; - final hasConflict = _conflictingEquipmentIds.contains(equipment.id); // CORRECTION ICI ! + final hasConflict = + _conflictingEquipmentIds.contains(equipment.id); // CORRECTION ICI ! final conflictDetails = _getConflictDetailsFor(equipment.id); // Bloquer la sélection si en conflit et non forcé @@ -1503,239 +1632,256 @@ class _EquipmentSelectionDialogState extends State { child: Padding( padding: const EdgeInsets.all(16), child: Column( - children: [ - Row( - children: [ - // Checkbox - Checkbox( - value: isSelected, - onChanged: canSelect - ? (value) => _toggleSelection( - equipment.id, - equipment.id, - SelectionType.equipment, - maxQuantity: availableQty, - ) - : null, - activeColor: AppColors.rouge, - ), + children: [ + Row( + children: [ + // Checkbox + Checkbox( + value: isSelected, + onChanged: canSelect + ? (value) => _toggleSelection( + equipment.id, + equipment.id, + SelectionType.equipment, + maxQuantity: availableQty, + ) + : null, + activeColor: AppColors.rouge, + ), - const SizedBox(width: 12), + const SizedBox(width: 12), - // Icône - equipment.category.getIcon(size: 32, color: equipment.category.color), + // Icône + equipment.category + .getIcon(size: 32, color: equipment.category.color), - const SizedBox(width: 16), + const SizedBox(width: 16), - // Infos - Expanded( + // Infos + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + equipment.id, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + ), + if (hasConflict) + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Colors.orange.shade700, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.warning, + size: 14, color: Colors.white), + const SizedBox(width: 4), + Text( + 'Déjà utilisé', + style: const TextStyle( + color: Colors.white, + fontSize: 11, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ], + ), + if (equipment.brand != null || + equipment.model != null) + Text( + '${equipment.brand ?? ''} ${equipment.model ?? ''}' + .trim(), + style: TextStyle( + color: Colors.grey.shade700, + fontSize: 14, + ), + ), + // Affichage des boîtes parentes + if (_getParentContainers(equipment.id).isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: 4), + child: Wrap( + spacing: 4, + runSpacing: 4, + children: _getParentContainers(equipment.id) + .map((container) { + return Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: Colors.blue.shade50, + border: Border.all( + color: Colors.blue.shade300), + borderRadius: BorderRadius.circular(10), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.inventory, + size: 12, + color: Colors.blue.shade700), + const SizedBox(width: 4), + Text( + container.name, + style: TextStyle( + fontSize: 10, + color: Colors.blue.shade700, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ); + }).toList(), + ), + ), + if (isConsumable) + Padding( + padding: const EdgeInsets.only(top: 4), + child: _buildQuantityInfo(equipment), + ), + ], + ), + ), + + // Sélecteur de quantité pour consommables (toujours affiché) + if (isConsumable && availableQty != null) + _buildQuantitySelector( + equipment.id, + selectedItem ?? + SelectedItem( + id: equipment.id, + name: equipment.id, + type: SelectionType.equipment, + quantity: 0, // Quantité 0 si non sélectionné + ), + availableQty, + isSelected: isSelected, // Passer l'état de sélection + ), + ], + ), + + // Affichage des conflits + if (hasConflict) + Container( + margin: const EdgeInsets.only(top: 12), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.orange.shade100, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.orange.shade300), + ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ - Expanded( - child: Text( - equipment.id, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - ), + Icon(Icons.info_outline, + size: 16, color: Colors.orange.shade900), + const SizedBox(width: 6), + Text( + 'Utilisé sur ${conflictDetails.length} événement(s) :', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: Colors.orange.shade900, ), ), - if (hasConflict) - Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: Colors.orange.shade700, - borderRadius: BorderRadius.circular(12), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.warning, size: 14, color: Colors.white), - const SizedBox(width: 4), - Text( - 'Déjà utilisé', - style: const TextStyle( - color: Colors.white, - fontSize: 11, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ), ], ), - if (equipment.brand != null || equipment.model != null) - Text( - '${equipment.brand ?? ''} ${equipment.model ?? ''}'.trim(), - style: TextStyle( - color: Colors.grey.shade700, - fontSize: 14, - ), - ), - // Affichage des boîtes parentes - if (_getParentContainers(equipment.id).isNotEmpty) - Padding( - padding: const EdgeInsets.only(top: 4), - child: Wrap( - spacing: 4, - runSpacing: 4, - children: _getParentContainers(equipment.id).map((container) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), - decoration: BoxDecoration( - color: Colors.blue.shade50, - border: Border.all(color: Colors.blue.shade300), - borderRadius: BorderRadius.circular(10), + const SizedBox(height: 6), + ...conflictDetails.take(2).map((detail) { + final eventName = detail['eventName'] as String? ?? + 'Événement inconnu'; + final viaContainer = + detail['viaContainer'] as String?; + final viaContainerName = + detail['viaContainerName'] as String?; + + return Padding( + padding: const EdgeInsets.only(left: 22, top: 4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '• $eventName', + style: TextStyle( + fontSize: 11, + color: Colors.orange.shade800, ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.inventory, size: 12, color: Colors.blue.shade700), - const SizedBox(width: 4), - Text( - container.name, - style: TextStyle( - fontSize: 10, - color: Colors.blue.shade700, - fontWeight: FontWeight.w500, - ), + ), + 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, ), - ], + ), ), - ); - }).toList(), + ], + ), + ); + }), + if (conflictDetails.length > 2) + Padding( + padding: const EdgeInsets.only(left: 22, top: 4), + child: Text( + '... et ${conflictDetails.length - 2} autre(s)', + style: TextStyle( + fontSize: 11, + color: Colors.orange.shade800, + fontStyle: FontStyle.italic, + ), ), ), - if (isConsumable) + if (!isSelected) Padding( - padding: const EdgeInsets.only(top: 4), - child: _buildQuantityInfo(equipment), + padding: const EdgeInsets.only(top: 8), + child: TextButton.icon( + onPressed: () => _toggleSelection( + equipment.id, + equipment.id, + SelectionType.equipment, + maxQuantity: availableQty, + force: true, + ), + icon: const Icon(Icons.warning, size: 16), + label: const Text('Forcer quand même', + style: TextStyle(fontSize: 12)), + style: TextButton.styleFrom( + foregroundColor: Colors.orange.shade900, + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 4), + ), + ), ), ], ), ), - - // Sélecteur de quantité pour consommables (toujours affiché) - if (isConsumable && availableQty != null) - _buildQuantitySelector( - equipment.id, - selectedItem ?? SelectedItem( - id: equipment.id, - name: equipment.id, - type: SelectionType.equipment, - quantity: 0, // Quantité 0 si non sélectionné - ), - availableQty, - isSelected: isSelected, // Passer l'état de sélection - ), - ], - ), - - // Affichage des conflits - if (hasConflict) - Container( - margin: const EdgeInsets.only(top: 12), - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.orange.shade100, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.orange.shade300), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon(Icons.info_outline, size: 16, color: Colors.orange.shade900), - const SizedBox(width: 6), - Text( - 'Utilisé sur ${conflictDetails.length} événement(s) :', - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: Colors.orange.shade900, - ), - ), - ], - ), - const SizedBox(height: 6), - ...conflictDetails.take(2).map((detail) { - final eventName = detail['eventName'] as String? ?? 'Événement inconnu'; - final viaContainer = detail['viaContainer'] as String?; - final viaContainerName = detail['viaContainerName'] as String?; - - return Padding( - padding: const EdgeInsets.only(left: 22, top: 4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '• $eventName', - style: TextStyle( - fontSize: 11, - color: Colors.orange.shade800, - ), - ), - if (viaContainer != null) - Padding( - padding: const EdgeInsets.only(left: 8), - child: Text( - 'via ${viaContainerName ?? viaContainer}', - style: TextStyle( - fontSize: 10, - color: Colors.grey.shade600, - fontStyle: FontStyle.italic, - ), - ), - ), - ], - ), - ); - }), - if (conflictDetails.length > 2) - Padding( - padding: const EdgeInsets.only(left: 22, top: 4), - child: Text( - '... et ${conflictDetails.length - 2} autre(s)', - style: TextStyle( - fontSize: 11, - color: Colors.orange.shade800, - fontStyle: FontStyle.italic, - ), - ), - ), - if (!isSelected) - Padding( - padding: const EdgeInsets.only(top: 8), - child: TextButton.icon( - onPressed: () => _toggleSelection( - equipment.id, - equipment.id, - SelectionType.equipment, - maxQuantity: availableQty, - force: true, - ), - icon: const Icon(Icons.warning, size: 16), - label: const Text('Forcer quand même', style: TextStyle(fontSize: 12)), - style: TextButton.styleFrom( - foregroundColor: Colors.orange.shade900, - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - ), - ), - ), - ], - ), - ), - ], + ], + ), ), ), ), ), - ), ); } @@ -1759,14 +1905,18 @@ class _EquipmentSelectionDialogState extends State { ? () { if (mounted) { setState(() { - _selectedItems[equipmentId] = selectedItem.copyWith(quantity: selectedItem.quantity - 1); + _selectedItems[equipmentId] = selectedItem.copyWith( + quantity: selectedItem.quantity - 1); }); - _selectionChangeNotifier.value++; // Notifier le changement + _selectionChangeNotifier + .value++; // Notifier le changement } } : null, iconSize: 20, - color: isSelected && selectedItem.quantity > 1 ? AppColors.rouge : Colors.grey, + color: isSelected && selectedItem.quantity > 1 + ? AppColors.rouge + : Colors.grey, ), Expanded( child: Text( @@ -1780,7 +1930,8 @@ class _EquipmentSelectionDialogState extends State { ), IconButton( icon: const Icon(Icons.add_circle_outline), - onPressed: (isSelected && selectedItem.quantity < maxQuantity) || !isSelected + onPressed: (isSelected && selectedItem.quantity < maxQuantity) || + !isSelected ? () { if (!isSelected) { // Premier clic : sélectionner avec quantité 1 @@ -1794,9 +1945,11 @@ class _EquipmentSelectionDialogState extends State { // 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 le changement + _selectionChangeNotifier + .value++; // Notifier le changement } } } @@ -1809,13 +1962,13 @@ class _EquipmentSelectionDialogState extends State { ); } - Widget _buildContainerCard(ContainerModel container, {Key? key}) { final isSelected = _selectedItems.containsKey(container.id); final isExpanded = _expandedContainers.contains(container.id); final conflictInfo = _containerConflicts[container.id]; final hasConflict = conflictInfo != null; - final isCompleteConflict = conflictInfo?.status == ContainerConflictStatus.complete; + final isCompleteConflict = + conflictInfo?.status == ContainerConflictStatus.complete; // Bloquer la sélection si tous les enfants sont en conflit (sauf si déjà sélectionné) final canSelect = !isCompleteConflict || isSelected; @@ -1824,240 +1977,263 @@ class _EquipmentSelectionDialogState extends State { key: key, child: Card( margin: const EdgeInsets.only(bottom: 12), - elevation: isSelected ? 4 : 1, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - side: isSelected - ? const BorderSide(color: AppColors.rouge, width: 2) - : hasConflict - ? BorderSide( - color: isCompleteConflict ? Colors.red.shade300 : Colors.orange.shade300, - width: 1, - ) - : BorderSide.none, - ), - child: Container( - decoration: hasConflict && !isSelected - ? BoxDecoration( - color: isCompleteConflict ? Colors.red.shade50 : Colors.orange.shade50, + elevation: isSelected ? 4 : 1, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + side: isSelected + ? const BorderSide(color: AppColors.rouge, width: 2) + : hasConflict + ? BorderSide( + color: isCompleteConflict + ? Colors.red.shade300 + : Colors.orange.shade300, + width: 1, + ) + : BorderSide.none, + ), + child: Container( + decoration: hasConflict && !isSelected + ? BoxDecoration( + color: isCompleteConflict + ? Colors.red.shade50 + : Colors.orange.shade50, + borderRadius: BorderRadius.circular(8), + ) + : null, + child: Column( + children: [ + InkWell( + onTap: canSelect + ? () => _toggleSelection( + container.id, + container.name, + SelectionType.container, + ) + : null, borderRadius: BorderRadius.circular(8), - ) - : null, - child: Column( - children: [ - InkWell( - onTap: canSelect - ? () => _toggleSelection( - container.id, - container.name, - SelectionType.container, - ) - : null, - borderRadius: BorderRadius.circular(8), - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - children: [ - Row( - children: [ - // Checkbox - Checkbox( - value: isSelected, - onChanged: canSelect - ? (value) => _toggleSelection( - container.id, - container.name, - SelectionType.container, - ) - : null, - activeColor: AppColors.rouge, - ), - - const SizedBox(width: 12), - - // Icône du conteneur - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: AppColors.rouge.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(8), - ), - child: container.type.getIcon(size: 28, color: AppColors.rouge), - ), - - const SizedBox(width: 16), - - // Infos principales - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Expanded( - child: Text( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + Row( + children: [ + // Checkbox + Checkbox( + value: isSelected, + onChanged: canSelect + ? (value) => _toggleSelection( container.id, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, + container.name, + SelectionType.container, + ) + : null, + activeColor: AppColors.rouge, + ), + + const SizedBox(width: 12), + + // Icône du conteneur + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: AppColors.rouge.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + ), + child: container.type + .getIcon(size: 28, color: AppColors.rouge), + ), + + const SizedBox(width: 16), + + // Infos principales + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + container.id, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), ), ), - ), - // Badge de statut de conflit - if (hasConflict) - Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - decoration: BoxDecoration( - color: isCompleteConflict - ? Colors.red.shade700 - : Colors.orange.shade700, - borderRadius: BorderRadius.circular(12), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - isCompleteConflict ? Icons.block : Icons.warning, - size: 14, - color: Colors.white, - ), - const SizedBox(width: 4), - Text( - isCompleteConflict ? 'Indisponible' : 'Partiellement utilisée', - style: const TextStyle( + // Badge de statut de conflit + if (hasConflict) + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: isCompleteConflict + ? Colors.red.shade700 + : Colors.orange.shade700, + borderRadius: + BorderRadius.circular(12), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + isCompleteConflict + ? Icons.block + : Icons.warning, + size: 14, color: Colors.white, - fontSize: 11, - fontWeight: FontWeight.bold, ), - ), - ], + const SizedBox(width: 4), + Text( + isCompleteConflict + ? 'Indisponible' + : 'Partiellement utilisée', + style: const TextStyle( + color: Colors.white, + fontSize: 11, + fontWeight: FontWeight.bold, + ), + ), + ], + ), ), - ), - ], - ), - const SizedBox(height: 4), - Text( - container.name, - style: TextStyle( - color: Colors.grey.shade700, - fontSize: 14, + ], ), - ), - const SizedBox(height: 4), - Row( - children: [ - Icon( - Icons.inventory_2, - size: 14, - color: Colors.blue.shade700, + const SizedBox(height: 4), + Text( + container.name, + style: TextStyle( + color: Colors.grey.shade700, + fontSize: 14, ), - const SizedBox(width: 4), - Text( - '${container.itemCount} équipement(s)', - style: const TextStyle( - fontSize: 13, - fontWeight: FontWeight.w500, - color: Colors.blue, - ), - ), - if (hasConflict) ...[ - const SizedBox(width: 8), + ), + const SizedBox(height: 4), + Row( + children: [ Icon( - Icons.warning, + Icons.inventory_2, size: 14, - color: isCompleteConflict ? Colors.red.shade700 : Colors.orange.shade700, + color: Colors.blue.shade700, ), const SizedBox(width: 4), Text( - conflictInfo.description, - style: TextStyle( - fontSize: 11, - color: isCompleteConflict ? Colors.red.shade700 : Colors.orange.shade700, + '${container.itemCount} équipement(s)', + style: const TextStyle( + fontSize: 13, fontWeight: FontWeight.w500, + color: Colors.blue, ), ), + if (hasConflict) ...[ + const SizedBox(width: 8), + Icon( + Icons.warning, + size: 14, + color: isCompleteConflict + ? Colors.red.shade700 + : Colors.orange.shade700, + ), + const SizedBox(width: 4), + Text( + conflictInfo.description, + style: TextStyle( + fontSize: 11, + color: isCompleteConflict + ? Colors.red.shade700 + : Colors.orange.shade700, + fontWeight: FontWeight.w500, + ), + ), + ], ], - ], + ), + ], + ), + ), + + // Bouton pour déplier/replier + IconButton( + icon: Icon( + isExpanded + ? Icons.expand_less + : Icons.expand_more, + color: AppColors.rouge, + ), + onPressed: () { + if (isExpanded) { + _expandedContainers.remove(container.id); + } else { + _expandedContainers.add(container.id); + } + _selectionChangeNotifier + .value++; // Notifier sans rebuild complet + }, + tooltip: isExpanded ? 'Replier' : 'Voir le contenu', + ), + ], + ), + + // Avertissement pour conteneur complètement indisponible + if (isCompleteConflict && !isSelected) + Container( + margin: const EdgeInsets.only(top: 12), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.red.shade100, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.red.shade300), + ), + child: Row( + children: [ + Icon(Icons.block, + size: 20, color: Colors.red.shade900), + const SizedBox(width: 8), + Expanded( + child: Text( + 'Cette boîte ne peut pas être sélectionnée car tous ses équipements sont déjà utilisés.', + style: TextStyle( + fontSize: 12, + color: Colors.red.shade900, + fontWeight: FontWeight.w500, + ), + ), ), ], ), ), - - // Bouton pour déplier/replier - IconButton( - icon: Icon( - isExpanded ? Icons.expand_less : Icons.expand_more, - color: AppColors.rouge, - ), - onPressed: () { - if (isExpanded) { - _expandedContainers.remove(container.id); - } else { - _expandedContainers.add(container.id); - } - _selectionChangeNotifier.value++; // Notifier sans rebuild complet - }, - tooltip: isExpanded ? 'Replier' : 'Voir le contenu', - ), - ], - ), - - // Avertissement pour conteneur complètement indisponible - if (isCompleteConflict && !isSelected) - Container( - margin: const EdgeInsets.only(top: 12), - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Colors.red.shade100, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.red.shade300), - ), - child: Row( - children: [ - Icon(Icons.block, size: 20, color: Colors.red.shade900), - const SizedBox(width: 8), - Expanded( - child: Text( - 'Cette boîte ne peut pas être sélectionnée car tous ses équipements sont déjà utilisés.', - style: TextStyle( - fontSize: 12, - color: Colors.red.shade900, - fontWeight: FontWeight.w500, - ), - ), - ), - ], - ), - ), - ], + ], + ), ), ), - ), - // Liste des enfants (si déplié) - if (isExpanded) - _buildContainerChildren(container, conflictInfo), - ], + // Liste des enfants (si déplié) + if (isExpanded) _buildContainerChildren(container, conflictInfo), + ], + ), ), ), - ), ); } /// Widget pour afficher les équipements enfants d'un conteneur - Widget _buildContainerChildren(ContainerModel container, ContainerConflictInfo? conflictInfo) { + Widget _buildContainerChildren( + ContainerModel container, ContainerConflictInfo? conflictInfo) { // Utiliser les équipements paginés et le cache final allEquipment = [..._paginatedEquipments, ..._cachedEquipment]; final childEquipments = allEquipment .where((eq) => container.equipmentIds.contains(eq.id)) .toList(); - DebugLog.info('[EquipmentSelectionDialog] Building container children for ${container.id}: ${container.equipmentIds.length} IDs, found ${childEquipments.length} equipment(s) in cache (total cache: ${_cachedEquipment.length})'); + DebugLog.info( + '[EquipmentSelectionDialog] Building container children for ${container.id}: ${container.equipmentIds.length} IDs, found ${childEquipments.length} equipment(s) in cache (total cache: ${_cachedEquipment.length})'); if (container.equipmentIds.isNotEmpty && childEquipments.isEmpty) { - DebugLog.error('[EquipmentSelectionDialog] Container ${container.id} has ${container.equipmentIds.length} equipment IDs but found 0 equipment in cache!'); - DebugLog.info('[EquipmentSelectionDialog] Looking for IDs: ${container.equipmentIds.take(5).join(", ")}...'); - DebugLog.info('[EquipmentSelectionDialog] Cache contains IDs: ${_cachedEquipment.take(5).map((e) => e.id).join(", ")}...'); + DebugLog.error( + '[EquipmentSelectionDialog] Container ${container.id} has ${container.equipmentIds.length} equipment IDs but found 0 equipment in cache!'); + DebugLog.info( + '[EquipmentSelectionDialog] Looking for IDs: ${container.equipmentIds.take(5).join(", ")}...'); + DebugLog.info( + '[EquipmentSelectionDialog] Cache contains IDs: ${_cachedEquipment.take(5).map((e) => e.id).join(", ")}...'); } if (childEquipments.isEmpty) { @@ -2105,94 +2281,98 @@ class _EquipmentSelectionDialogState extends State { ), const SizedBox(height: 12), ...childEquipments.map((eq) { - final hasConflict = _equipmentConflicts.containsKey(eq.id); - final conflicts = _equipmentConflicts[eq.id] ?? []; + final hasConflict = _equipmentConflicts.containsKey(eq.id); + final conflicts = _equipmentConflicts[eq.id] ?? []; - return Container( - margin: const EdgeInsets.only(bottom: 8), - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: hasConflict ? Colors.orange.shade50 : Colors.white, - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: hasConflict ? Colors.orange.shade300 : Colors.grey.shade300, - ), - ), - child: Row( - children: [ - // Flèche de hiérarchie - Icon( - Icons.subdirectory_arrow_right, - size: 16, - color: Colors.grey.shade600, + return Container( + margin: const EdgeInsets.only(bottom: 8), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: hasConflict ? Colors.orange.shade50 : Colors.white, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: hasConflict + ? Colors.orange.shade300 + : Colors.grey.shade300, + ), + ), + child: Row( + children: [ + // Flèche de hiérarchie + Icon( + Icons.subdirectory_arrow_right, + size: 16, + color: Colors.grey.shade600, + ), + const SizedBox(width: 8), + + // Icône de l'équipement + eq.category.getIcon(size: 20, color: eq.category.color), + const SizedBox(width: 12), + + // Nom de l'équipement + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + eq.id, + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + color: Colors.grey.shade800, ), - const SizedBox(width: 8), - - // Icône de l'équipement - eq.category.getIcon(size: 20, color: eq.category.color), - const SizedBox(width: 12), - - // Nom de l'équipement - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - eq.id, - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w500, - color: Colors.grey.shade800, - ), - ), - if (eq.brand != null || eq.model != null) - Text( - '${eq.brand ?? ''} ${eq.model ?? ''}'.trim(), - style: TextStyle( - fontSize: 11, - color: Colors.grey.shade600, - ), - ), - ], + ), + if (eq.brand != null || eq.model != null) + Text( + '${eq.brand ?? ''} ${eq.model ?? ''}'.trim(), + style: TextStyle( + fontSize: 11, + color: Colors.grey.shade600, ), ), + ], + ), + ), - // Indicateur de conflit - if (hasConflict) ...[ - const SizedBox(width: 8), - Tooltip( - message: 'Utilisé sur ${conflicts.length} événement(s)', - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3), - decoration: BoxDecoration( - color: Colors.orange.shade700, - borderRadius: BorderRadius.circular(10), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.warning, size: 12, color: Colors.white), - const SizedBox(width: 4), - Text( - '${conflicts.length}', - style: const TextStyle( - color: Colors.white, - fontSize: 10, - fontWeight: FontWeight.bold, - ), - ), - ], - ), + // Indicateur de conflit + if (hasConflict) ...[ + const SizedBox(width: 8), + Tooltip( + message: 'Utilisé sur ${conflicts.length} événement(s)', + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, vertical: 3), + decoration: BoxDecoration( + color: Colors.orange.shade700, + borderRadius: BorderRadius.circular(10), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.warning, + size: 12, color: Colors.white), + const SizedBox(width: 4), + Text( + '${conflicts.length}', + style: const TextStyle( + color: Colors.white, + fontSize: 10, + fontWeight: FontWeight.bold, ), ), ], - ], + ), ), - ); - }), + ), + ], ], ), ); + }), + ], + ), + ); } Widget _buildSelectionPanel() { @@ -2255,7 +2435,6 @@ class _EquipmentSelectionDialogState extends State { ], ), ), - Expanded( child: totalDisplayed == 0 ? const Center( @@ -2278,7 +2457,8 @@ class _EquipmentSelectionDialogState extends State { ), ), ), - ...selectedContainers.map((e) => _buildSelectedContainerTile(e.key, e.value)), + ...selectedContainers.map( + (e) => _buildSelectedContainerTile(e.key, e.value)), ], if (standaloneEquipmentCount > 0) ...[ Padding( @@ -2291,7 +2471,8 @@ class _EquipmentSelectionDialogState extends State { ), ), ), - ...selectedStandaloneEquipment.map((e) => _buildSelectedItemTile(e.key, e.value)), + ...selectedStandaloneEquipment + .map((e) => _buildSelectedItemTile(e.key, e.value)), ], ], ), @@ -2300,7 +2481,6 @@ class _EquipmentSelectionDialogState extends State { ); } - /// Récupère les IDs des équipements d'un conteneur (depuis le cache) List _getContainerEquipmentIds(String containerId) { // On doit récupérer le conteneur depuis le provider de manière synchrone @@ -2348,7 +2528,8 @@ class _EquipmentSelectionDialogState extends State { } else { _expandedContainers.add(id); } - _selectionChangeNotifier.value++; // Notifier sans rebuild complet + _selectionChangeNotifier + .value++; // Notifier sans rebuild complet }, ), IconButton( @@ -2385,7 +2566,8 @@ class _EquipmentSelectionDialogState extends State { style: TextStyle(fontSize: 12, color: Colors.grey.shade700), ), subtitle: item.quantity > 1 - ? Text('Qté: ${item.quantity}', style: const TextStyle(fontSize: 10)) + ? Text('Qté: ${item.quantity}', + style: const TextStyle(fontSize: 10)) : null, // PAS de bouton de suppression pour les enfants ), @@ -2520,7 +2702,8 @@ class _EquipmentSelectionDialogState extends State { style: ElevatedButton.styleFrom( backgroundColor: AppColors.rouge, foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 14), + padding: + const EdgeInsets.symmetric(horizontal: 32, vertical: 14), ), child: const Text('Valider la sélection'), ), @@ -2531,4 +2714,3 @@ class _EquipmentSelectionDialogState extends State { ); } } - diff --git a/em2rp/lib/views/widgets/event/equipment_selection_pagination.dart b/em2rp/lib/views/widgets/event/equipment_selection_pagination.dart new file mode 100644 index 0000000..fe97b6b --- /dev/null +++ b/em2rp/lib/views/widgets/event/equipment_selection_pagination.dart @@ -0,0 +1,13 @@ +bool shouldAutoLoadNextPage({ + required bool hasMoreData, + required bool isLoadingMore, + required bool hasClients, + required double maxScrollExtent, +}) { + if (!hasMoreData || isLoadingMore) { + return false; + } + + // If the list cannot scroll yet, preload the next page to avoid a truncated view. + return !hasClients || maxScrollExtent <= 0; +} diff --git a/em2rp/test/views/widgets/event/equipment_selection_pagination_test.dart b/em2rp/test/views/widgets/event/equipment_selection_pagination_test.dart new file mode 100644 index 0000000..6f7a563 --- /dev/null +++ b/em2rp/test/views/widgets/event/equipment_selection_pagination_test.dart @@ -0,0 +1,61 @@ +import 'package:em2rp/views/widgets/event/equipment_selection_pagination.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('shouldAutoLoadNextPage', () { + test('returns false when there is no more data', () { + final result = shouldAutoLoadNextPage( + hasMoreData: false, + isLoadingMore: false, + hasClients: true, + maxScrollExtent: 100, + ); + + expect(result, isFalse); + }); + + test('returns false while a page is already loading', () { + final result = shouldAutoLoadNextPage( + hasMoreData: true, + isLoadingMore: true, + hasClients: true, + maxScrollExtent: 0, + ); + + expect(result, isFalse); + }); + + test('returns true when list has no scroll client yet', () { + final result = shouldAutoLoadNextPage( + hasMoreData: true, + isLoadingMore: false, + hasClients: false, + maxScrollExtent: 0, + ); + + expect(result, isTrue); + }); + + test('returns true when list is not scrollable yet', () { + final result = shouldAutoLoadNextPage( + hasMoreData: true, + isLoadingMore: false, + hasClients: true, + maxScrollExtent: 0, + ); + + expect(result, isTrue); + }); + + test('returns false when list is scrollable', () { + final result = shouldAutoLoadNextPage( + hasMoreData: true, + isLoadingMore: false, + hasClients: true, + maxScrollExtent: 250, + ); + + expect(result, isFalse); + }); + }); +} diff --git a/em2rp/web/version.json b/em2rp/web/version.json index f2119f0..349c392 100644 --- a/em2rp/web/version.json +++ b/em2rp/web/version.json @@ -1,7 +1,7 @@ { - "version": "1.1.18", + "version": "1.1.19", "updateUrl": "https://app.em2events.fr", "forceUpdate": true, - "releaseNotes": "Fix BUG : Ajout equipement à un evenement existant, boutons de modification de statut d'un evenement ne fonctionnaient pas. Refonte legere de la page calendrier.", - "timestamp": "2026-03-12T20:11:54.548Z" + "releaseNotes": "Fix BUG : Problème de cache avec les équipements non affichés dans le dialog de sélection d'équipement. Amélioration de la gestion du cache pour éviter les problèmes d'affichage.", + "timestamp": "2026-03-24T11:14:01.828Z" } \ No newline at end of file