Cette mise à jour refactorise en profondeur la boîte de dialogue de sélection d'équipement pour améliorer ses performances, sa stabilité et son ergonomie. Le chargement des données a été entièrement revu pour être plus robuste et les interactions utilisateur sont désormais plus fluides et intuitives. **Améliorations de performance et de stabilité :** - **Chargement de données asynchrone et robuste :** Remplacement de la logique de chargement dans `initState` par une nouvelle méthode `_initializeData`. Cette méthode force le chargement des équipements et des conteneurs via `ensureLoaded()` et attend activement leur complétion avant de poursuivre, garantissant ainsi que toutes les données sont disponibles avant l'affichage. - **Mise en cache locale :** Les données des équipements et conteneurs sont mises en cache (`_cachedEquipment`, `_cachedContainers`) après le chargement initial. Toute la boîte de dialogue utilise désormais ce cache, éliminant les appels répétés aux `Stream` et réduisant les rebuilds inutiles. - **Fiabilisation des `setState` :** Les modifications de la sélection (`_selectedItems`) sont maintenant systématiquement wrappées dans des appels `setState` pour assurer que l'interface graphique se met à jour de manière fiable, corrigeant des bugs où la sélection n'était pas reflétée visuellement. **Nouvelles fonctionnalités et améliorations de l'UX :** - **Sections dépliables :** Les listes "Boîtes" et "Tous les équipements" sont désormais dans des sections qui peuvent être repliées, permettant à l'utilisateur de se concentrer sur l'une ou l'autre et d'améliorer la lisibilité sur les petits écrans. - **Option d'affichage des conflits :** Ajout d'une checkbox "Afficher les équipements déjà utilisés". Lorsqu'elle est décochée, tous les équipements et boîtes en conflit avec les dates de l'événement sont masqués, simplifiant la recherche de matériel disponible. - **Meilleure gestion des filtres :** Le filtre par catégorie s'applique désormais aussi aux boîtes, n'affichant que celles qui contiennent au moins un équipement de la catégorie sélectionnée. - **Notifications de sélection affinées :** Le compteur dans le pied de page (`_selectedItems.length`) est maintenant mis à jour en temps réel à chaque modification de la sélection grâce à un `ValueListenableBuilder`. **Refactorisation mineure :** - **`EquipmentProvider` :** Ajout d'un getter `allEquipment` pour fournir un accès à la liste complète des équipements, non filtrée, utilisée par la boîte de dialogue pour sa logique de cache et de filtrage.
298 lines
9.0 KiB
Dart
298 lines
9.0 KiB
Dart
import 'package:flutter/foundation.dart';
|
|
import 'package:em2rp/models/equipment_model.dart';
|
|
import 'package:em2rp/services/data_service.dart';
|
|
import 'package:em2rp/services/api_service.dart';
|
|
|
|
class EquipmentProvider extends ChangeNotifier {
|
|
final DataService _dataService = DataService(FirebaseFunctionsApiService());
|
|
|
|
List<EquipmentModel> _equipment = [];
|
|
List<String> _models = [];
|
|
List<String> _brands = [];
|
|
|
|
EquipmentCategory? _selectedCategory;
|
|
EquipmentStatus? _selectedStatus;
|
|
String? _selectedModel;
|
|
String _searchQuery = '';
|
|
bool _isLoading = false;
|
|
bool _isInitialized = false; // Flag pour savoir si les équipements ont été chargés
|
|
|
|
// Constructeur - Ne charge PAS automatiquement
|
|
// Les équipements seront chargés à la demande (page de gestion ou via getEquipmentsByIds)
|
|
EquipmentProvider();
|
|
|
|
// Getters
|
|
List<EquipmentModel> get equipment => _filteredEquipment;
|
|
List<EquipmentModel> get allEquipment => _equipment; // Tous les équipements sans filtre
|
|
List<String> get models => _models;
|
|
List<String> get brands => _brands;
|
|
EquipmentCategory? get selectedCategory => _selectedCategory;
|
|
EquipmentStatus? get selectedStatus => _selectedStatus;
|
|
String? get selectedModel => _selectedModel;
|
|
String get searchQuery => _searchQuery;
|
|
bool get isLoading => _isLoading;
|
|
bool get isInitialized => _isInitialized;
|
|
|
|
/// S'assure que les équipements sont chargés (charge si nécessaire)
|
|
Future<void> ensureLoaded() async {
|
|
if (_isInitialized || _isLoading) {
|
|
print('[EquipmentProvider] Equipment already loaded or loading, skipping...');
|
|
return;
|
|
}
|
|
print('[EquipmentProvider] Equipment not loaded, loading now...');
|
|
await loadEquipments();
|
|
}
|
|
|
|
/// Charger tous les équipements via l'API (utilisé par la page de gestion)
|
|
Future<void> loadEquipments() async {
|
|
print('[EquipmentProvider] Starting to load equipments...');
|
|
_isLoading = true;
|
|
notifyListeners();
|
|
|
|
try {
|
|
print('[EquipmentProvider] Calling getEquipments API...');
|
|
final equipmentsData = await _dataService.getEquipments();
|
|
print('[EquipmentProvider] Received ${equipmentsData.length} equipments from API');
|
|
|
|
_equipment = equipmentsData.map((data) {
|
|
return EquipmentModel.fromMap(data, data['id'] as String);
|
|
}).toList();
|
|
print('[EquipmentProvider] Mapped ${_equipment.length} equipment models');
|
|
|
|
// Extraire les modèles et marques uniques
|
|
_extractUniqueValues();
|
|
|
|
_isInitialized = true;
|
|
|
|
_isLoading = false;
|
|
notifyListeners();
|
|
print('[EquipmentProvider] Equipment loading complete');
|
|
} catch (e) {
|
|
print('[EquipmentProvider] Error loading equipments: $e');
|
|
_isLoading = false;
|
|
notifyListeners();
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
/// Charge plusieurs équipements par leurs IDs (optimisé pour les détails d'événement)
|
|
Future<List<EquipmentModel>> getEquipmentsByIds(List<String> equipmentIds) async {
|
|
if (equipmentIds.isEmpty) return [];
|
|
|
|
print('[EquipmentProvider] Loading ${equipmentIds.length} equipments by IDs...');
|
|
|
|
try {
|
|
// Vérifier d'abord le cache local
|
|
final cachedEquipments = <EquipmentModel>[];
|
|
final missingIds = <String>[];
|
|
|
|
for (final id in equipmentIds) {
|
|
final cached = _equipment.firstWhere(
|
|
(eq) => eq.id == id,
|
|
orElse: () => EquipmentModel(
|
|
id: '',
|
|
name: '',
|
|
category: EquipmentCategory.other,
|
|
status: EquipmentStatus.available,
|
|
parentBoxIds: [],
|
|
maintenanceIds: [],
|
|
createdAt: DateTime.now(),
|
|
updatedAt: DateTime.now(),
|
|
),
|
|
);
|
|
|
|
if (cached.id.isNotEmpty) {
|
|
cachedEquipments.add(cached);
|
|
} else {
|
|
missingIds.add(id);
|
|
}
|
|
}
|
|
|
|
print('[EquipmentProvider] Found ${cachedEquipments.length} in cache, ${missingIds.length} missing');
|
|
|
|
// Si tous sont en cache, retourner directement
|
|
if (missingIds.isEmpty) {
|
|
return cachedEquipments;
|
|
}
|
|
|
|
// Charger les manquants depuis l'API
|
|
final equipmentsData = await _dataService.getEquipmentsByIds(missingIds);
|
|
|
|
final loadedEquipments = equipmentsData.map((data) {
|
|
return EquipmentModel.fromMap(data, data['id'] as String);
|
|
}).toList();
|
|
|
|
// Ajouter au cache
|
|
for (final eq in loadedEquipments) {
|
|
if (!_equipment.any((e) => e.id == eq.id)) {
|
|
_equipment.add(eq);
|
|
}
|
|
}
|
|
|
|
print('[EquipmentProvider] Loaded ${loadedEquipments.length} equipments from API');
|
|
|
|
// Retourner tous les équipements (cache + chargés)
|
|
return [...cachedEquipments, ...loadedEquipments];
|
|
} catch (e) {
|
|
print('[EquipmentProvider] Error loading equipments by IDs: $e');
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
/// Extraire modèles et marques uniques
|
|
void _extractUniqueValues() {
|
|
final modelSet = <String>{};
|
|
final brandSet = <String>{};
|
|
|
|
for (final eq in _equipment) {
|
|
if (eq.model != null && eq.model!.isNotEmpty) {
|
|
modelSet.add(eq.model!);
|
|
}
|
|
if (eq.brand != null && eq.brand!.isNotEmpty) {
|
|
brandSet.add(eq.brand!);
|
|
}
|
|
}
|
|
|
|
_models = modelSet.toList()..sort();
|
|
_brands = brandSet.toList()..sort();
|
|
}
|
|
|
|
/// Obtenir les équipements filtrés
|
|
List<EquipmentModel> get _filteredEquipment {
|
|
var filtered = _equipment;
|
|
|
|
if (_selectedCategory != null) {
|
|
filtered = filtered.where((eq) => eq.category == _selectedCategory).toList();
|
|
}
|
|
|
|
if (_selectedStatus != null) {
|
|
filtered = filtered.where((eq) => eq.status == _selectedStatus).toList();
|
|
}
|
|
|
|
if (_selectedModel != null && _selectedModel!.isNotEmpty) {
|
|
filtered = filtered.where((eq) => eq.model == _selectedModel).toList();
|
|
}
|
|
|
|
if (_searchQuery.isNotEmpty) {
|
|
final query = _searchQuery.toLowerCase();
|
|
filtered = filtered.where((eq) {
|
|
return eq.name.toLowerCase().contains(query) ||
|
|
eq.id.toLowerCase().contains(query) ||
|
|
(eq.model?.toLowerCase().contains(query) ?? false) ||
|
|
(eq.brand?.toLowerCase().contains(query) ?? false);
|
|
}).toList();
|
|
}
|
|
|
|
return filtered;
|
|
}
|
|
|
|
/// Définir le filtre de catégorie
|
|
void setSelectedCategory(EquipmentCategory? category) {
|
|
_selectedCategory = category;
|
|
notifyListeners();
|
|
}
|
|
|
|
/// Définir le filtre de statut
|
|
void setSelectedStatus(EquipmentStatus? status) {
|
|
_selectedStatus = status;
|
|
notifyListeners();
|
|
}
|
|
|
|
/// Définir le filtre de modèle
|
|
void setSelectedModel(String? model) {
|
|
_selectedModel = model;
|
|
notifyListeners();
|
|
}
|
|
|
|
/// Définir la requête de recherche
|
|
void setSearchQuery(String query) {
|
|
_searchQuery = query;
|
|
notifyListeners();
|
|
}
|
|
|
|
/// Réinitialiser tous les filtres
|
|
void clearFilters() {
|
|
_selectedCategory = null;
|
|
_selectedStatus = null;
|
|
_selectedModel = null;
|
|
_searchQuery = '';
|
|
notifyListeners();
|
|
}
|
|
|
|
/// Recharger les équipements
|
|
Future<void> refresh() async {
|
|
await loadEquipments();
|
|
}
|
|
|
|
// === MÉTHODES STREAM (COMPATIBILITÉ) ===
|
|
|
|
/// Stream des équipements (pour compatibilité avec ancien code)
|
|
Stream<List<EquipmentModel>> get equipmentStream async* {
|
|
yield _equipment;
|
|
}
|
|
|
|
/// Supprimer un équipement
|
|
Future<void> deleteEquipment(String equipmentId) async {
|
|
try {
|
|
await _dataService.deleteEquipment(equipmentId);
|
|
await loadEquipments(); // Recharger la liste
|
|
} catch (e) {
|
|
print('Error deleting equipment: $e');
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
/// Ajouter un équipement
|
|
Future<void> addEquipment(EquipmentModel equipment) async {
|
|
try {
|
|
await _dataService.createEquipment(equipment.id, equipment.toMap());
|
|
await loadEquipments(); // Recharger la liste
|
|
} catch (e) {
|
|
print('Error adding equipment: $e');
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
/// Mettre à jour un équipement
|
|
Future<void> updateEquipment(EquipmentModel equipment) async {
|
|
try {
|
|
await _dataService.updateEquipment(equipment.id, equipment.toMap());
|
|
await loadEquipments(); // Recharger la liste
|
|
} catch (e) {
|
|
print('Error updating equipment: $e');
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
/// Charger les marques
|
|
Future<void> loadBrands() async {
|
|
// Les marques sont déjà chargées avec loadEquipments
|
|
_extractUniqueValues();
|
|
}
|
|
|
|
/// Charger les modèles
|
|
Future<void> loadModels() async {
|
|
// Les modèles sont déjà chargés avec loadEquipments
|
|
_extractUniqueValues();
|
|
}
|
|
|
|
/// Charger les modèles d'une marque spécifique
|
|
Future<List<String>> loadModelsByBrand(String brand) async {
|
|
// Filtrer les modèles par marque
|
|
final modelsByBrand = _equipment
|
|
.where((eq) => eq.brand == brand && eq.model != null)
|
|
.map((eq) => eq.model!)
|
|
.toSet()
|
|
.toList();
|
|
return modelsByBrand;
|
|
}
|
|
|
|
/// Calculer le statut réel d'un équipement (compatibilité)
|
|
Future<EquipmentStatus> calculateRealStatus(EquipmentModel equipment) async {
|
|
// Pour l'instant, retourner le statut stocké
|
|
// TODO: Implémenter le calcul réel si nécessaire
|
|
return equipment.status;
|
|
}
|
|
}
|
|
|