import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:em2rp/models/equipment_model.dart'; import 'package:em2rp/models/container_model.dart'; import 'package:em2rp/models/event_model.dart'; import 'package:em2rp/providers/equipment_provider.dart'; import 'package:em2rp/providers/container_provider.dart'; import 'package:em2rp/services/event_availability_service.dart'; import 'package:em2rp/utils/colors.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 } /// Informations sur les conflits d'un conteneur class ContainerConflictInfo { final ContainerConflictStatus status; final List conflictingEquipmentIds; final int totalChildren; ContainerConflictInfo({ required this.status, required this.conflictingEquipmentIds, required this.totalChildren, }); String get description { if (status == ContainerConflictStatus.none) return ''; if (status == ContainerConflictStatus.complete) { return 'Tous les équipements sont déjà utilisés'; } return '${conflictingEquipmentIds.length}/${totalChildren} équipement(s) déjà utilisé(s)'; } } /// Item sélectionné (équipement ou conteneur) class SelectedItem { final String id; final String name; final SelectionType type; final int quantity; // Pour consommables/câbles SelectedItem({ required this.id, required this.name, required this.type, this.quantity = 1, }); SelectedItem copyWith({int? quantity}) { return SelectedItem( id: id, name: name, type: type, quantity: quantity ?? this.quantity, ); } } /// Dialog complet de sélection de matériel pour un événement class EquipmentSelectionDialog extends StatefulWidget { final DateTime startDate; final DateTime endDate; final List alreadyAssigned; final List alreadyAssignedContainers; final String? excludeEventId; const EquipmentSelectionDialog({ super.key, required this.startDate, required this.endDate, this.alreadyAssigned = const [], this.alreadyAssignedContainers = const [], this.excludeEventId, }); @override State createState() => _EquipmentSelectionDialogState(); } class _EquipmentSelectionDialogState extends State { final TextEditingController _searchController = TextEditingController(); final EventAvailabilityService _availabilityService = EventAvailabilityService(); EquipmentCategory? _selectedCategory; Map _selectedItems = {}; Map _availableQuantities = {}; // Pour consommables Map> _recommendedContainers = {}; // Recommandations Map> _equipmentConflicts = {}; // Conflits de disponibilité Map _containerConflicts = {}; // Conflits des conteneurs Set _expandedContainers = {}; // Conteneurs dépliés dans la liste bool _isLoadingQuantities = false; bool _isLoadingConflicts = false; String _searchQuery = ''; @override void initState() { super.initState(); _initializeAlreadyAssigned(); _loadAvailableQuantities(); _loadEquipmentConflicts(); _loadContainerConflicts(); } /// Initialise la sélection avec le matériel déjà assigné Future _initializeAlreadyAssigned() async { // Ajouter les équipements déjà assignés for (var eq in widget.alreadyAssigned) { _selectedItems[eq.equipmentId] = SelectedItem( id: eq.equipmentId, name: eq.equipmentId, type: SelectionType.equipment, quantity: eq.quantity, ); } // Ajouter les conteneurs déjà assignés if (widget.alreadyAssignedContainers.isNotEmpty) { try { final containerProvider = context.read(); final containers = await containerProvider.containersStream.first; for (var containerId in widget.alreadyAssignedContainers) { final container = containers.firstWhere( (c) => c.id == containerId, orElse: () => ContainerModel( id: containerId, name: 'Inconnu', type: ContainerType.flightCase, status: EquipmentStatus.available, equipmentIds: [], createdAt: DateTime.now(), updatedAt: DateTime.now(), ), ); _selectedItems[containerId] = SelectedItem( id: containerId, name: container.name, type: SelectionType.container, ); // Charger le cache des enfants _containerEquipmentCache[containerId] = List.from(container.equipmentIds); // Ajouter les enfants comme sélectionnés aussi for (var equipmentId in container.equipmentIds) { if (!_selectedItems.containsKey(equipmentId)) { _selectedItems[equipmentId] = SelectedItem( id: equipmentId, name: equipmentId, type: SelectionType.equipment, quantity: 1, ); } } } } catch (e) { print('[EquipmentSelectionDialog] Error loading already assigned containers: $e'); } } print('[EquipmentSelectionDialog] Initialized with ${_selectedItems.length} already assigned items'); } @override void dispose() { _searchController.dispose(); super.dispose(); } /// Charge les quantités disponibles pour tous les consommables/câbles Future _loadAvailableQuantities() async { setState(() => _isLoadingQuantities = true); try { final equipmentProvider = context.read(); // EquipmentProvider utilise un stream, récupérons les données final equipmentStream = equipmentProvider.equipmentStream; final equipment = await equipmentStream.first; final consumables = equipment.where((eq) => eq.category == EquipmentCategory.consumable || eq.category == EquipmentCategory.cable); for (var eq in consumables) { final available = await _availabilityService.getAvailableQuantity( equipment: eq, startDate: widget.startDate, endDate: widget.endDate, excludeEventId: widget.excludeEventId, ); _availableQuantities[eq.id] = available; } } catch (e) { print('Error loading quantities: $e'); } finally { if (mounted) setState(() => _isLoadingQuantities = false); } } /// Charge les conflits de disponibilité pour tous les équipements Future _loadEquipmentConflicts() async { setState(() => _isLoadingConflicts = true); try { print('[EquipmentSelectionDialog] Loading equipment conflicts...'); final equipmentProvider = context.read(); final equipment = await equipmentProvider.equipmentStream.first; print('[EquipmentSelectionDialog] Checking conflicts for ${equipment.length} equipments'); for (var eq in equipment) { final conflicts = await _availabilityService.checkEquipmentAvailability( equipmentId: eq.id, equipmentName: eq.id, startDate: widget.startDate, endDate: widget.endDate, excludeEventId: widget.excludeEventId, ); if (conflicts.isNotEmpty) { print('[EquipmentSelectionDialog] Found ${conflicts.length} conflict(s) for ${eq.id}'); _equipmentConflicts[eq.id] = conflicts; } } print('[EquipmentSelectionDialog] Total equipments with conflicts: ${_equipmentConflicts.length}'); } catch (e) { print('[EquipmentSelectionDialog] Error loading conflicts: $e'); } finally { if (mounted) setState(() => _isLoadingConflicts = false); } } /// Charge les conflits de disponibilité pour tous les conteneurs Future _loadContainerConflicts() async { try { print('[EquipmentSelectionDialog] Loading container conflicts...'); final containerProvider = context.read(); final containers = await containerProvider.containersStream.first; print('[EquipmentSelectionDialog] Checking conflicts for ${containers.length} containers'); for (var container in containers) { final conflictingChildren = []; // Vérifier chaque équipement enfant for (var equipmentId in container.equipmentIds) { if (_equipmentConflicts.containsKey(equipmentId)) { conflictingChildren.add(equipmentId); } } if (conflictingChildren.isNotEmpty) { final status = conflictingChildren.length == container.equipmentIds.length ? ContainerConflictStatus.complete : ContainerConflictStatus.partial; _containerConflicts[container.id] = ContainerConflictInfo( status: status, conflictingEquipmentIds: conflictingChildren, totalChildren: container.equipmentIds.length, ); print('[EquipmentSelectionDialog] Container ${container.id}: ${status.name} conflict (${conflictingChildren.length}/${container.equipmentIds.length} children)'); } } print('[EquipmentSelectionDialog] Total containers with conflicts: ${_containerConflicts.length}'); } catch (e) { print('[EquipmentSelectionDialog] Error loading container conflicts: $e'); } } /// Recherche les conteneurs recommandés pour un équipement Future _findRecommendedContainers(String equipmentId) async { try { final containerProvider = context.read(); // Récupérer les conteneurs depuis le stream final containerStream = containerProvider.containersStream; final containers = await containerStream.first; final recommended = containers .where((container) => container.equipmentIds.contains(equipmentId)) .toList(); if (recommended.isNotEmpty) { setState(() { _recommendedContainers[equipmentId] = recommended; }); } } catch (e) { print('Error finding recommended containers: $e'); } } /// 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 && _equipmentConflicts.containsKey(id)) { // Demander confirmation pour forcer final shouldForce = await _showForceConfirmationDialog(id); if (shouldForce == true) { _toggleSelection(id, name, type, maxQuantity: maxQuantity, force: true); } return; } if (_selectedItems.containsKey(id)) { // Désélectionner print('[EquipmentSelectionDialog] Deselecting $type: $id'); print('[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 await _deselectContainerChildren(id); } // Mise à jour sans setState pour éviter le flash _selectedItems.remove(id); print('[EquipmentSelectionDialog] After deselection, _selectedItems count: ${_selectedItems.length}'); print('[EquipmentSelectionDialog] Remaining items: ${_selectedItems.keys.toList()}'); // Forcer uniquement la reconstruction du panneau de sélection et de la card concernée if (mounted) setState(() {}); } else { // Sélectionner print('[EquipmentSelectionDialog] Selecting $type: $id'); // Mise à jour sans setState pour éviter le flash _selectedItems[id] = SelectedItem( id: id, name: name, type: type, quantity: 1, ); // Si c'est un équipement, chercher les conteneurs recommandés if (type == SelectionType.equipment) { _findRecommendedContainers(id); } // Si c'est un conteneur, sélectionner ses enfants en cascade if (type == SelectionType.container) { await _selectContainerChildren(id); } // Forcer uniquement la reconstruction du panneau de sélection et de la card concernée if (mounted) setState(() {}); } } /// Sélectionner tous les enfants d'un conteneur Future _selectContainerChildren(String containerId) async { try { final containerProvider = context.read(); final equipmentProvider = context.read(); final containers = await containerProvider.containersStream.first; final equipment = await equipmentProvider.equipmentStream.first; final container = containers.firstWhere( (c) => c.id == containerId, orElse: () => ContainerModel( id: containerId, name: 'Inconnu', type: ContainerType.flightCase, status: EquipmentStatus.available, equipmentIds: [], createdAt: DateTime.now(), updatedAt: DateTime.now(), ), ); // Mettre à jour le cache _containerEquipmentCache[containerId] = List.from(container.equipmentIds); // Sélectionner chaque enfant (sans bloquer, car ils sont "composés") for (var equipmentId in container.equipmentIds) { if (!_selectedItems.containsKey(equipmentId)) { final eq = equipment.firstWhere( (e) => e.id == equipmentId, orElse: () => EquipmentModel( id: equipmentId, name: 'Inconnu', category: EquipmentCategory.other, status: EquipmentStatus.available, parentBoxIds: [], maintenanceIds: [], createdAt: DateTime.now(), updatedAt: DateTime.now(), ), ); if (mounted) { setState(() { _selectedItems[equipmentId] = SelectedItem( id: equipmentId, name: eq.id, type: SelectionType.equipment, quantity: 1, ); }); } } } } catch (e) { print('Error selecting container children: $e'); } } /// Désélectionner tous les enfants d'un conteneur Future _deselectContainerChildren(String containerId) async { try { final containerProvider = context.read(); final containers = await containerProvider.containersStream.first; final container = containers.firstWhere( (c) => c.id == containerId, orElse: () => ContainerModel( id: containerId, name: 'Inconnu', type: ContainerType.flightCase, status: EquipmentStatus.available, equipmentIds: [], createdAt: DateTime.now(), updatedAt: DateTime.now(), ), ); // Retirer les enfants de _selectedItems for (var equipmentId in container.equipmentIds) { _selectedItems.remove(equipmentId); } // Nettoyer le cache _containerEquipmentCache.remove(containerId); // Retirer de la liste des conteneurs expandés _expandedContainers.remove(containerId); print('[EquipmentSelectionDialog] Deselected container $containerId and ${container.equipmentIds.length} children'); } catch (e) { print('Error deselecting container children: $e'); } } /// Affiche un dialog pour confirmer le forçage d'un équipement en conflit Future _showForceConfirmationDialog(String equipmentId) async { final conflicts = _equipmentConflicts[equipmentId] ?? []; return showDialog( context: context, builder: (context) => AlertDialog( title: const Row( children: [ Icon(Icons.warning, color: Colors.orange), SizedBox(width: 8), 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) :'), 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), ), ], ), ), ], ), )), ], ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(false), child: const Text('Annuler'), ), ElevatedButton( onPressed: () => Navigator.of(context).pop(true), style: ElevatedButton.styleFrom( backgroundColor: Colors.orange, foregroundColor: Colors.white, ), child: const Text('Forcer quand même'), ), ], ), ); } @override Widget build(BuildContext context) { final screenSize = MediaQuery.of(context).size; final dialogWidth = screenSize.width * 0.9; final dialogHeight = screenSize.height * 0.85; return Dialog( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), child: Container( width: dialogWidth.clamp(600.0, 1200.0), height: dialogHeight.clamp(500.0, 900.0), child: Column( children: [ _buildHeader(), _buildSearchAndFilters(), Expanded( child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Liste principale Expanded( flex: 2, child: _buildMainList(), ), // Panneau latéral : sélection + recommandations Container( width: 320, decoration: BoxDecoration( color: Colors.grey.shade50, border: Border( left: BorderSide(color: Colors.grey.shade300), ), ), child: Column( children: [ Expanded(child: _buildSelectionPanel()), if (_hasRecommendations) Container( height: 200, decoration: BoxDecoration( border: Border( top: BorderSide(color: Colors.grey.shade300), ), ), child: _buildRecommendationsPanel(), ), ], ), ), ], ), ), _buildFooter(), ], ), ), ); } Widget _buildHeader() { return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: AppColors.rouge, borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), ), child: Row( children: [ const Icon(Icons.add_circle, color: Colors.white, size: 28), const SizedBox(width: 12), const Expanded( child: Text( 'Ajouter du matériel', style: TextStyle( color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold, ), ), ), IconButton( icon: const Icon(Icons.close, color: Colors.white), onPressed: () => Navigator.of(context).pop(), ), ], ), ); } Widget _buildSearchAndFilters() { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, border: Border(bottom: BorderSide(color: Colors.grey.shade300)), ), child: Column( children: [ // Barre de recherche TextField( controller: _searchController, decoration: InputDecoration( hintText: 'Rechercher du matériel ou des boîtes...', prefixIcon: const Icon(Icons.search, color: AppColors.rouge), suffixIcon: _searchQuery.isNotEmpty ? IconButton( icon: const Icon(Icons.clear), onPressed: () { _searchController.clear(); setState(() => _searchQuery = ''); }, ) : null, border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), ), contentPadding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), ), onChanged: (value) { setState(() => _searchQuery = value.toLowerCase()); }, ), const SizedBox(height: 12), // Filtres par catégorie (pour les équipements) SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: [ _buildFilterChip('Tout', null), const SizedBox(width: 8), ...EquipmentCategory.values.map((category) { return Padding( padding: const EdgeInsets.only(right: 8), child: _buildFilterChip(category.label, category), ); }), ], ), ), ], ), ); } Widget _buildFilterChip(String label, EquipmentCategory? category) { final isSelected = _selectedCategory == category; return FilterChip( label: Text(label), selected: isSelected, onSelected: (selected) { setState(() { _selectedCategory = selected ? category : null; }); }, selectedColor: AppColors.rouge, checkmarkColor: Colors.white, labelStyle: TextStyle( color: isSelected ? Colors.white : Colors.black87, ), ); } Widget _buildMainList() { // Afficher un indicateur de chargement si les données sont en cours de chargement if (_isLoadingQuantities || _isLoadingConflicts) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const CircularProgressIndicator(color: AppColors.rouge), const SizedBox(height: 16), Text( _isLoadingConflicts ? 'Vérification de la disponibilité...' : 'Chargement des quantités disponibles...', style: TextStyle(color: Colors.grey.shade600), ), ], ), ); } // Vue hiérarchique unique : Boîtes en haut, TOUS les équipements en bas return _buildHierarchicalList(); } /// Vue hiérarchique unique Widget _buildHierarchicalList() { return Consumer2( builder: (context, containerProvider, equipmentProvider, child) { return StreamBuilder>( stream: containerProvider.containersStream, builder: (context, containerSnapshot) { return StreamBuilder>( stream: equipmentProvider.equipmentStream, builder: (context, equipmentSnapshot) { if (containerSnapshot.connectionState == ConnectionState.waiting || equipmentSnapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); } final allContainers = containerSnapshot.data ?? []; final allEquipment = equipmentSnapshot.data ?? []; // Filtrage des boîtes final filteredContainers = allContainers.where((container) { if (_searchQuery.isNotEmpty) { final searchLower = _searchQuery.toLowerCase(); return container.id.toLowerCase().contains(searchLower) || container.name.toLowerCase().contains(searchLower); } return true; }).toList(); // Filtrage des équipements (TOUS, pas seulement les orphelins) final filteredEquipment = allEquipment.where((eq) { // Filtre par catégorie if (_selectedCategory != null && eq.category != _selectedCategory) { return false; } // Filtre par recherche if (_searchQuery.isNotEmpty) { final searchLower = _searchQuery.toLowerCase(); return eq.id.toLowerCase().contains(searchLower) || (eq.brand?.toLowerCase().contains(searchLower) ?? false) || (eq.model?.toLowerCase().contains(searchLower) ?? false); } return true; }).toList(); return ListView( padding: const EdgeInsets.all(16), children: [ // SECTION 1 : BOÎTES if (filteredContainers.isNotEmpty) ...[ _buildSectionHeader('Boîtes', Icons.inventory, filteredContainers.length), const SizedBox(height: 12), ...filteredContainers.map((container) => _buildContainerCard( container, key: ValueKey('container_${container.id}'), )), const SizedBox(height: 24), ], // SECTION 2 : TOUS LES ÉQUIPEMENTS if (filteredEquipment.isNotEmpty) ...[ _buildSectionHeader('Tous les équipements', Icons.inventory_2, filteredEquipment.length), const SizedBox(height: 12), ...filteredEquipment.map((equipment) => _buildEquipmentCard( equipment, key: ValueKey('equipment_${equipment.id}'), )), ], // Message si rien n'est trouvé if (filteredContainers.isEmpty && filteredEquipment.isEmpty) Center( child: Padding( padding: const EdgeInsets.all(32), child: Column( children: [ 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), ), ], ), ), ), ], ); }, ); }, ); }, ); } /// Header de section Widget _buildSectionHeader(String title, IconData icon, int count) { return Container( padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12), decoration: BoxDecoration( color: AppColors.rouge.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8), ), child: Row( children: [ Icon(icon, color: AppColors.rouge, size: 20), const SizedBox(width: 8), Text( title, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: AppColors.rouge, ), ), const Spacer(), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( color: AppColors.rouge, borderRadius: BorderRadius.circular(12), ), child: Text( '$count', style: const TextStyle( color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold, ), ), ), ], ), ); } Widget _buildEquipmentCard(EquipmentModel equipment, {Key? key}) { final isSelected = _selectedItems.containsKey(equipment.id); final isConsumable = equipment.category == EquipmentCategory.consumable || equipment.category == EquipmentCategory.cable; final availableQty = _availableQuantities[equipment.id]; final selectedItem = _selectedItems[equipment.id]; final hasConflict = _equipmentConflicts.containsKey(equipment.id); final conflicts = _equipmentConflicts[equipment.id] ?? []; // Bloquer la sélection si en conflit et non forcé final canSelect = !hasConflict || 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: Colors.orange.shade300, width: 1) : BorderSide.none, ), child: InkWell( onTap: canSelect ? () => _toggleSelection( equipment.id, equipment.id, SelectionType.equipment, maxQuantity: availableQty, ) : null, borderRadius: BorderRadius.circular(8), child: Container( decoration: hasConflict && !isSelected ? BoxDecoration( color: Colors.orange.shade50, borderRadius: BorderRadius.circular(8), ) : null, 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, ), const SizedBox(width: 12), // Icône equipment.category.getIcon(size: 32, color: equipment.category.color), 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 && availableQty != null) Padding( padding: const EdgeInsets.only(top: 4), child: Text( 'Disponible : $availableQty ${equipment.category == EquipmentCategory.cable ? "m" : ""}', style: TextStyle( color: availableQty > 0 ? Colors.green : Colors.red, fontWeight: FontWeight.w500, fontSize: 13, ), ), ), ], ), ), // Sélecteur de quantité pour consommables if (isSelected && isConsumable && availableQty != null) _buildQuantitySelector(equipment.id, selectedItem!, availableQty), ], ), // 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 ${conflicts.length} événement(s) :', style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: Colors.orange.shade900, ), ), ], ), const SizedBox(height: 6), ...conflicts.take(2).map((conflict) => Padding( padding: const EdgeInsets.only(left: 22, top: 4), child: Text( '• ${conflict.conflictingEvent.name} (${conflict.overlapDays} jour(s))', style: TextStyle( fontSize: 11, color: Colors.orange.shade800, ), ), )), if (conflicts.length > 2) Padding( padding: const EdgeInsets.only(left: 22, top: 4), child: Text( '... et ${conflicts.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é (sans setState global pour éviter le refresh) Widget _buildQuantitySelector(String equipmentId, SelectedItem selectedItem, int maxQuantity) { return Container( width: 120, child: Row( children: [ IconButton( icon: const Icon(Icons.remove_circle_outline), onPressed: selectedItem.quantity > 1 ? () { setState(() { _selectedItems[equipmentId] = selectedItem.copyWith(quantity: selectedItem.quantity - 1); }); } : null, iconSize: 20, ), Expanded( child: Text( '${selectedItem.quantity}', textAlign: TextAlign.center, style: const TextStyle(fontWeight: FontWeight.bold), ), ), IconButton( icon: const Icon(Icons.add_circle_outline), onPressed: selectedItem.quantity < maxQuantity ? () { setState(() { _selectedItems[equipmentId] = selectedItem.copyWith(quantity: selectedItem.quantity + 1); }); } : null, iconSize: 20, ), ], ), ); } 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; // 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, 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, 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: TextStyle( fontSize: 13, fontWeight: FontWeight.w500, color: Colors.blue.shade700, ), ), 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: () { setState(() { if (isExpanded) { _expandedContainers.remove(container.id); } else { _expandedContainers.add(container.id); } }); }, 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), ], ), ), ), ); } /// Widget pour afficher les équipements enfants d'un conteneur Widget _buildContainerChildren(ContainerModel container, ContainerConflictInfo? conflictInfo) { return Consumer( builder: (context, provider, child) { return StreamBuilder>( stream: provider.equipmentStream, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Padding( padding: EdgeInsets.all(16), child: Center(child: CircularProgressIndicator()), ); } final allEquipment = snapshot.data ?? []; final childEquipments = allEquipment .where((eq) => container.equipmentIds.contains(eq.id)) .toList(); if (childEquipments.isEmpty) { return Container( decoration: BoxDecoration( color: Colors.grey.shade50, border: Border(top: BorderSide(color: Colors.grey.shade300)), ), padding: const EdgeInsets.all(16), child: Row( children: [ Icon(Icons.info_outline, size: 16, color: Colors.grey.shade600), const SizedBox(width: 8), Text( 'Aucun équipement dans ce conteneur', style: TextStyle(color: Colors.grey.shade600, fontSize: 13), ), ], ), ); } return Container( decoration: BoxDecoration( color: Colors.grey.shade50, border: Border(top: BorderSide(color: Colors.grey.shade300)), ), padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(Icons.list, size: 16, color: Colors.grey.shade700), const SizedBox(width: 6), Text( 'Contenu de la boîte :', style: TextStyle( fontSize: 13, color: Colors.grey.shade700, fontWeight: FontWeight.bold, ), ), ], ), const SizedBox(height: 12), ...childEquipments.map((eq) { 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, ), ), 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, ), ), ], ), ), ), ], ], ), ); }), ], ), ); }, ); }, ); } Widget _buildSelectionPanel() { // 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 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é final selectedStandaloneEquipment = _selectedItems.entries .where((e) => e.value.type == SelectionType.equipment) .where((e) => !equipmentIdsInContainers.contains(e.key)) .toList(); final containerCount = selectedContainers.length; final standaloneEquipmentCount = selectedStandaloneEquipment.length; final totalDisplayed = containerCount + standaloneEquipmentCount; return Column( children: [ Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: AppColors.rouge, ), child: Row( children: [ const Icon(Icons.check_circle, color: Colors.white), const SizedBox(width: 8), const Text( 'Sélection', style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16, ), ), const Spacer(), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), ), child: Text( '$totalDisplayed', style: const TextStyle( color: AppColors.rouge, fontWeight: FontWeight.bold, ), ), ), ], ), ), Expanded( child: totalDisplayed == 0 ? const Center( child: Text( 'Aucune sélection', style: TextStyle(color: Colors.grey), ), ) : ListView( padding: const EdgeInsets.all(8), children: [ if (containerCount > 0) ...[ Padding( padding: const EdgeInsets.all(8), child: Text( 'Boîtes ($containerCount)', style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 14, ), ), ), ...selectedContainers.map((e) => _buildSelectedContainerTile(e.key, e.value)), ], if (standaloneEquipmentCount > 0) ...[ Padding( padding: const EdgeInsets.all(8), child: Text( 'Équipements ($standaloneEquipmentCount)', style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 14, ), ), ), ...selectedStandaloneEquipment.map((e) => _buildSelectedItemTile(e.key, e.value)), ], ], ), ), ], ); } /// 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 // Pour cela, on va maintenir un cache local return _containerEquipmentCache[containerId] ?? []; } // Cache local pour les équipements des conteneurs Map> _containerEquipmentCache = {}; Widget _buildSelectedContainerTile(String id, SelectedItem item) { final isExpanded = _expandedContainers.contains(id); final childrenIds = _getContainerEquipmentIds(id); final childrenCount = childrenIds.length; return Column( children: [ ListTile( dense: true, leading: Icon( Icons.inventory, size: 20, color: AppColors.rouge, ), title: Text( item.name, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.bold), ), subtitle: Text( '$childrenCount équipement(s)', style: const TextStyle(fontSize: 11), ), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ if (childrenCount > 0) IconButton( icon: Icon( isExpanded ? Icons.expand_less : Icons.expand_more, size: 18, ), onPressed: () { setState(() { if (isExpanded) { _expandedContainers.remove(id); } else { _expandedContainers.add(id); } }); }, ), IconButton( icon: const Icon(Icons.close, size: 18), onPressed: () => _toggleSelection(id, item.name, item.type), ), ], ), ), if (isExpanded && childrenCount > 0) ...childrenIds.map((equipmentId) { final childItem = _selectedItems[equipmentId]; if (childItem != null) { return _buildSelectedChildEquipmentTile(equipmentId, childItem); } return const SizedBox.shrink(); }).toList(), ], ); } Widget _buildSelectedChildEquipmentTile(String id, SelectedItem item) { return Padding( padding: const EdgeInsets.only(left: 40), child: ListTile( dense: true, leading: Icon( Icons.subdirectory_arrow_right, size: 16, color: Colors.grey.shade600, ), title: Text( item.name, style: TextStyle(fontSize: 12, color: Colors.grey.shade700), ), subtitle: item.quantity > 1 ? Text('Qté: ${item.quantity}', style: const TextStyle(fontSize: 10)) : null, // PAS de bouton de suppression pour les enfants ), ); } Widget _buildSelectedItemTile(String id, SelectedItem item) { return ListTile( dense: true, leading: Icon( Icons.inventory_2, size: 20, color: AppColors.rouge, ), title: Text( item.name, style: const TextStyle(fontSize: 13), ), subtitle: item.quantity > 1 ? Text('Qté: ${item.quantity}', style: const TextStyle(fontSize: 11)) : null, trailing: IconButton( icon: const Icon(Icons.close, size: 18), onPressed: () => _toggleSelection(id, item.name, item.type), ), ); } bool get _hasRecommendations => _recommendedContainers.isNotEmpty; Widget _buildRecommendationsPanel() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.blue.shade700, ), child: const Row( children: [ Icon(Icons.lightbulb, color: Colors.white, size: 20), SizedBox(width: 8), Text( 'Boîtes recommandées', style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold, ), ), ], ), ), Expanded( child: ListView( padding: const EdgeInsets.all(8), children: _recommendedContainers.values .expand((list) => list) .toSet() // Enlever les doublons .map((container) => _buildRecommendedContainerTile(container)) .toList(), ), ), ], ); } Widget _buildRecommendedContainerTile(ContainerModel container) { final isAlreadySelected = _selectedItems.containsKey(container.id); return Card( margin: const EdgeInsets.only(bottom: 8), child: ListTile( dense: true, leading: container.type.getIcon(size: 24, color: Colors.blue.shade700), title: Text( container.name, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.bold), ), subtitle: Text( '${container.itemCount} équipement(s)', style: const TextStyle(fontSize: 11), ), trailing: isAlreadySelected ? const Icon(Icons.check_circle, color: Colors.green) : IconButton( icon: const Icon(Icons.add_circle_outline, color: Colors.blue), onPressed: () => _toggleSelection( container.id, container.name, SelectionType.container, ), ), ), ); } Widget _buildFooter() { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, border: Border(top: BorderSide(color: Colors.grey.shade300)), boxShadow: [ BoxShadow( color: Colors.grey.withValues(alpha: 0.2), spreadRadius: 1, blurRadius: 5, offset: const Offset(0, -2), ), ], ), child: Row( children: [ Text( '${_selectedItems.length} élément(s) sélectionné(s)', style: const TextStyle(fontWeight: FontWeight.w500), ), const Spacer(), OutlinedButton( onPressed: () => Navigator.of(context).pop(), child: const Text('Annuler'), ), const SizedBox(width: 12), ElevatedButton( onPressed: _selectedItems.isEmpty ? null : () => Navigator.of(context).pop(_selectedItems), style: ElevatedButton.styleFrom( backgroundColor: AppColors.rouge, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 14), ), child: const Text('Valider la sélection'), ), ], ), ); } }