Cette mise à jour introduit la gestion complète des utilisateurs (création, mise à jour, suppression) via des Cloud Functions et optimise de manière significative le chargement des données dans toute l'application.
**Features :**
- **Gestion des utilisateurs (Backend & Frontend) :**
- Ajout des Cloud Functions `getUser`, `updateUser` et `deleteUser` pour gérer les utilisateurs de manière sécurisée, en respectant les permissions des rôles.
- L'authentification passe désormais par `onCall` pour plus de sécurité.
- **Optimisation du chargement des données :**
- Introduction de nouvelles Cloud Functions `getEquipmentsByIds` et `getContainersByIds` pour récupérer uniquement les documents nécessaires, réduisant ainsi la charge sur le client et Firestore.
- Les fournisseurs (`EquipmentProvider`, `ContainerProvider`) ont été refactorisés pour utiliser un chargement à la demande (`ensureLoaded`) et mettre en cache les données récupérées.
- Les écrans de détails et de préparation d'événements n'utilisent plus de `Stream` globaux, mais chargent les équipements et boites spécifiques via ces nouvelles fonctions, améliorant considérablement les performances.
**Refactorisation et Améliorations :**
- **Backend (Cloud Functions) :**
- Le service de vérification de disponibilité (`checkEquipmentAvailability`) est désormais une Cloud Function, déplaçant la logique métier côté serveur.
- La récupération des données (utilisateurs, événements, alertes) a été centralisée derrière des Cloud Functions, remplaçant les appels directs à Firestore depuis le client.
- Amélioration de la sérialisation des données (timestamps, références) dans les réponses des fonctions.
- **Frontend (Flutter) :**
- `LocalUserProvider` charge désormais les informations de l'utilisateur connecté via la fonction `getCurrentUser`, incluant son rôle et ses permissions en un seul appel.
- `AlertProvider` utilise des fonctions pour charger et manipuler les alertes, abandonnant le `Stream` Firestore.
- `EventAvailabilityService` utilise maintenant la Cloud Function `checkEquipmentAvailability` au lieu d'une logique client complexe.
- Correction de la gestion des références de rôles (`roles/ADMIN`) et des `DocumentReference` pour les utilisateurs dans l'ensemble de l'application.
- Le service d'export ICS (`IcsExportService`) a été simplifié, partant du principe que les données nécessaires (utilisateurs, options) sont déjà chargées dans l'application.
297 lines
8.9 KiB
Dart
297 lines
8.9 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<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;
|
|
}
|
|
}
|
|
|