Files
EM2_ERP/em2rp/lib/providers/equipment_provider.dart
ElPoyo 67b85d323c refactor: Amélioration et stabilisation du sélecteur d'équipement
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.
2026-01-15 23:59:25 +01:00

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;
}
}