feat: Sécurisation Firestore, gestion des prix HT/TTC et refactorisation majeure
Cette mise à jour verrouille l'accès direct à Firestore depuis le client pour renforcer la sécurité et introduit une gestion complète des prix HT/TTC dans toute l'application. Elle apporte également des améliorations significatives des permissions, des optimisations de performance et de nouvelles fonctionnalités.
### Sécurité et Backend
- **Firestore Rules :** Ajout de `firestore.rules` qui bloque par défaut tous les accès en lecture/écriture depuis le client. Toutes les opérations de données doivent maintenant passer par les Cloud Functions, renforçant considérablement la sécurité.
- **Index Firestore :** Création d'un fichier `firestore.indexes.json` pour optimiser les requêtes sur la collection `events`.
- **Cloud Functions :** Les fonctions de création/mise à jour d'événements ont été adaptées pour accepter des ID de documents (utilisateurs, type d'événement) et les convertir en `DocumentReference` côté serveur, simplifiant les appels depuis le client.
### Gestion des Prix HT/TTC
- **Calcul Automatisé :** Introduction d'un helper `PriceHelpers` et d'un widget `PriceHtTtcFields` pour calculer et synchroniser automatiquement les prix HT et TTC dans le formulaire d'événement.
- **Affichage Détaillé :**
- Les détails des événements et des options affichent désormais les prix HT, la TVA et le TTC séparément pour plus de clarté.
- Le prix de base (`basePrice`) est maintenant traité comme un prix TTC dans toute l'application.
### Permissions et Rôles
- **Centralisation (`AppPermission`) :** Création d'une énumération `AppPermission` pour centraliser toutes les permissions de l'application, avec descriptions et catégories.
- **Rôles Prédéfinis :** Définition de rôles standards (Admin, Manager, Technicien, User) avec des jeux de permissions prédéfinis.
- **Filtre par Utilisateur :** Ajout d'un filtre par utilisateur sur la page Calendrier, visible uniquement pour les utilisateurs ayant la permission `view_all_user_events`.
### Améliorations et Optimisations (Frontend)
- **`DebugLog` :** Ajout d'un utilitaire `DebugLog` pour gérer les logs, qui sont automatiquement désactivés en mode production.
- **Optimisation du Sélecteur d'Équipement :**
- La boîte de dialogue de sélection d'équipement a été lourdement optimisée pour éviter les reconstructions complètes de la liste lors de la sélection/désélection d'items.
- Utilisation de `ValueNotifier` et de caches locaux (`_cachedContainers`, `_cachedEquipment`) pour des mises à jour d'UI plus ciblées et fluides.
- La position du scroll est désormais préservée.
- **Catégorie d'Équipement :** Ajout de la catégorie `Vehicle` (Véhicule) pour les équipements.
- **Formulaires :** Les formulaires de création/modification d'événements et d'équipements ont été nettoyés de leurs logs de débogage excessifs.
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import 'package:em2rp/utils/debug_log.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:em2rp/models/equipment_model.dart';
|
||||
@@ -88,12 +89,14 @@ class EquipmentSelectionDialog extends StatefulWidget {
|
||||
|
||||
class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
final ScrollController _scrollController = ScrollController(); // Préserve la position de scroll
|
||||
final EventAvailabilityService _availabilityService = EventAvailabilityService();
|
||||
final DataService _dataService = DataService(apiService);
|
||||
|
||||
EquipmentCategory? _selectedCategory;
|
||||
|
||||
Map<String, SelectedItem> _selectedItems = {};
|
||||
final ValueNotifier<int> _selectionChangeNotifier = ValueNotifier<int>(0); // Pour notifier les changements de sélection sans setState
|
||||
Map<String, int> _availableQuantities = {}; // Pour consommables
|
||||
Map<String, List<ContainerModel>> _recommendedContainers = {}; // Recommandations
|
||||
Map<String, List<AvailabilityConflict>> _equipmentConflicts = {}; // Conflits de disponibilité (détaillés)
|
||||
@@ -108,8 +111,14 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
||||
|
||||
bool _isLoadingQuantities = false;
|
||||
bool _isLoadingConflicts = false;
|
||||
bool _conflictsLoaded = false; // Flag pour éviter de recharger indéfiniment
|
||||
String _searchQuery = '';
|
||||
|
||||
// Cache pour éviter les rebuilds inutiles
|
||||
List<ContainerModel> _cachedContainers = [];
|
||||
List<EquipmentModel> _cachedEquipment = [];
|
||||
bool _initialDataLoaded = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -186,18 +195,19 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
print('[EquipmentSelectionDialog] Error loading already assigned containers: $e');
|
||||
}
|
||||
}
|
||||
|
||||
print('[EquipmentSelectionDialog] Initialized with ${_selectedItems.length} already assigned items');
|
||||
}
|
||||
|
||||
}
|
||||
} catch (e) {
|
||||
DebugLog.error('[EquipmentSelectionDialog] Error loading already assigned containers', e);
|
||||
}
|
||||
}
|
||||
|
||||
DebugLog.info('[EquipmentSelectionDialog] Initialized with ${_selectedItems.length} already assigned items');
|
||||
}
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
_scrollController.dispose(); // Nettoyer le ScrollController
|
||||
_selectionChangeNotifier.dispose(); // Nettoyer le ValueNotifier
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -226,7 +236,7 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
||||
_availableQuantities[eq.id] = available;
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error loading quantities: $e');
|
||||
DebugLog.error('Error loading quantities', e);
|
||||
} finally {
|
||||
if (mounted) setState(() => _isLoadingQuantities = false);
|
||||
}
|
||||
@@ -238,7 +248,7 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
||||
setState(() => _isLoadingConflicts = true);
|
||||
|
||||
try {
|
||||
print('[EquipmentSelectionDialog] Loading conflicts (optimized batch method)...');
|
||||
DebugLog.info('[EquipmentSelectionDialog] Loading conflicts (optimized batch method)...');
|
||||
|
||||
final startTime = DateTime.now();
|
||||
|
||||
@@ -254,7 +264,7 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
||||
final endTime = DateTime.now();
|
||||
final duration = endTime.difference(startTime);
|
||||
|
||||
print('[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<dynamic>?)
|
||||
@@ -268,8 +278,8 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
||||
final conflictDetails = result['conflictDetails'] as Map<String, dynamic>? ?? {};
|
||||
final equipmentQuantities = result['equipmentQuantities'] as Map<String, dynamic>? ?? {};
|
||||
|
||||
print('[EquipmentSelectionDialog] Found ${conflictingEquipmentIds.length} equipment(s) and ${conflictingContainerIds.length} container(s) in conflict');
|
||||
print('[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(() {
|
||||
@@ -277,6 +287,7 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
||||
_conflictingContainerIds = conflictingContainerIds;
|
||||
_conflictDetails = conflictDetails;
|
||||
_equipmentQuantities = equipmentQuantities;
|
||||
_conflictsLoaded = true; // Marquer comme chargé
|
||||
});
|
||||
}
|
||||
|
||||
@@ -284,7 +295,7 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
||||
await _updateContainerConflictStatus();
|
||||
|
||||
} catch (e) {
|
||||
print('[EquipmentSelectionDialog] Error loading conflicts: $e');
|
||||
DebugLog.error('[EquipmentSelectionDialog] Error loading conflicts', e);
|
||||
} finally {
|
||||
if (mounted) setState(() => _isLoadingConflicts = false);
|
||||
}
|
||||
@@ -292,10 +303,14 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
||||
|
||||
/// Met à jour le statut de conflit des conteneurs basé sur les IDs en conflit
|
||||
Future<void> _updateContainerConflictStatus() async {
|
||||
if (!mounted) return; // Vérifier si le widget est toujours monté
|
||||
|
||||
try {
|
||||
final containerProvider = context.read<ContainerProvider>();
|
||||
final containers = await containerProvider.containersStream.first;
|
||||
|
||||
if (!mounted) return; // Vérifier à nouveau après l'async
|
||||
|
||||
for (var container in containers) {
|
||||
// Vérifier si le conteneur lui-même est en conflit
|
||||
if (_conflictingContainerIds.contains(container.id)) {
|
||||
@@ -323,13 +338,13 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
||||
totalChildren: container.equipmentIds.length,
|
||||
);
|
||||
|
||||
print('[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)');
|
||||
}
|
||||
}
|
||||
|
||||
print('[EquipmentSelectionDialog] Total containers with conflicts: ${_containerConflicts.length}');
|
||||
DebugLog.info('[EquipmentSelectionDialog] Total containers with conflicts: ${_containerConflicts.length}');
|
||||
} catch (e) {
|
||||
print('[EquipmentSelectionDialog] Error updating container conflicts: $e');
|
||||
DebugLog.error('[EquipmentSelectionDialog] Error updating container conflicts', e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -355,7 +370,7 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
||||
if (availableQty == null) return const SizedBox.shrink();
|
||||
|
||||
return Text(
|
||||
'Disponible : $availableQty ${equipment.category == EquipmentCategory.cable ? "m" : ""}',
|
||||
'Disponible : $availableQty',
|
||||
style: TextStyle(
|
||||
color: availableQty > 0 ? Colors.green : Colors.red,
|
||||
fontWeight: FontWeight.w500,
|
||||
@@ -580,7 +595,7 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error finding recommended containers: $e');
|
||||
DebugLog.error('Error finding recommended containers', e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -602,26 +617,26 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
||||
|
||||
if (_selectedItems.containsKey(id)) {
|
||||
// Désélectionner
|
||||
print('[EquipmentSelectionDialog] Deselecting $type: $id');
|
||||
print('[EquipmentSelectionDialog] Before deselection, _selectedItems count: ${_selectedItems.length}');
|
||||
DebugLog.info('[EquipmentSelectionDialog] Deselecting $type: $id');
|
||||
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
|
||||
await _deselectContainerChildren(id);
|
||||
}
|
||||
|
||||
// Mise à jour sans setState pour éviter le flash
|
||||
// Mise à jour sans setState - utiliser ValueNotifier pour notifier uniquement les cards concernées
|
||||
_selectedItems.remove(id);
|
||||
print('[EquipmentSelectionDialog] After deselection, _selectedItems count: ${_selectedItems.length}');
|
||||
print('[EquipmentSelectionDialog] Remaining items: ${_selectedItems.keys.toList()}');
|
||||
DebugLog.info('[EquipmentSelectionDialog] After deselection, _selectedItems count: ${_selectedItems.length}');
|
||||
DebugLog.info('[EquipmentSelectionDialog] Remaining items: ${_selectedItems.keys.toList()}');
|
||||
|
||||
// Forcer uniquement la reconstruction du panneau de sélection et de la card concernée
|
||||
if (mounted) setState(() {});
|
||||
// Notifier le changement sans rebuilder toute la liste
|
||||
_selectionChangeNotifier.value++;
|
||||
} else {
|
||||
// Sélectionner
|
||||
print('[EquipmentSelectionDialog] Selecting $type: $id');
|
||||
DebugLog.info('[EquipmentSelectionDialog] Selecting $type: $id');
|
||||
|
||||
// Mise à jour sans setState pour éviter le flash
|
||||
// Mise à jour sans setState - utiliser ValueNotifier
|
||||
_selectedItems[id] = SelectedItem(
|
||||
id: id,
|
||||
name: name,
|
||||
@@ -639,8 +654,8 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
||||
await _selectContainerChildren(id);
|
||||
}
|
||||
|
||||
// Forcer uniquement la reconstruction du panneau de sélection et de la card concernée
|
||||
if (mounted) setState(() {});
|
||||
// Notifier le changement sans rebuilder toute la liste
|
||||
_selectionChangeNotifier.value++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -699,7 +714,7 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error selecting container children: $e');
|
||||
DebugLog.error('Error selecting container children', e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -733,9 +748,9 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
||||
// Retirer de la liste des conteneurs expandés
|
||||
_expandedContainers.remove(containerId);
|
||||
|
||||
print('[EquipmentSelectionDialog] Deselected container $containerId and ${container.equipmentIds.length} children');
|
||||
DebugLog.info('[EquipmentSelectionDialog] Deselected container $containerId and ${container.equipmentIds.length} children');
|
||||
} catch (e) {
|
||||
print('Error deselecting container children: $e');
|
||||
DebugLog.error('Error deselecting container children', e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -843,7 +858,12 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(child: _buildSelectionPanel()),
|
||||
Expanded(
|
||||
child: ValueListenableBuilder<int>(
|
||||
valueListenable: _selectionChangeNotifier,
|
||||
builder: (context, _, __) => _buildSelectionPanel(),
|
||||
),
|
||||
),
|
||||
if (_hasRecommendations)
|
||||
Container(
|
||||
height: 200,
|
||||
@@ -997,34 +1017,41 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
||||
return _buildHierarchicalList();
|
||||
}
|
||||
|
||||
/// Vue hiérarchique unique
|
||||
/// Vue hiérarchique unique avec cache pour éviter les rebuilds inutiles
|
||||
Widget _buildHierarchicalList() {
|
||||
return Consumer2<ContainerProvider, EquipmentProvider>(
|
||||
builder: (context, containerProvider, equipmentProvider, child) {
|
||||
return StreamBuilder<List<ContainerModel>>(
|
||||
stream: containerProvider.containersStream,
|
||||
builder: (context, containerSnapshot) {
|
||||
return StreamBuilder<List<EquipmentModel>>(
|
||||
stream: equipmentProvider.equipmentStream,
|
||||
builder: (context, equipmentSnapshot) {
|
||||
if (containerSnapshot.connectionState == ConnectionState.waiting ||
|
||||
equipmentSnapshot.connectionState == ConnectionState.waiting) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
// Charger les données initiales dans le cache si pas encore fait
|
||||
if (!_initialDataLoaded) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_cachedContainers = containerProvider.containers;
|
||||
_cachedEquipment = equipmentProvider.equipment;
|
||||
_initialDataLoaded = true;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
final allContainers = containerSnapshot.data ?? [];
|
||||
final allEquipment = equipmentSnapshot.data ?? [];
|
||||
// Utiliser les données du cache au lieu des streams
|
||||
final allContainers = _cachedContainers.isNotEmpty ? _cachedContainers : containerProvider.containers;
|
||||
final allEquipment = _cachedEquipment.isNotEmpty ? _cachedEquipment : equipmentProvider.equipment;
|
||||
|
||||
// Charger les conflits une seule fois après le chargement des données
|
||||
if (!_isLoadingConflicts && _conflictingEquipmentIds.isEmpty && allEquipment.isNotEmpty) {
|
||||
// Lancer le chargement des conflits en arrière-plan
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_loadEquipmentConflicts();
|
||||
});
|
||||
}
|
||||
// Charger les conflits une seule fois après le chargement des données
|
||||
if (!_isLoadingConflicts && !_conflictsLoaded && allEquipment.isNotEmpty) {
|
||||
// Lancer le chargement des conflits en arrière-plan
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_loadEquipmentConflicts();
|
||||
});
|
||||
}
|
||||
|
||||
// Filtrage des boîtes
|
||||
final filteredContainers = allContainers.where((container) {
|
||||
// Utiliser ValueListenableBuilder pour rebuild uniquement sur changement de sélection
|
||||
return ValueListenableBuilder<int>(
|
||||
valueListenable: _selectionChangeNotifier,
|
||||
builder: (context, _, __) {
|
||||
// Filtrage des boîtes
|
||||
final filteredContainers = allContainers.where((container) {
|
||||
if (_searchQuery.isNotEmpty) {
|
||||
final searchLower = _searchQuery.toLowerCase();
|
||||
return container.id.toLowerCase().contains(searchLower) ||
|
||||
@@ -1052,7 +1079,9 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
||||
}).toList();
|
||||
|
||||
return ListView(
|
||||
controller: _scrollController, // Préserve la position de scroll
|
||||
padding: const EdgeInsets.all(16),
|
||||
cacheExtent: 1000, // Cache plus d'items pour éviter les rebuilds lors du scroll
|
||||
children: [
|
||||
// SECTION 1 : BOÎTES
|
||||
if (filteredContainers.isNotEmpty) ...[
|
||||
@@ -1094,10 +1123,8 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
); // Fin du ValueListenableBuilder
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -1304,9 +1331,19 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
||||
),
|
||||
),
|
||||
|
||||
// Sélecteur de quantité pour consommables
|
||||
if (isSelected && isConsumable && availableQty != null)
|
||||
_buildQuantitySelector(equipment.id, selectedItem!, availableQty),
|
||||
// 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
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -1414,40 +1451,62 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
||||
);
|
||||
}
|
||||
|
||||
/// Widget pour le sélecteur de quantité (sans setState global pour éviter le refresh)
|
||||
Widget _buildQuantitySelector(String equipmentId, SelectedItem selectedItem, int maxQuantity) {
|
||||
/// 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,
|
||||
int maxQuantity, {
|
||||
required bool isSelected,
|
||||
}) {
|
||||
final displayQuantity = isSelected ? selectedItem.quantity : 0;
|
||||
|
||||
return Container(
|
||||
width: 120,
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.remove_circle_outline),
|
||||
onPressed: selectedItem.quantity > 1
|
||||
onPressed: isSelected && selectedItem.quantity > 1
|
||||
? () {
|
||||
setState(() {
|
||||
_selectedItems[equipmentId] = selectedItem.copyWith(quantity: selectedItem.quantity - 1);
|
||||
});
|
||||
_selectedItems[equipmentId] = selectedItem.copyWith(quantity: selectedItem.quantity - 1);
|
||||
_selectionChangeNotifier.value++; // Notifier sans rebuild complet
|
||||
}
|
||||
: null,
|
||||
iconSize: 20,
|
||||
color: isSelected && selectedItem.quantity > 1 ? AppColors.rouge : Colors.grey,
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'${selectedItem.quantity}',
|
||||
'$displayQuantity',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isSelected ? Colors.black : Colors.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add_circle_outline),
|
||||
onPressed: selectedItem.quantity < maxQuantity
|
||||
onPressed: (isSelected && selectedItem.quantity < maxQuantity) || !isSelected
|
||||
? () {
|
||||
setState(() {
|
||||
if (!isSelected) {
|
||||
// Premier clic : sélectionner avec quantité 1
|
||||
_toggleSelection(
|
||||
equipmentId,
|
||||
selectedItem.name,
|
||||
SelectionType.equipment,
|
||||
maxQuantity: maxQuantity,
|
||||
);
|
||||
} else {
|
||||
// Item déjà sélectionné : incrémenter
|
||||
_selectedItems[equipmentId] = selectedItem.copyWith(quantity: selectedItem.quantity + 1);
|
||||
});
|
||||
_selectionChangeNotifier.value++; // Notifier sans rebuild complet
|
||||
}
|
||||
}
|
||||
: null,
|
||||
iconSize: 20,
|
||||
color: AppColors.rouge,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -1635,13 +1694,12 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
||||
color: AppColors.rouge,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
if (isExpanded) {
|
||||
_expandedContainers.remove(container.id);
|
||||
} else {
|
||||
_expandedContainers.add(container.id);
|
||||
}
|
||||
});
|
||||
if (isExpanded) {
|
||||
_expandedContainers.remove(container.id);
|
||||
} else {
|
||||
_expandedContainers.add(container.id);
|
||||
}
|
||||
_selectionChangeNotifier.value++; // Notifier sans rebuild complet
|
||||
},
|
||||
tooltip: isExpanded ? 'Replier' : 'Voir le contenu',
|
||||
),
|
||||
@@ -1996,13 +2054,12 @@ class _EquipmentSelectionDialogState extends State<EquipmentSelectionDialog> {
|
||||
size: 18,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
if (isExpanded) {
|
||||
_expandedContainers.remove(id);
|
||||
} else {
|
||||
_expandedContainers.add(id);
|
||||
}
|
||||
});
|
||||
if (isExpanded) {
|
||||
_expandedContainers.remove(id);
|
||||
} else {
|
||||
_expandedContainers.add(id);
|
||||
}
|
||||
_selectionChangeNotifier.value++; // Notifier sans rebuild complet
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
|
||||
Reference in New Issue
Block a user