diff --git a/em2rp/functions/.env b/em2rp/functions/.env index cd93c6f..2898597 100644 --- a/em2rp/functions/.env +++ b/em2rp/functions/.env @@ -7,4 +7,4 @@ SMTP_PASS="aL8@Rx8xqFrNij$a" # URL de l'application APP_URL="https://app.em2events.fr" -GEMINI_API_KEY="AIzaSyBdBdjFLma2pLenmFBlqZHArS4GVF-mclo" +GEMINI_API_KEY="AIzaSyB0hOvBjWeWjdrxVARzfErZ_uGuArlvmQc" diff --git a/em2rp/lib/views/widgets/event/equipment_selection_dialog.dart b/em2rp/lib/views/widgets/event/equipment_selection_dialog.dart index a527870..9180687 100644 --- a/em2rp/lib/views/widgets/event/equipment_selection_dialog.dart +++ b/em2rp/lib/views/widgets/event/equipment_selection_dialog.dart @@ -1,4 +1,4 @@ -import 'package:em2rp/utils/debug_log.dart'; +import 'package:em2rp/utils/debug_log.dart'; import 'package:flutter/material.dart'; import 'package:em2rp/models/equipment_model.dart'; import 'package:em2rp/models/container_model.dart'; @@ -7,16 +7,15 @@ 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 +/// 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 @@ -34,18 +33,18 @@ class ContainerConflictInfo { String get description { if (status == ContainerConflictStatus.none) return ''; if (status == ContainerConflictStatus.complete) { - return 'Tous les équipements sont déjà utilisés'; + return 'Tous les ├®quipements sont d├®j├á utilis├®s'; } - return '${conflictingEquipmentIds.length}/$totalChildren équipement(s) déjà utilisé(s)'; + return '${conflictingEquipmentIds.length}/$totalChildren ├®quipement(s) d├®j├á utilis├®(s)'; } } -/// Item sélectionné (équipement ou conteneur) +/// Item s├®lectionn├® (├®quipement ou conteneur) class SelectedItem { final String id; final String name; final SelectionType type; - final int quantity; // Pour consommables/câbles + final int quantity; // Pour consommables/c├óbles SelectedItem({ required this.id, @@ -64,7 +63,7 @@ class SelectedItem { } } -/// Dialog complet de sélection de matériel pour un événement +/// Dialog complet de s├®lection de mat├®riel pour un ├®v├®nement class EquipmentSelectionDialog extends StatefulWidget { final DateTime startDate; final DateTime endDate; @@ -82,50 +81,39 @@ 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 + // 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 _conflictDetails = {}; // D├®tails des conflits par ID + 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; @@ -134,7 +122,7 @@ class _EquipmentSelectionDialogState extends State { final List _paginatedEquipments = []; final List _paginatedContainers = []; - // Cache pour éviter les rebuilds inutiles + // Cache pour ├®viter les rebuilds inutiles final List _cachedContainers = []; final List _cachedEquipment = []; @@ -145,7 +133,7 @@ class _EquipmentSelectionDialogState extends State { // Ajouter le listener de scroll pour lazy loading _scrollController.addListener(_onScroll); - // Charger immédiatement les données de manière asynchrone + // Charger imm├®diatement les donn├®es de mani├¿re asynchrone _initializeData(); } @@ -154,64 +142,26 @@ class _EquipmentSelectionDialogState extends State { if (_isLoadingMore) return; if (_scrollController.hasClients && - _scrollController.position.pixels >= - _scrollController.position.maxScrollExtent - 300) { - // Charger la page suivante selon le type affiché + _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 + /// Initialise toutes les donn├®es n├®cessaires Future _initializeData() async { try { - // 1. Charger les conflits (batch optimisé) + // 1. Charger les conflits (batch optimis├®) await _loadEquipmentConflicts(); - // 2. Initialiser la sélection avec le matériel déjà assigné + // 2. Initialiser la s├®lection avec le mat├®riel d├®j├á assign├® await _initializeAlreadyAssigned(); - // 3. Charger la première page selon le type sélectionné + // 3. Charger la premi├¿re page selon le type s├®lectionn├® if (_displayType == SelectionType.equipment) { await _loadNextEquipmentPage(); } else { @@ -226,7 +176,7 @@ class _EquipmentSelectionDialogState extends State { Future _initializeAlreadyAssigned() async { final Map initialSelection = {}; - // Ajouter les équipements déjà assignés + // Ajouter les ├®quipements d├®j├á assign├®s for (var eq in widget.alreadyAssigned) { initialSelection[eq.equipmentId] = SelectedItem( id: eq.equipmentId, @@ -236,13 +186,13 @@ class _EquipmentSelectionDialogState extends State { ); } - // Ajouter les conteneurs déjà assignés + // Ajouter les conteneurs d├®j├á assign├®s if (widget.alreadyAssignedContainers.isNotEmpty) { try { - // Pour les conteneurs déjà assignés, on va les chercher via l'API si nécessaire - // ou créer des conteneurs temporaires + // Pour les conteneurs d├®j├á assign├®s, on va les chercher via l'API si n├®cessaire + // ou cr├®er des conteneurs temporaires for (var containerId in widget.alreadyAssignedContainers) { - // Chercher dans le cache ou créer un conteneur temporaire + // Chercher dans le cache ou cr├®er un conteneur temporaire final container = _cachedContainers.firstWhere( (c) => c.id == containerId, orElse: () => ContainerModel( @@ -263,10 +213,9 @@ 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 + // Ajouter les enfants comme s├®lectionn├®s aussi for (var equipmentId in container.equipmentIds) { if (!initialSelection.containsKey(equipmentId)) { initialSelection[equipmentId] = SelectedItem( @@ -279,24 +228,21 @@ class _EquipmentSelectionDialogState extends State { } } } catch (e) { - DebugLog.error( - '[EquipmentSelectionDialog] Error loading already assigned containers', - e); + DebugLog.error('[EquipmentSelectionDialog] Error loading already assigned containers', e); } } - // Mettre à jour la sélection et notifier + // Mettre ├á jour la s├®lection et notifier if (mounted && initialSelection.isNotEmpty) { setState(() { _selectedItems = initialSelection; }); _selectionChangeNotifier.value++; - DebugLog.info( - '[EquipmentSelectionDialog] Initialized with ${_selectedItems.length} already assigned items'); + DebugLog.info('[EquipmentSelectionDialog] Initialized with ${_selectedItems.length} already assigned items'); } } - /// Charge la page suivante d'équipements (lazy loading) + /// Charge la page suivante d'├®quipements (lazy loading) Future _loadNextEquipmentPage() async { if (_isLoadingMore || !_hasMoreEquipments) return; @@ -307,18 +253,18 @@ class _EquipmentSelectionDialogState extends State { limit: 50, 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(() { @@ -328,18 +274,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 quantites pour les consommables/cbles de cette page await _loadAvailableQuantities(newEquipments); - - // Si la liste ne peut pas scroller, précharger la page suivante. - _scheduleAutoLoadIfListNotScrollable(); + + // Vrifier si on doit charger d'autres lments (ex: tout a t filtr) + WidgetsBinding.instance.addPostFrameCallback((_) { + _checkIfMoreItemsNeeded(); + }); } } catch (e) { - DebugLog.error( - '[EquipmentSelectionDialog] Error loading equipment page', e); + DebugLog.error('[EquipmentSelectionDialog] Error loading equipment page', e); if (mounted) { setState(() => _isLoadingMore = false); } @@ -357,53 +303,51 @@ class _EquipmentSelectionDialogState extends State { limit: 50, startAfter: _lastContainerId, searchQuery: _searchQuery.isNotEmpty ? _searchQuery : null, - category: _selectedCategory?.name, // Filtre par catégorie d'équipements + category: _selectedCategory?.name, // Filtre par cat├®gorie d'├®quipements sortBy: 'id', sortOrder: 'asc', ); 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 + // D'abord, extraire TOUS les ├®quipements final List allEquipmentsToCache = []; for (var data in containersData) { final map = data as Map; final containerId = map['id'] as String; - // Debug: vérifier si le champ 'equipment' existe + // 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(); + // 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(); if (mounted) { setState(() { - // Ajouter tous les équipements au cache DANS le setState + // Ajouter tous les ├®quipements au cache DANS le setState for (var eq in allEquipmentsToCache) { if (!_cachedEquipment.any((e) => e.id == eq.id)) { _cachedEquipment.add(eq); @@ -416,27 +360,25 @@ 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(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + _checkIfMoreItemsNeeded(); + }); } } catch (e) { - DebugLog.error( - '[EquipmentSelectionDialog] Error loading container page', e); + DebugLog.error('[EquipmentSelectionDialog] Error loading container page', e); if (mounted) { setState(() => _isLoadingMore = false); } } } - /// Recharge depuis le début (appelé lors d'un changement de filtre/recherche) + /// Recharge depuis le d├®but (appel├® lors d'un changement de filtre/recherche) Future _reloadData() async { setState(() { _paginatedEquipments.clear(); @@ -496,7 +438,7 @@ class _EquipmentSelectionDialogState extends State { super.dispose(); } - /// Charge les quantités disponibles pour les consommables/câbles d'une liste d'équipements + /// Charge les quantit├®s disponibles pour les consommables/c├óbles d'une liste d'├®quipements Future _loadAvailableQuantities(List equipments) async { if (!mounted) return; @@ -506,7 +448,7 @@ class _EquipmentSelectionDialogState extends State { eq.category == EquipmentCategory.cable); for (var eq in consumables) { - // Ne recharger que si on n'a pas déjà la quantité + // Ne recharger que si on n'a pas d├®j├á la quantit├® if (!_availableQuantities.containsKey(eq.id)) { final available = await _availabilityService.getAvailableQuantity( equipment: eq, @@ -522,54 +464,44 @@ class _EquipmentSelectionDialogState extends State { } } - /// Charge les conflits de disponibilité pour tous les équipements et conteneurs - /// Version optimisée : un seul appel API au lieu d'un par équipement + /// Charge les conflits de disponibilit├® pour tous les ├®quipements et conteneurs + /// Version optimis├®e : un seul appel API au lieu d'un par ├®quipement Future _loadEquipmentConflicts() async { setState(() => _isLoadingConflicts = true); try { - DebugLog.info( - '[EquipmentSelectionDialog] Loading conflicts (optimized batch method)...'); + DebugLog.info('[EquipmentSelectionDialog] Loading conflicts (optimized batch method)...'); final startTime = DateTime.now(); - // UN SEUL appel API pour récupérer TOUS les équipements en conflit + // UN SEUL appel API pour r├®cup├®rer TOUS les ├®quipements en conflit final result = await _dataService.getConflictingEquipmentIds( startDate: widget.startDate, endDate: widget.endDate, excludeEventId: widget.excludeEventId, - installationTime: 0, // TODO: Récupérer depuis l'événement si nécessaire + installationTime: 0, // TODO: R├®cup├®rer depuis l'├®v├®nement si n├®cessaire disassemblyTime: 0, ); 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(() { @@ -578,19 +510,18 @@ class _EquipmentSelectionDialogState extends State { _conflictDetails = conflictDetails; _equipmentQuantities = equipmentQuantities; - // Convertir conflictDetails en equipmentConflicts pour l'affichage détaillé + // Convertir conflictDetails en equipmentConflicts pour l'affichage d├®taill├® _equipmentConflicts.clear(); conflictDetails.forEach((itemId, conflicts) { final conflictList = (conflicts as List).map((conflict) { final conflictMap = conflict as Map; - // Créer un EventModel minimal pour le conflit + // Cr├®er un EventModel minimal pour le conflit final conflictEvent = EventModel( 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, @@ -609,22 +540,15 @@ 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, - equipmentName: '', // Sera résolu lors de l'affichage + equipmentName: '', // Sera r├®solu lors de l'affichage conflictingEvent: conflictEvent, overlapDays: overlapDays.clamp(1, 999), ); @@ -634,8 +558,9 @@ class _EquipmentSelectionDialogState extends State { }); } - // Mettre à jour les statuts de conteneurs + // Mettre ├á jour les statuts de conteneurs await _updateContainerConflictStatus(); + } catch (e) { DebugLog.error('[EquipmentSelectionDialog] Error loading conflicts', e); } finally { @@ -643,14 +568,14 @@ class _EquipmentSelectionDialogState extends State { } } - /// Met à jour le statut de conflit des conteneurs basé sur les IDs en conflit + /// Met ├á jour le statut de conflit des conteneurs bas├® sur les IDs en conflit Future _updateContainerConflictStatus() async { if (!mounted) return; try { - // Utiliser les containers paginés chargés + // Utiliser les containers pagin├®s charg├®s for (var container in _paginatedContainers) { - // Vérifier si le conteneur lui-même est en conflit + // V├®rifier si le conteneur lui-m├¬me est en conflit if (_conflictingContainerIds.contains(container.id)) { _containerConflicts[container.id] = ContainerConflictInfo( status: ContainerConflictStatus.complete, @@ -660,16 +585,15 @@ class _EquipmentSelectionDialogState extends State { continue; } - // Vérifier si des équipements enfants sont en conflit + // V├®rifier si des ├®quipements enfants sont en conflit final conflictingChildren = container.equipmentIds .where((eqId) => _conflictingEquipmentIds.contains(eqId)) .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, @@ -677,25 +601,22 @@ 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 + // 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); } } - /// Récupère les détails des conflits pour un équipement/conteneur donné + /// R├®cup├¿re les d├®tails des conflits pour un ├®quipement/conteneur donn├® List> _getConflictDetailsFor(String id) { final details = _conflictDetails[id]; if (details == null) return []; @@ -707,13 +628,12 @@ class _EquipmentSelectionDialogState extends State { return []; } - /// Construit l'affichage des quantités pour les câbles/consommables + /// 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) + // Pas d'info de quantit├®, utiliser l'ancien syst├¿me (availableQuantities) final availableQty = _availableQuantities[equipment.id]; if (availableQty == null) return const SizedBox.shrink(); @@ -764,9 +684,8 @@ class _EquipmentSelectionDialogState extends State { ); } - /// Affiche un dialog avec les détails des réservations de quantité - Future _showQuantityDetailsDialog( - EquipmentModel equipment, Map quantityInfo) async { + /// Affiche un dialog avec les d├®tails des r├®servations de quantit├® + 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; @@ -781,7 +700,7 @@ class _EquipmentSelectionDialogState extends State { const SizedBox(width: 8), Expanded( child: Text( - 'Quantités - ${equipment.name}', + 'Quantit├®s - ${equipment.name}', style: const TextStyle(fontSize: 18), ), ), @@ -793,7 +712,7 @@ class _EquipmentSelectionDialogState extends State { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Résumé + // R├®sum├® Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( @@ -807,7 +726,7 @@ class _EquipmentSelectionDialogState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - 'Quantité totale :', + 'Quantit├® totale :', style: TextStyle( fontWeight: FontWeight.bold, color: Colors.grey.shade800, @@ -837,9 +756,7 @@ 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, ), ), @@ -850,10 +767,10 @@ class _EquipmentSelectionDialogState extends State { ), const SizedBox(height: 16), - // Liste des réservations + // Liste des r├®servations if (reservations.isNotEmpty) ...[ Text( - 'Utilisé sur ${reservations.length} événement(s) :', + 'Utilis├® sur ${reservations.length} ├®v├®nement(s) :', style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 14, @@ -866,12 +783,10 @@ 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), @@ -890,8 +805,7 @@ class _EquipmentSelectionDialogState extends State { ), title: Text( eventName, - style: - const TextStyle(fontWeight: FontWeight.w500), + style: const TextStyle(fontWeight: FontWeight.w500), ), subtitle: viaContainer != null ? Text( @@ -929,25 +843,22 @@ class _EquipmentSelectionDialogState extends State { ); } - /// Recherche les conteneurs recommandés pour un équipement - /// NOTE: Désactivé avec le lazy loading - on ne charge pas tous les containers d'un coup + /// Recherche les conteneurs recommand├®s pour un ├®quipement + /// NOTE: D├®sactiv├® avec le lazy loading - on ne charge pas tous les containers d'un coup Future _findRecommendedContainers(String equipmentId) async { - // Désactivé pour le moment avec le lazy loading - // On pourrait implémenter une API dédiée si nécessaire + // D├®sactiv├® pour le moment avec le lazy loading + // On pourrait impl├®menter une API d├®di├®e si n├®cessaire return; } - /// Obtenir les boîtes parentes d'un équipement de manière synchrone depuis le cache + /// Obtenir les bo├«tes parentes d'un ├®quipement de mani├¿re synchrone depuis le cache List _getParentContainers(String equipmentId) { return _recommendedContainers[equipmentId] ?? []; } - 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)) { + 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)) { // Demander confirmation pour forcer final shouldForce = await _showForceConfirmationDialog(id); if (shouldForce == true) { @@ -957,35 +868,32 @@ class _EquipmentSelectionDialogState extends State { } if (_selectedItems.containsKey(id)) { - // Désélectionner + // 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 + // Si c'est un conteneur, d├®s├®lectionner d'abord ses enfants de mani├¿re asynchrone await _deselectContainerChildren(id); } - // Mise à jour avec setState pour garantir le rebuild + // 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()}'); + DebugLog.info('[EquipmentSelectionDialog] After deselection, _selectedItems count: ${_selectedItems.length}'); + DebugLog.info('[EquipmentSelectionDialog] Remaining items: ${_selectedItems.keys.toList()}'); // Notifier le changement _selectionChangeNotifier.value++; } else { - // Sélectionner + // S├®lectionner DebugLog.info('[EquipmentSelectionDialog] Selecting $type: $id'); - // Mise à jour avec setState pour garantir le rebuild + // Mise ├á jour avec setState pour garantir le rebuild if (mounted) { setState(() { _selectedItems[id] = SelectedItem( @@ -997,12 +905,12 @@ class _EquipmentSelectionDialogState extends State { }); } - // Si c'est un équipement, chercher les conteneurs recommandés + // Si c'est un ├®quipement, chercher les conteneurs recommand├®s if (type == SelectionType.equipment) { _findRecommendedContainers(id); } - // Si c'est un conteneur, sélectionner ses enfants en cascade + // Si c'est un conteneur, s├®lectionner ses enfants en cascade if (type == SelectionType.container) { await _selectContainerChildren(id); } @@ -1012,12 +920,11 @@ class _EquipmentSelectionDialogState extends State { } } - /// Sélectionner tous les enfants d'un conteneur + /// S├®lectionner tous les enfants d'un conteneur Future _selectContainerChildren(String containerId) async { try { - // Chercher le container dans les données paginées ou le cache - final container = - [..._paginatedContainers, ..._cachedContainers].firstWhere( + // Chercher le container dans les donn├®es pagin├®es ou le cache + final container = [..._paginatedContainers, ..._cachedContainers].firstWhere( (c) => c.id == containerId, orElse: () => ContainerModel( id: containerId, @@ -1030,13 +937,13 @@ class _EquipmentSelectionDialogState extends State { ), ); - // Mettre à jour le cache + // Mettre ├á jour le cache _containerEquipmentCache[containerId] = List.from(container.equipmentIds); - // Sélectionner chaque enfant (sans bloquer, car ils sont "composés") + // S├®lectionner chaque enfant (sans bloquer, car ils sont "compos├®s") for (var equipmentId in container.equipmentIds) { if (!_selectedItems.containsKey(equipmentId)) { - // Chercher l'équipement dans les données paginées ou le cache + // Chercher l'├®quipement dans les donn├®es pagin├®es ou le cache final eq = [..._paginatedEquipments, ..._cachedEquipment].firstWhere( (e) => e.id == equipmentId, orElse: () => EquipmentModel( @@ -1063,19 +970,17 @@ 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); } } - /// Désélectionner tous les enfants d'un conteneur + /// D├®s├®lectionner tous les enfants d'un conteneur Future _deselectContainerChildren(String containerId) async { try { - // Chercher le container dans les données paginées ou le cache - final container = - [..._paginatedContainers, ..._cachedContainers].firstWhere( + // Chercher le container dans les donn├®es pagin├®es ou le cache + final container = [..._paginatedContainers, ..._cachedContainers].firstWhere( (c) => c.id == containerId, orElse: () => ContainerModel( id: containerId, @@ -1098,19 +1003,18 @@ class _EquipmentSelectionDialogState extends State { // Nettoyer le cache _containerEquipmentCache.remove(containerId); - // Retirer de la liste des conteneurs expandés + // Retirer de la liste des conteneurs expand├®s _expandedContainers.remove(containerId); }); } - DebugLog.info( - '[EquipmentSelectionDialog] Deselected container $containerId and ${container.equipmentIds.length} children'); + DebugLog.info('[EquipmentSelectionDialog] Deselected container $containerId and ${container.equipmentIds.length} children'); } catch (e) { DebugLog.error('Error deselecting container children', e); } } - /// Affiche un dialog pour confirmer le forçage d'un équipement en conflit + /// Affiche un dialog pour confirmer le for├ºage d'un ├®quipement en conflit Future _showForceConfirmationDialog(String equipmentId) async { final conflicts = _equipmentConflicts[equipmentId] ?? []; @@ -1121,43 +1025,40 @@ class _EquipmentSelectionDialogState extends State { children: [ Icon(Icons.warning, color: Colors.orange), SizedBox(width: 8), - Text('Équipement déjà utilisé'), + Text('├ëquipement d├®j├á utilis├®'), ], ), content: Column( 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), - ), - Text( - 'Chevauchement : ${conflict.overlapDays} jour(s)', - style: const TextStyle( - fontSize: 12, color: Colors.grey), - ), - ], + 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), + ), + ], + ), ), - )), + ], + ), + )), ], ), actions: [ @@ -1171,13 +1072,14 @@ class _EquipmentSelectionDialogState extends State { backgroundColor: Colors.orange, foregroundColor: Colors.white, ), - child: const Text('Forcer quand même'), + child: const Text('Forcer quand m├¬me'), ), ], ), ); } + @override Widget build(BuildContext context) { final screenSize = MediaQuery.of(context).size; @@ -1205,7 +1107,7 @@ class _EquipmentSelectionDialogState extends State { child: _buildMainList(), ), - // Panneau latéral : sélection + recommandations + // Panneau lat├®ral : s├®lection + recommandations Container( width: 320, decoration: BoxDecoration( @@ -1258,7 +1160,7 @@ class _EquipmentSelectionDialogState extends State { const SizedBox(width: 12), const Expanded( child: Text( - 'Ajouter du matériel', + 'Ajouter du mat├®riel', style: TextStyle( color: Colors.white, fontSize: 20, @@ -1288,7 +1190,7 @@ class _EquipmentSelectionDialogState extends State { TextField( controller: _searchController, decoration: InputDecoration( - hintText: 'Rechercher du matériel ou des boîtes...', + hintText: 'Rechercher du mat├®riel ou des bo├«tes...', prefixIcon: const Icon(Icons.search, color: AppColors.rouge), suffixIcon: _searchQuery.isNotEmpty ? IconButton( @@ -1302,19 +1204,18 @@ 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()); - // Recharger depuis le début avec le nouveau filtre + // Recharger depuis le d├®but avec le nouveau filtre _reloadData(); }, ), const SizedBox(height: 12), - // Filtres par catégorie (pour les équipements) + // Filtres par cat├®gorie (pour les ├®quipements) SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( @@ -1333,7 +1234,7 @@ class _EquipmentSelectionDialogState extends State { const SizedBox(height: 12), - // Checkbox pour afficher les équipements en conflit + // Checkbox pour afficher les ├®quipements en conflit Row( children: [ Checkbox( @@ -1342,20 +1243,16 @@ class _EquipmentSelectionDialogState extends State { setState(() { _showConflictingItems = value ?? false; }); - - // Des filtres plus stricts peuvent masquer la page courante. - _scheduleAutoLoadIfListNotScrollable(); }, activeColor: AppColors.rouge, ), const Text( - 'Afficher les équipements déjà utilisés', + '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', + message: 'Afficher les ├®quipements et bo├«tes qui sont d├®j├á utilis├®s durant ces dates', child: Icon( Icons.info_outline, size: 18, @@ -1367,7 +1264,7 @@ class _EquipmentSelectionDialogState extends State { const SizedBox(height: 12), - // Chip pour switcher entre Équipements et Containers + // Chip pour switcher entre ├ëquipements et Containers Row( children: [ const Text( @@ -1376,7 +1273,7 @@ class _EquipmentSelectionDialogState extends State { ), const SizedBox(width: 8), ChoiceChip( - label: const Text('Équipements'), + label: const Text('├ëquipements'), selected: _displayType == SelectionType.equipment, onSelected: (selected) { if (selected && _displayType != SelectionType.equipment) { @@ -1388,9 +1285,7 @@ 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), @@ -1407,9 +1302,7 @@ 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, ), ), ], @@ -1429,7 +1322,7 @@ class _EquipmentSelectionDialogState extends State { setState(() { _selectedCategory = selected ? category : null; }); - // Recharger depuis le début avec le nouveau filtre + // Recharger depuis le d├®but avec le nouveau filtre _reloadData(); }, selectedColor: AppColors.rouge, @@ -1441,7 +1334,7 @@ class _EquipmentSelectionDialogState extends State { } Widget _buildMainList() { - // Afficher un indicateur de chargement si les données sont en cours de chargement + // Afficher un indicateur de chargement si les donn├®es sont en cours de chargement if (_isLoadingConflicts) { return Center( child: Column( @@ -1450,7 +1343,7 @@ class _EquipmentSelectionDialogState extends State { const CircularProgressIndicator(color: AppColors.rouge), const SizedBox(height: 16), Text( - 'Vérification de la disponibilité...', + 'V├®rification de la disponibilit├®...', style: TextStyle(color: Colors.grey.shade600), ), ], @@ -1458,42 +1351,40 @@ class _EquipmentSelectionDialogState extends State { ); } - // Vue hiérarchique unique : Boîtes en haut, TOUS les équipements en bas + // Vue hi├®rarchique unique : Bo├«tes en haut, TOUS les ├®quipements en bas return _buildHierarchicalList(); } - /// Vue hiérarchique unique avec cache pour éviter les rebuilds inutiles + /// Vue hi├®rarchique unique avec cache pour ├®viter les rebuilds inutiles Widget _buildHierarchicalList() { return ValueListenableBuilder( valueListenable: _selectionChangeNotifier, builder: (context, _, __) { - // Filtrer les données paginées selon le type affiché + // Filtrer les donn├®es pagin├®es selon le type affich├® List itemWidgets = []; if (_displayType == SelectionType.equipment) { - // Filtrer côté client pour "Afficher équipements déjà utilisés" + // 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 final filteredContainers = _paginatedContainers.where((container) { if (!_showConflictingItems) { - // Vérifier si le container lui-même est en conflit + // V├®rifier si le container lui-m├¬me est en conflit if (_conflictingContainerIds.contains(container.id)) { return false; } - // Vérifier si le container a des équipements enfants en conflit + // V├®rifier si le container a des ├®quipements enfants en conflit final hasConflictingChildren = container.equipmentIds.any( (eqId) => _conflictingEquipmentIds.contains(eqId), ); @@ -1506,23 +1397,29 @@ 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(); } - return ListView( - controller: _scrollController, - padding: const EdgeInsets.all(16), - children: [ - // Header - _buildSectionHeader( - _displayType == SelectionType.equipment - ? 'Équipements' - : 'Containers', - _displayType == SelectionType.equipment - ? Icons.inventory_2 - : Icons.inventory, + return NotificationListener( + onNotification: (ScrollNotification scrollInfo) { + if (!_isLoadingMore && scrollInfo.metrics.pixels >= scrollInfo.metrics.maxScrollExtent - 300) { + if (_displayType == SelectionType.equipment && _hasMoreEquipments) { + _loadNextEquipmentPage(); + } else if (_displayType == SelectionType.container && _hasMoreContainers) { + _loadNextContainerPage(); + } + } + return false; + }, + child: ListView( + controller: _scrollController, + padding: const EdgeInsets.all(16), + children: [ + // Header + _buildSectionHeader( + _displayType == SelectionType.equipment ? '├ëquipements' : 'Containers', + _displayType == SelectionType.equipment ? Icons.inventory_2 : Icons.inventory, itemWidgets.length, ), const SizedBox(height: 12), @@ -1540,10 +1437,7 @@ 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( @@ -1554,20 +1448,18 @@ class _EquipmentSelectionDialogState extends State { ), ), - // Message si rien trouvé + // Message si rien trouv├® if (itemWidgets.isEmpty && !_isLoadingMore) Center( child: Padding( 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), + 'Aucun r├®sultat trouv├®', + style: TextStyle(fontSize: 16, color: Colors.grey.shade600), ), ], ), @@ -1580,7 +1472,7 @@ class _EquipmentSelectionDialogState extends State { ); } - /// Header de section (version simple, gardée pour compatibilité) + /// 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), @@ -1624,14 +1516,13 @@ 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é + // Bloquer la s├®lection si en conflit et non forc├® final canSelect = !hasConflict || isSelected; return RepaintBoundary( @@ -1667,261 +1558,244 @@ 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( - 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), - ), + // Infos + Expanded( 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, + Expanded( + child: Text( + equipment.id, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), ), ), - ], - ), - 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 (hasConflict) + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Colors.orange.shade700, + borderRadius: BorderRadius.circular(12), ), - 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, + 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 (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 (equipment.brand != null || equipment.model != null) + Text( + '${equipment.brand ?? ''} ${equipment.model ?? ''}'.trim(), + style: TextStyle( + color: Colors.grey.shade700, + fontSize: 14, ), ), - if (!isSelected) + // Affichage des bo├«tes parentes + if (_getParentContainers(equipment.id).isNotEmpty) 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), - ), + 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: [ + 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), + ), + ), + ), + ], + ), + ), + ], ), ), ), ), - ); + ), + ); } - /// Widget pour le sélecteur de quantité - /// Si isSelected = false, le premier clic sur + sélectionne l'item avec quantité 1 + /// Widget pour le s├®lecteur de quantit├® + /// Si isSelected = false, le premier clic sur + s├®lectionne l'item avec quantit├® 1 Widget _buildQuantitySelector( String equipmentId, SelectedItem selectedItem, @@ -1940,18 +1814,14 @@ 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( @@ -1965,11 +1835,10 @@ 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 + // Premier clic : s├®lectionner avec quantit├® 1 _toggleSelection( equipmentId, selectedItem.name, @@ -1977,14 +1846,12 @@ class _EquipmentSelectionDialogState extends State { maxQuantity: maxQuantity, ); } else { - // Item déjà sélectionné : incrémenter + // Item d├®j├á s├®lectionn├® : incr├®menter if (mounted) { setState(() { - _selectedItems[equipmentId] = selectedItem.copyWith( - quantity: selectedItem.quantity + 1); + _selectedItems[equipmentId] = selectedItem.copyWith(quantity: selectedItem.quantity + 1); }); - _selectionChangeNotifier - .value++; // Notifier le changement + _selectionChangeNotifier.value++; // Notifier le changement } } } @@ -1997,278 +1864,255 @@ 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é) + // Bloquer la s├®lection si tous les enfants sont en conflit (sauf si d├®j├á s├®lectionn├®) final canSelect = !isCompleteConflict || isSelected; return RepaintBoundary( 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, - borderRadius: BorderRadius.circular(8), - ) - : null, - child: Column( - children: [ - InkWell( - onTap: canSelect - ? () => _toggleSelection( - container.id, - container.name, - SelectionType.container, - ) - : null, + 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), - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - children: [ - Row( - children: [ - // Checkbox - Checkbox( - value: isSelected, - onChanged: canSelect - ? (value) => _toggleSelection( + ) + : 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( 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( - 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( - 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(width: 4), - Text( - '${container.itemCount} équipement(s)', style: const TextStyle( - fontSize: 13, - fontWeight: FontWeight.w500, - color: Colors.blue, + fontWeight: FontWeight.bold, + fontSize: 16, ), ), - if (hasConflict) ...[ - const SizedBox(width: 8), - Icon( - Icons.warning, - size: 14, + ), + // 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), ), - const SizedBox(width: 4), - Text( - conflictInfo.description, - style: TextStyle( - fontSize: 11, - color: isCompleteConflict - ? Colors.red.shade700 - : Colors.orange.shade700, - fontWeight: FontWeight.w500, - ), + 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( + color: Colors.white, + fontSize: 11, + fontWeight: FontWeight.bold, + ), + ), + ], ), - ], - ], + ), + ], + ), + const SizedBox(height: 4), + Text( + container.name, + style: TextStyle( + color: Colors.grey.shade700, + fontSize: 14, ), - ], - ), - ), - - // 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, + ), + const SizedBox(height: 4), + Row( + children: [ + Icon( + Icons.inventory_2, + size: 14, + color: Colors.blue.shade700, ), - ), + 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), + 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, + ), + ), + ), + ], + ), + ), + ], ), ), + ), - // 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) { - // Utiliser les équipements paginés et le cache + /// Widget pour afficher les ├®quipements enfants d'un conteneur + 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) { @@ -2283,7 +2127,7 @@ class _EquipmentSelectionDialogState extends State { Icon(Icons.info_outline, size: 16, color: Colors.grey.shade600), const SizedBox(width: 8), Text( - 'Aucun équipement dans ce conteneur', + 'Aucun ├®quipement dans ce conteneur', style: TextStyle(color: Colors.grey.shade600, fontSize: 13), ), ], @@ -2305,7 +2149,7 @@ class _EquipmentSelectionDialogState extends State { Icon(Icons.list, size: 16, color: Colors.grey.shade700), const SizedBox(width: 6), Text( - 'Contenu de la boîte :', + 'Contenu de la bo├«te :', style: TextStyle( fontSize: 13, color: Colors.grey.shade700, @@ -2316,114 +2160,110 @@ 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, - ), - 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, - ), + 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, ), - if (eq.brand != null || eq.model != null) - Text( - '${eq.brand ?? ''} ${eq.model ?? ''}'.trim(), - style: TextStyle( - fontSize: 11, - color: Colors.grey.shade600, + ), + 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, + ), + ), + 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() { - // Compter uniquement les conteneurs et équipements "racine" (pas enfants de conteneurs) + // Compter uniquement les conteneurs et ├®quipements "racine" (pas enfants de conteneurs) final selectedContainers = _selectedItems.entries .where((e) => e.value.type == SelectionType.container) .toList(); - // Collecter tous les IDs d'équipements qui sont enfants de conteneurs sélectionnés + // Collecter tous les IDs d'├®quipements qui sont enfants de conteneurs s├®lectionn├®s final Set equipmentIdsInContainers = {}; for (var containerEntry in selectedContainers) { final childrenIds = _getContainerEquipmentIds(containerEntry.key); equipmentIdsInContainers.addAll(childrenIds); } - // Équipements qui ne sont PAS enfants d'un conteneur sélectionné + // ├ëquipements qui ne sont PAS enfants d'un conteneur s├®lectionn├® final selectedStandaloneEquipment = _selectedItems.entries .where((e) => e.value.type == SelectionType.equipment) .where((e) => !equipmentIdsInContainers.contains(e.key)) @@ -2445,7 +2285,7 @@ class _EquipmentSelectionDialogState extends State { const Icon(Icons.check_circle, color: Colors.white), const SizedBox(width: 8), const Text( - 'Sélection', + 'S├®lection', style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold, @@ -2470,11 +2310,12 @@ class _EquipmentSelectionDialogState extends State { ], ), ), + Expanded( child: totalDisplayed == 0 ? const Center( child: Text( - 'Aucune sélection', + 'Aucune s├®lection', style: TextStyle(color: Colors.grey), ), ) @@ -2485,29 +2326,27 @@ class _EquipmentSelectionDialogState extends State { Padding( padding: const EdgeInsets.all(8), child: Text( - 'Boîtes ($containerCount)', + 'Bo├«tes ($containerCount)', style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 14, ), ), ), - ...selectedContainers.map( - (e) => _buildSelectedContainerTile(e.key, e.value)), + ...selectedContainers.map((e) => _buildSelectedContainerTile(e.key, e.value)), ], if (standaloneEquipmentCount > 0) ...[ Padding( padding: const EdgeInsets.all(8), child: Text( - 'Équipements ($standaloneEquipmentCount)', + '├ëquipements ($standaloneEquipmentCount)', style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 14, ), ), ), - ...selectedStandaloneEquipment - .map((e) => _buildSelectedItemTile(e.key, e.value)), + ...selectedStandaloneEquipment.map((e) => _buildSelectedItemTile(e.key, e.value)), ], ], ), @@ -2516,14 +2355,15 @@ class _EquipmentSelectionDialogState extends State { ); } - /// Récupère les IDs des équipements d'un conteneur (depuis le cache) + + /// 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 + // On doit r├®cup├®rer le conteneur depuis le provider de mani├¿re synchrone // Pour cela, on va maintenir un cache local return _containerEquipmentCache[containerId] ?? []; } - // Cache local pour les équipements des conteneurs + // Cache local pour les ├®quipements des conteneurs final Map> _containerEquipmentCache = {}; Widget _buildSelectedContainerTile(String id, SelectedItem item) { @@ -2545,7 +2385,7 @@ class _EquipmentSelectionDialogState extends State { style: const TextStyle(fontSize: 13, fontWeight: FontWeight.bold), ), subtitle: Text( - '$childrenCount équipement(s)', + '$childrenCount ├®quipement(s)', style: const TextStyle(fontSize: 11), ), trailing: Row( @@ -2563,8 +2403,7 @@ class _EquipmentSelectionDialogState extends State { } else { _expandedContainers.add(id); } - _selectionChangeNotifier - .value++; // Notifier sans rebuild complet + _selectionChangeNotifier.value++; // Notifier sans rebuild complet }, ), IconButton( @@ -2601,8 +2440,7 @@ 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 ), @@ -2622,7 +2460,7 @@ class _EquipmentSelectionDialogState extends State { style: const TextStyle(fontSize: 13), ), subtitle: item.quantity > 1 - ? Text('Qté: ${item.quantity}', style: const TextStyle(fontSize: 11)) + ? Text('Qt├®: ${item.quantity}', style: const TextStyle(fontSize: 11)) : null, trailing: IconButton( icon: const Icon(Icons.close, size: 18), @@ -2647,7 +2485,7 @@ class _EquipmentSelectionDialogState extends State { Icon(Icons.lightbulb, color: Colors.white, size: 20), SizedBox(width: 8), Text( - 'Boîtes recommandées', + 'Bo├«tes recommand├®es', style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold, @@ -2683,7 +2521,7 @@ class _EquipmentSelectionDialogState extends State { style: const TextStyle(fontSize: 13, fontWeight: FontWeight.bold), ), subtitle: Text( - '${container.itemCount} équipement(s)', + '${container.itemCount} ├®quipement(s)', style: const TextStyle(fontSize: 11), ), trailing: isAlreadySelected @@ -2721,7 +2559,7 @@ class _EquipmentSelectionDialogState extends State { child: Row( children: [ Text( - '${_selectedItems.length} élément(s) sélectionné(s)', + '${_selectedItems.length} ├®l├®ment(s) s├®lectionn├®(s)', style: const TextStyle(fontWeight: FontWeight.w500), ), const Spacer(), @@ -2737,10 +2575,9 @@ 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'), + child: const Text('Valider la s├®lection'), ), ], ), @@ -2749,3 +2586,4 @@ class _EquipmentSelectionDialogState extends State { ); } } +