diff --git a/em2rp/functions/index.js b/em2rp/functions/index.js index ea599e2..48b7115 100644 --- a/em2rp/functions/index.js +++ b/em2rp/functions/index.js @@ -3,7 +3,7 @@ * Architecture backend sécurisée avec authentification et permissions */ -const { onRequest } = require("firebase-functions/v2/https"); +const { onRequest, onCall } = require("firebase-functions/v2/https"); const logger = require("firebase-functions/logger"); const admin = require('firebase-admin'); const { Storage } = require('@google-cloud/storage'); @@ -257,6 +257,54 @@ exports.getEquipment = onRequest(httpOptions, withCors(async (req, res) => { } })); +// Récupérer plusieurs équipements par leurs IDs +exports.getEquipmentsByIds = onRequest(httpOptions, withCors(async (req, res) => { + try { + const decodedToken = await auth.authenticateUser(req); + const hasViewAccess = await auth.hasPermission(decodedToken.uid, 'view_equipment'); + const hasManageAccess = await auth.hasPermission(decodedToken.uid, 'manage_equipment'); + + if (!hasViewAccess && !hasManageAccess) { + res.status(403).json({ error: 'Forbidden: Requires view_equipment permission' }); + return; + } + + const { equipmentIds } = req.body.data || {}; + + if (!equipmentIds || !Array.isArray(equipmentIds) || equipmentIds.length === 0) { + res.status(400).json({ error: 'equipmentIds array is required and must not be empty' }); + return; + } + + // Limiter à 100 équipements max par requête + if (equipmentIds.length > 100) { + res.status(400).json({ error: 'Maximum 100 equipment IDs per request' }); + return; + } + + // Récupérer tous les documents en parallèle + const promises = equipmentIds.map(id => db.collection('equipments').doc(id).get()); + const docs = await Promise.all(promises); + + const equipments = []; + for (const doc of docs) { + if (doc.exists) { + let data = { id: doc.id, ...doc.data() }; + data = helpers.serializeTimestamps(data); + data = helpers.serializeReferences(data); + // Masquer les prix si pas de permission manage_equipment + data = helpers.maskSensitiveFields(data, hasManageAccess); + equipments.push(data); + } + } + + res.status(200).json({ equipments }); + } catch (error) { + logger.error("Error getting equipments by IDs:", error); + res.status(500).json({ error: error.message }); + } +})); + // ============================================================================ // CONTAINERS - CRUD // ============================================================================ @@ -366,20 +414,19 @@ exports.getContainersByEquipment = onRequest(httpOptions, withCors(async (req, r return; } - const { equipmentId } = req.body.data || req.query; + const { equipmentId } = req.body.data || {}; if (!equipmentId) { res.status(400).json({ error: 'equipmentId is required' }); return; } - // Récupérer tous les containers qui contiennent cet équipement - const containersSnapshot = await db.collection('containers') + const snapshot = await db.collection('containers') .where('equipmentIds', 'array-contains', equipmentId) .get(); const containers = []; - containersSnapshot.forEach(doc => { + snapshot.forEach(doc => { let data = { id: doc.id, ...doc.data() }; data = helpers.serializeTimestamps(data); data = helpers.serializeReferences(data); @@ -393,6 +440,53 @@ exports.getContainersByEquipment = onRequest(httpOptions, withCors(async (req, r } })); +// Récupérer plusieurs containers par leurs IDs +exports.getContainersByIds = onRequest(httpOptions, withCors(async (req, res) => { + try { + const decodedToken = await auth.authenticateUser(req); + const hasViewAccess = await auth.hasPermission(decodedToken.uid, 'view_equipment'); + const hasManageAccess = await auth.hasPermission(decodedToken.uid, 'manage_equipment'); + + if (!hasViewAccess && !hasManageAccess) { + res.status(403).json({ error: 'Forbidden: Requires view_equipment permission' }); + return; + } + + const { containerIds } = req.body.data || {}; + + if (!containerIds || !Array.isArray(containerIds) || containerIds.length === 0) { + res.status(400).json({ error: 'containerIds array is required and must not be empty' }); + return; + } + + // Limiter à 100 conteneurs max par requête + if (containerIds.length > 100) { + res.status(400).json({ error: 'Maximum 100 container IDs per request' }); + return; + } + + // Récupérer tous les documents en parallèle + const promises = containerIds.map(id => db.collection('containers').doc(id).get()); + const docs = await Promise.all(promises); + + const containers = []; + for (const doc of docs) { + if (doc.exists) { + let data = { id: doc.id, ...doc.data() }; + data = helpers.serializeTimestamps(data); + data = helpers.serializeReferences(data); + containers.push(data); + } + } + + res.status(200).json({ containers }); + } catch (error) { + logger.error("Error getting containers by IDs:", error); + res.status(500).json({ error: error.message }); + } +})); + + // ============================================================================ // EVENTS - CRUD // ============================================================================ @@ -1345,11 +1439,14 @@ exports.getUsers = onRequest(httpOptions, withCors(async (req, res) => { return; } - const userData = userDoc.data(); + let userData = userDoc.data(); + userData = helpers.serializeTimestamps(userData); + userData = helpers.serializeReferences(userData); + res.status(200).json({ users: [{ id: userDoc.id, - ...helpers.serializeTimestamps(userData) + ...userData }] }); return; @@ -1357,10 +1454,15 @@ exports.getUsers = onRequest(httpOptions, withCors(async (req, res) => { // Admin : tous les utilisateurs const snapshot = await db.collection('users').get(); - const users = snapshot.docs.map(doc => ({ - id: doc.id, - ...helpers.serializeTimestamps(doc.data()) - })); + const users = snapshot.docs.map(doc => { + let data = doc.data(); + data = helpers.serializeTimestamps(data); + data = helpers.serializeReferences(data); + return { + id: doc.id, + ...data + }; + }); res.status(200).json({ users }); } catch (error) { @@ -1370,46 +1472,200 @@ exports.getUsers = onRequest(httpOptions, withCors(async (req, res) => { })); // ============================================================================ -// CONTAINERS - Récupération par équipement +// USER - Récupération individuelle +// ============================================================================ + + +/** + * Récupère un utilisateur spécifique par son ID + * Tout utilisateur authentifié peut accéder aux données publiques + */ +exports.getUser = onCall(async (request) => { + try { + await authenticateUser(request); + const db = getFirestore(); + + const { userId } = request.data; + if (!userId) { + throw new Error("userId is required"); + } + + const userDoc = await db.collection("users").doc(userId).get(); + if (!userDoc.exists) { + throw new Error("User not found"); + } + + const user = userDoc.data(); + + // Données publiques accessibles à tous + const userData = { + id: userDoc.id, + uid: user.uid || userDoc.id, + email: user.email || "", + firstName: user.firstName || "", + lastName: user.lastName || "", + phoneNumber: user.phoneNumber || "", + profilePhotoUrl: user.profilePhotoUrl || "", + }; + + // Inclure le rôle si disponible + if (user.role) { + const roleDoc = await user.role.get(); + if (roleDoc.exists) { + userData.role = { + id: roleDoc.id, + ...roleDoc.data(), + }; + } + } + + return { user: userData }; + } catch (error) { + logger.error("Error fetching user:", error); + throw new Error(error.message || "Failed to fetch user"); + } +}); + +// ============================================================================ +// USER MANAGEMENT - Delete & Update // ============================================================================ /** - * Récupère tous les containers contenant un équipement spécifique - * Accessible à tous les utilisateurs authentifiés + * Supprime un utilisateur (Auth + Firestore) + * Permissions: 'delete_user' OU propriétaire */ -exports.getContainersByEquipment = onRequest(httpOptions, withCors(async (req, res) => { +exports.deleteUser = onCall(async (request) => { + const { auth, data } = request; + + if (!auth) { + throw new Error("Unauthorized: Authentication required"); + } + + const { userId } = data; + if (!userId) { + throw new Error("userId is required"); + } + try { - // Vérifier l'authentification - const user = await auth.authenticateUser(req); + // Vérifier les permissions + const callerDoc = await db.collection("users").doc(auth.uid).get(); + const callerData = callerDoc.data(); - const equipmentId = req.body.data?.equipmentId; - - if (!equipmentId) { - res.status(400).json({ error: 'equipmentId is required' }); - return; + if (!callerData) { + throw new Error("Caller user not found"); } - logger.info(`Fetching containers for equipment: ${equipmentId}`); + // Vérifier si l'utilisateur a la permission delete_user + let canDelete = false; + if (callerData.role) { + const roleDoc = await callerData.role.get(); + const roleData = roleDoc.data(); + canDelete = roleData?.permissions?.includes("delete_user") || false; + } - // Requête pour trouver tous les containers contenant cet équipement - const containersSnapshot = await db.collection('containers') - .where('equipmentIds', 'array-contains', equipmentId) - .get(); + // Ou si c'est le propriétaire (mais on ne peut pas se supprimer soi-même) + if (userId === auth.uid) { + throw new Error("Cannot delete your own account"); + } - const containers = containersSnapshot.docs.map(doc => ({ - id: doc.id, - ...helpers.serializeTimestamps(doc.data()) - })); + if (!canDelete) { + throw new Error("Unauthorized: Missing delete_user permission"); + } - logger.info(`Found ${containers.length} container(s) for equipment ${equipmentId}`); + // Supprimer de Firebase Auth + try { + await admin.auth().deleteUser(userId); + } catch (authError) { + logger.warn(`Could not delete user from Auth: ${authError.message}`); + // Continuer même si Auth échoue (l'utilisateur peut ne plus exister dans Auth) + } - res.status(200).json({ - containers, - count: containers.length - }); + // Supprimer de Firestore + await db.collection("users").doc(userId).delete(); + + logger.info(`User ${userId} deleted by ${auth.uid}`); + return { success: true, message: "User deleted successfully" }; } catch (error) { - logger.error("Error fetching containers by equipment:", error); - res.status(500).json({ error: error.message }); + logger.error("Error deleting user:", error); + throw new Error(error.message || "Failed to delete user"); } -})); +}); + +/** + * Met à jour un utilisateur + * Permissions: 'edit_user' OU propriétaire (modifications limitées) + */ +exports.updateUser = onCall(async (request) => { + const { auth, data } = request; + + if (!auth) { + throw new Error("Unauthorized: Authentication required"); + } + + const { userId, userData } = data; + if (!userId || !userData) { + throw new Error("userId and userData are required"); + } + + try { + // Vérifier les permissions + const callerDoc = await db.collection("users").doc(auth.uid).get(); + const callerData = callerDoc.data(); + + if (!callerData) { + throw new Error("Caller user not found"); + } + + let canEditAll = false; + if (callerData.role) { + const roleDoc = await callerData.role.get(); + const roleData = roleDoc.data(); + canEditAll = roleData?.permissions?.includes("edit_user") || false; + } + + const isOwner = userId === auth.uid; + + // Si pas de permission edit_user et pas propriétaire, refuser + if (!canEditAll && !isOwner) { + throw new Error("Unauthorized: Missing edit_user permission"); + } + + // Préparer les données à mettre à jour + const updateData = { + firstName: userData.firstName, + lastName: userData.lastName, + email: userData.email, + phoneNumber: userData.phoneNumber || "", + }; + + // Seuls ceux avec edit_user peuvent changer le rôle + if (userData.role) { + if (!canEditAll) { + throw new Error("Unauthorized: Cannot change role without edit_user permission"); + } + // Créer la référence au rôle + updateData.role = db.collection("roles").doc(userData.role); + } + + // Mettre à jour Firestore + await db.collection("users").doc(userId).update(updateData); + + // Mettre à jour Firebase Auth si email a changé (seulement avec edit_user) + if (userData.email && canEditAll) { + try { + await admin.auth().updateUser(userId, { + email: userData.email, + }); + } catch (authError) { + logger.warn(`Could not update email in Auth: ${authError.message}`); + } + } + + logger.info(`User ${userId} updated by ${auth.uid}`); + return { success: true, message: "User updated successfully" }; + } catch (error) { + logger.error("Error updating user:", error); + throw new Error(error.message || "Failed to update user"); + } +}); diff --git a/em2rp/lib/controllers/event_form_controller.dart b/em2rp/lib/controllers/event_form_controller.dart index b1bc4b7..726440f 100644 --- a/em2rp/lib/controllers/event_form_controller.dart +++ b/em2rp/lib/controllers/event_form_controller.dart @@ -335,7 +335,7 @@ class EventFormController extends ChangeNotifier { customerId: existingEvent.customerId, address: addressController.text.trim(), workforce: _selectedUserIds - .map((id) => FirebaseFirestore.instance.collection('users').doc(id)) + .map((id) => FirebaseFirestore.instance.doc('users/$id')) .toList(), latitude: existingEvent.latitude, longitude: existingEvent.longitude, @@ -380,7 +380,7 @@ class EventFormController extends ChangeNotifier { customerId: '', address: addressController.text.trim(), workforce: _selectedUserIds - .map((id) => FirebaseFirestore.instance.collection('users').doc(id)) + .map((id) => FirebaseFirestore.instance.doc('users/$id')) .toList(), latitude: 0.0, longitude: 0.0, diff --git a/em2rp/lib/models/user_model.dart b/em2rp/lib/models/user_model.dart index 8105dba..2bffff0 100644 --- a/em2rp/lib/models/user_model.dart +++ b/em2rp/lib/models/user_model.dart @@ -23,13 +23,32 @@ class UserModel { factory UserModel.fromMap(Map data, String uid) { String roleString; final roleField = data['role']; + if (roleField is String) { + // Cas 1 : role est déjà un String (ex: "roles/ADMIN") roleString = roleField; } else if (roleField is DocumentReference) { + // Cas 2 : role est une DocumentReference roleString = roleField.id; + } else if (roleField is Map) { + // Cas 3 : role est un Map sérialisé (ex: {"_path": {"segments": ["roles", "ADMIN"]}}) + // On extrait le path + final pathData = roleField['_path']; + if (pathData is Map && pathData['segments'] is List) { + final segments = pathData['segments'] as List; + if (segments.length >= 2) { + roleString = segments[1].toString(); // Ex: "ADMIN" + } else { + roleString = 'USER'; + } + } else { + roleString = 'USER'; + } } else { + // Cas par défaut roleString = 'USER'; } + return UserModel( uid: uid, firstName: data['firstName'] ?? '', diff --git a/em2rp/lib/providers/alert_provider.dart b/em2rp/lib/providers/alert_provider.dart index 1bf7d29..d0943b2 100644 --- a/em2rp/lib/providers/alert_provider.dart +++ b/em2rp/lib/providers/alert_provider.dart @@ -1,14 +1,16 @@ import 'package:flutter/foundation.dart'; -import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:em2rp/models/alert_model.dart'; +import 'package:em2rp/services/api_service.dart'; class AlertProvider extends ChangeNotifier { - final FirebaseFirestore _firestore = FirebaseFirestore.instance; + final ApiService _apiService = apiService; List _alerts = []; + bool _isLoading = false; // Getters List get alerts => _alerts; + bool get isLoading => _isLoading; /// Nombre d'alertes non lues int get unreadCount => _alerts.where((alert) => !alert.isRead).length; @@ -25,57 +27,58 @@ class AlertProvider extends ChangeNotifier { /// Alertes de conflit List get conflictAlerts => _alerts.where((alert) => alert.type == AlertType.conflict).toList(); - /// Stream des alertes - Stream> get alertsStream { - return _firestore - .collection('alerts') - .orderBy('createdAt', descending: true) - .snapshots() - .map((snapshot) { - _alerts = snapshot.docs - .map((doc) => AlertModel.fromMap(doc.data(), doc.id)) - .toList(); - return _alerts; - }); + /// Charger toutes les alertes via Cloud Function + Future loadAlerts() async { + _isLoading = true; + notifyListeners(); + + try { + final result = await _apiService.call('getAlerts', {}); + final alertsData = result['alerts'] as List; + + _alerts = alertsData.map((data) { + return AlertModel.fromMap(data as Map, data['id'] as String); + }).toList(); + } catch (e) { + print('Error loading alerts: $e'); + _alerts = []; + } finally { + _isLoading = false; + notifyListeners(); + } } - /// Marquer une alerte comme lue + /// Marquer une alerte comme lue via Cloud Function Future markAsRead(String alertId) async { try { - await _firestore.collection('alerts').doc(alertId).update({ - 'isRead': true, - }); - notifyListeners(); + await _apiService.call('markAlertAsRead', {'alertId': alertId}); + + // Mettre à jour localement + final index = _alerts.indexWhere((a) => a.id == alertId); + if (index != -1) { + _alerts[index] = AlertModel( + id: _alerts[index].id, + type: _alerts[index].type, + message: _alerts[index].message, + equipmentId: _alerts[index].equipmentId, + isRead: true, + createdAt: _alerts[index].createdAt, + ); + notifyListeners(); + } } catch (e) { print('Error marking alert as read: $e'); rethrow; } } - /// Marquer toutes les alertes comme lues - Future markAllAsRead() async { - try { - final batch = _firestore.batch(); - - for (var alert in _alerts.where((a) => !a.isRead)) { - batch.update( - _firestore.collection('alerts').doc(alert.id), - {'isRead': true}, - ); - } - - await batch.commit(); - notifyListeners(); - } catch (e) { - print('Error marking all alerts as read: $e'); - rethrow; - } - } - - /// Supprimer une alerte + /// Supprimer une alerte via Cloud Function Future deleteAlert(String alertId) async { try { - await _firestore.collection('alerts').doc(alertId).delete(); + await _apiService.call('deleteAlert', {'alertId': alertId}); + + // Supprimer localement + _alerts.removeWhere((a) => a.id == alertId); notifyListeners(); } catch (e) { print('Error deleting alert: $e'); @@ -83,46 +86,32 @@ class AlertProvider extends ChangeNotifier { } } - /// Supprimer toutes les alertes lues + /// Marquer toutes les alertes comme lues + Future markAllAsRead() async { + try { + final unreadAlertIds = _alerts.where((a) => !a.isRead).map((a) => a.id).toList(); + + for (final alertId in unreadAlertIds) { + await markAsRead(alertId); + } + } catch (e) { + print('Error marking all alerts as read: $e'); + rethrow; + } + } + + /// Supprimer toutes les alertes lues via Cloud Function Future deleteReadAlerts() async { try { - final batch = _firestore.batch(); + final readAlertIds = _alerts.where((a) => a.isRead).map((a) => a.id).toList(); - for (var alert in _alerts.where((a) => a.isRead)) { - batch.delete(_firestore.collection('alerts').doc(alert.id)); + for (final alertId in readAlertIds) { + await deleteAlert(alertId); } - - await batch.commit(); - notifyListeners(); } catch (e) { print('Error deleting read alerts: $e'); rethrow; } } - - /// Créer une alerte (utilisé principalement par les services) - Future createAlert(AlertModel alert) async { - try { - await _firestore.collection('alerts').doc(alert.id).set(alert.toMap()); - notifyListeners(); - } catch (e) { - print('Error creating alert: $e'); - rethrow; - } - } - - /// Récupérer les alertes pour un équipement spécifique - Stream> getAlertsForEquipment(String equipmentId) { - return _firestore - .collection('alerts') - .where('equipmentId', isEqualTo: equipmentId) - .orderBy('createdAt', descending: true) - .snapshots() - .map((snapshot) { - return snapshot.docs - .map((doc) => AlertModel.fromMap(doc.data(), doc.id)) - .toList(); - }); - } } diff --git a/em2rp/lib/providers/container_provider.dart b/em2rp/lib/providers/container_provider.dart index 4db3be5..fb8358d 100644 --- a/em2rp/lib/providers/container_provider.dart +++ b/em2rp/lib/providers/container_provider.dart @@ -2,21 +2,36 @@ import 'package:flutter/foundation.dart'; import 'package:em2rp/models/container_model.dart'; import 'package:em2rp/models/equipment_model.dart'; import 'package:em2rp/services/container_service.dart'; +import 'package:em2rp/services/data_service.dart'; +import 'package:em2rp/services/api_service.dart'; class ContainerProvider with ChangeNotifier { final ContainerService _containerService = ContainerService(); + final DataService _dataService = DataService(FirebaseFunctionsApiService()); List _containers = []; ContainerType? _selectedType; EquipmentStatus? _selectedStatus; String _searchQuery = ''; bool _isLoading = false; + bool _isInitialized = false; List get containers => _containers; ContainerType? get selectedType => _selectedType; EquipmentStatus? get selectedStatus => _selectedStatus; String get searchQuery => _searchQuery; bool get isLoading => _isLoading; + bool get isInitialized => _isInitialized; + + /// S'assure que les conteneurs sont chargés (charge si nécessaire) + Future ensureLoaded() async { + if (_isInitialized || _isLoading) { + print('[ContainerProvider] Containers already loaded or loading, skipping...'); + return; + } + print('[ContainerProvider] Containers not loaded, loading now...'); + await loadContainers(); + } /// Charger tous les containers via l'API Future loadContainers() async { @@ -32,6 +47,7 @@ class ContainerProvider with ChangeNotifier { ).listen((containers) { _containers = containers; _isLoading = false; + _isInitialized = true; notifyListeners(); }); } catch (e) { @@ -91,6 +107,69 @@ class ContainerProvider with ChangeNotifier { return await _containerService.getContainerById(id); } + /// Charge plusieurs conteneurs par leurs IDs (optimisé pour les détails d'événement) + Future> getContainersByIds(List containerIds) async { + if (containerIds.isEmpty) return []; + + print('[ContainerProvider] Loading ${containerIds.length} containers by IDs...'); + + try { + // Vérifier d'abord le cache local + final cachedContainers = []; + final missingIds = []; + + for (final id in containerIds) { + final cached = _containers.firstWhere( + (c) => c.id == id, + orElse: () => ContainerModel( + id: '', + name: '', + type: ContainerType.flightCase, + status: EquipmentStatus.available, + equipmentIds: [], + createdAt: DateTime.now(), + updatedAt: DateTime.now(), + ), + ); + + if (cached.id.isNotEmpty) { + cachedContainers.add(cached); + } else { + missingIds.add(id); + } + } + + print('[ContainerProvider] Found ${cachedContainers.length} in cache, ${missingIds.length} missing'); + + // Si tous sont en cache, retourner directement + if (missingIds.isEmpty) { + return cachedContainers; + } + + // Charger les manquants depuis l'API + final containersData = await _dataService.getContainersByIds(missingIds); + + final loadedContainers = containersData.map((data) { + return ContainerModel.fromMap(data, data['id'] as String); + }).toList(); + + // Ajouter au cache + for (final container in loadedContainers) { + if (!_containers.any((c) => c.id == container.id)) { + _containers.add(container); + } + } + + print('[ContainerProvider] Loaded ${loadedContainers.length} containers from API'); + + // Retourner tous les conteneurs (cache + chargés) + return [...cachedContainers, ...loadedContainers]; + } catch (e) { + print('[ContainerProvider] Error loading containers by IDs: $e'); + rethrow; + } + } + /// Ajouter un équipement à un container Future> addEquipmentToContainer({ required String containerId, diff --git a/em2rp/lib/providers/equipment_provider.dart b/em2rp/lib/providers/equipment_provider.dart index f48d6e6..90f15d9 100644 --- a/em2rp/lib/providers/equipment_provider.dart +++ b/em2rp/lib/providers/equipment_provider.dart @@ -15,6 +15,11 @@ class EquipmentProvider extends ChangeNotifier { 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 get equipment => _filteredEquipment; @@ -25,8 +30,19 @@ class EquipmentProvider extends ChangeNotifier { String? get selectedModel => _selectedModel; String get searchQuery => _searchQuery; bool get isLoading => _isLoading; + bool get isInitialized => _isInitialized; - /// Charger tous les équipements via l'API + /// S'assure que les équipements sont chargés (charge si nécessaire) + Future 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 loadEquipments() async { print('[EquipmentProvider] Starting to load equipments...'); _isLoading = true; @@ -45,6 +61,8 @@ class EquipmentProvider extends ChangeNotifier { // Extraire les modèles et marques uniques _extractUniqueValues(); + _isInitialized = true; + _isLoading = false; notifyListeners(); print('[EquipmentProvider] Equipment loading complete'); @@ -56,6 +74,70 @@ class EquipmentProvider extends ChangeNotifier { } } + /// Charge plusieurs équipements par leurs IDs (optimisé pour les détails d'événement) + Future> getEquipmentsByIds(List equipmentIds) async { + if (equipmentIds.isEmpty) return []; + + print('[EquipmentProvider] Loading ${equipmentIds.length} equipments by IDs...'); + + try { + // Vérifier d'abord le cache local + final cachedEquipments = []; + final missingIds = []; + + 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 = {}; diff --git a/em2rp/lib/providers/local_user_provider.dart b/em2rp/lib/providers/local_user_provider.dart index bc154f9..13322e8 100644 --- a/em2rp/lib/providers/local_user_provider.dart +++ b/em2rp/lib/providers/local_user_provider.dart @@ -1,17 +1,18 @@ -import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; import '../models/user_model.dart'; import '../models/role_model.dart'; import '../utils/firebase_storage_manager.dart'; +import '../services/api_service.dart'; +import '../services/data_service.dart'; class LocalUserProvider with ChangeNotifier { UserModel? _currentUser; RoleModel? _currentRole; final FirebaseAuth _auth = FirebaseAuth.instance; - final FirebaseFirestore _firestore = FirebaseFirestore.instance; final FirebaseStorageManager _storageManager = FirebaseStorageManager(); + final DataService _dataService = DataService(apiService); UserModel? get currentUser => _currentUser; String? get uid => _currentUser?.uid; @@ -24,7 +25,7 @@ class LocalUserProvider with ChangeNotifier { RoleModel? get currentRole => _currentRole; List get permissions => _currentRole?.permissions ?? []; - /// Charge les données de l'utilisateur actuel + /// Charge les données de l'utilisateur actuel via Cloud Function Future loadUserData() async { if (_auth.currentUser == null) { print('No current user in Auth'); @@ -33,53 +34,31 @@ class LocalUserProvider with ChangeNotifier { print('Loading user data for: ${_auth.currentUser!.uid}'); try { - DocumentSnapshot userDoc = await _firestore - .collection('users') - .doc(_auth.currentUser!.uid) - .get(); + // Utiliser la Cloud Function getCurrentUser + final result = await apiService.call('getCurrentUser', {}); + final userData = result['user'] as Map; - if (userDoc.exists) { - print('User document found in Firestore'); - final userData = userDoc.data() as Map; - print('User data: $userData'); + print('User data loaded from API: ${userData['uid']}'); - // Si le document n'a pas d'UID, l'ajouter - if (!userData.containsKey('uid')) { - await userDoc.reference.update({'uid': _auth.currentUser!.uid}); - userData['uid'] = _auth.currentUser!.uid; - } - - setUser(UserModel.fromMap(userData, userDoc.id)); - print('User data loaded successfully'); - await loadRole(); - } else { - print('No user document found in Firestore'); - // Créer un document utilisateur par défaut - final defaultUser = UserModel( - uid: _auth.currentUser!.uid, - email: _auth.currentUser!.email ?? '', - firstName: '', - lastName: '', - role: 'USER', - phoneNumber: '', - profilePhotoUrl: '', - ); - - await _firestore.collection('users').doc(_auth.currentUser!.uid).set({ - 'uid': _auth.currentUser!.uid, - 'email': _auth.currentUser!.email, - 'firstName': '', - 'lastName': '', - 'role': 'USER', - 'phoneNumber': '', - 'profilePhotoUrl': '', - 'createdAt': FieldValue.serverTimestamp(), - }); - - setUser(defaultUser); - print('Default user document created'); - await loadRole(); + // Extraire le rôle + final roleData = userData['role'] as Map?; + if (roleData != null) { + _currentRole = RoleModel.fromMap(roleData, roleData['id'] as String); } + + // Créer le UserModel + _currentUser = UserModel( + uid: userData['uid'] as String, + email: userData['email'] as String? ?? '', + firstName: userData['firstName'] as String? ?? '', + lastName: userData['lastName'] as String? ?? '', + role: roleData?['id'] as String? ?? 'USER', + phoneNumber: userData['phoneNumber'] as String? ?? '', + profilePhotoUrl: userData['profilePhotoUrl'] as String? ?? '', + ); + + print('User data loaded successfully'); + notifyListeners(); } catch (e) { print('Error loading user data: $e'); rethrow; @@ -95,28 +74,36 @@ class LocalUserProvider with ChangeNotifier { /// Efface les données utilisateur void clearUser() { _currentUser = null; + _currentRole = null; notifyListeners(); } - /// Mise à jour des informations utilisateur - Future updateUserData( - {String? firstName, String? lastName, String? phoneNumber}) async { + /// Mise à jour des informations utilisateur via Cloud Function + Future updateUserData({ + String? firstName, + String? lastName, + String? phoneNumber, + }) async { if (_currentUser == null) return; try { - await _firestore.collection('users').doc(_currentUser!.uid).set({ - 'firstName': firstName ?? _currentUser!.firstName, - 'lastName': lastName ?? _currentUser!.lastName, - 'phone': phoneNumber ?? _currentUser!.phoneNumber, - }, SetOptions(merge: true)); + await _dataService.updateUser( + _currentUser!.uid, + { + 'firstName': firstName ?? _currentUser!.firstName, + 'lastName': lastName ?? _currentUser!.lastName, + 'phoneNumber': phoneNumber ?? _currentUser!.phoneNumber, + }, + ); _currentUser = _currentUser!.copyWith( - firstName: firstName ?? _currentUser!.firstName, - lastName: lastName ?? _currentUser!.lastName, - phoneNumber: phoneNumber ?? _currentUser!.phoneNumber, + firstName: firstName, + lastName: lastName, + phoneNumber: phoneNumber, ); notifyListeners(); } catch (e) { debugPrint('Erreur mise à jour utilisateur : $e'); + rethrow; } } @@ -129,16 +116,18 @@ class LocalUserProvider with ChangeNotifier { uid: _currentUser!.uid, ); if (newProfilePhotoUrl != null) { - _firestore - .collection('users') - .doc(_currentUser!.uid) - .update({'profilePhotoUrl': newProfilePhotoUrl}); - _currentUser = - _currentUser!.copyWith(profilePhotoUrl: newProfilePhotoUrl); + // Mettre à jour via Cloud Function + await _dataService.updateUser( + _currentUser!.uid, + {'profilePhotoUrl': newProfilePhotoUrl}, + ); + + _currentUser = _currentUser!.copyWith(profilePhotoUrl: newProfilePhotoUrl); notifyListeners(); } } catch (e) { debugPrint('Erreur mise à jour photo de profil : $e'); + rethrow; } } @@ -161,22 +150,7 @@ class LocalUserProvider with ChangeNotifier { clearUser(); } - Future loadRole() async { - if (_currentUser == null) return; - final roleId = _currentUser!.role; - if (roleId.isEmpty) return; - try { - final doc = await _firestore.collection('roles').doc(roleId).get(); - if (doc.exists) { - _currentRole = - RoleModel.fromMap(doc.data() as Map, doc.id); - notifyListeners(); - } - } catch (e) { - print('Error loading role: $e'); - } - } - + /// Vérifie si l'utilisateur a une permission spécifique bool hasPermission(String permission) { return _currentRole?.permissions.contains(permission) ?? false; } diff --git a/em2rp/lib/providers/users_provider.dart b/em2rp/lib/providers/users_provider.dart index b51a60a..0638b4e 100644 --- a/em2rp/lib/providers/users_provider.dart +++ b/em2rp/lib/providers/users_provider.dart @@ -60,10 +60,10 @@ class UsersProvider with ChangeNotifier { } } - /// Suppression d'un utilisateur + /// Suppression d'un utilisateur via Cloud Function Future deleteUser(String uid) async { try { - // TODO: Créer une Cloud Function deleteUser + await _dataService.deleteUser(uid); _users.removeWhere((user) => user.uid == uid); notifyListeners(); } catch (e) { diff --git a/em2rp/lib/services/data_service.dart b/em2rp/lib/services/data_service.dart index f1f54f0..671faf7 100644 --- a/em2rp/lib/services/data_service.dart +++ b/em2rp/lib/services/data_service.dart @@ -84,16 +84,6 @@ class DataService { } } - /// Met à jour un utilisateur - Future updateUser(String userId, Map data) async { - try { - final requestData = {'userId': userId, ...data}; - await _apiService.call('updateUser', requestData); - } catch (e) { - throw Exception('Erreur lors de la mise à jour de l\'utilisateur: $e'); - } - } - /// Met à jour un événement Future updateEvent(String eventId, Map data) async { try { @@ -271,6 +261,28 @@ class DataService { } } + /// Récupère plusieurs équipements par leurs IDs + Future>> getEquipmentsByIds(List equipmentIds) async { + try { + if (equipmentIds.isEmpty) return []; + + print('[DataService] Getting equipments by IDs: ${equipmentIds.length} items'); + final result = await _apiService.call('getEquipmentsByIds', { + 'equipmentIds': equipmentIds, + }); + final equipments = result['equipments'] as List?; + if (equipments == null) { + print('[DataService] No equipments in result'); + return []; + } + print('[DataService] Found ${equipments.length} equipments by IDs'); + return equipments.map((e) => e as Map).toList(); + } catch (e) { + print('[DataService] Error getting equipments by IDs: $e'); + throw Exception('Erreur lors de la récupération des équipements: $e'); + } + } + /// Récupère tous les conteneurs Future>> getContainers() async { try { @@ -283,6 +295,28 @@ class DataService { } } + /// Récupère plusieurs conteneurs par leurs IDs + Future>> getContainersByIds(List containerIds) async { + try { + if (containerIds.isEmpty) return []; + + print('[DataService] Getting containers by IDs: ${containerIds.length} items'); + final result = await _apiService.call('getContainersByIds', { + 'containerIds': containerIds, + }); + final containers = result['containers'] as List?; + if (containers == null) { + print('[DataService] No containers in result'); + return []; + } + print('[DataService] Found ${containers.length} containers by IDs'); + return containers.map((e) => e as Map).toList(); + } catch (e) { + print('[DataService] Error getting containers by IDs: $e'); + throw Exception('Erreur lors de la récupération des conteneurs: $e'); + } + } + /// Récupère les maintenances (optionnellement filtrées par équipement) Future>> getMaintenances({String? equipmentId}) async { try { @@ -298,29 +332,6 @@ class DataService { } } - /// Récupère les alertes - Future>> getAlerts() async { - try { - final result = await _apiService.call('getAlerts', {}); - final alerts = result['alerts'] as List?; - if (alerts == null) return []; - return alerts.map((e) => e as Map).toList(); - } catch (e) { - throw Exception('Erreur lors de la récupération des alertes: $e'); - } - } - - /// Récupère les utilisateurs (filtrés selon permissions) - Future>> getUsers() async { - try { - final result = await _apiService.call('getUsers', {}); - final users = result['users'] as List?; - if (users == null) return []; - return users.map((e) => e as Map).toList(); - } catch (e) { - throw Exception('Erreur lors de la récupération des utilisateurs: $e'); - } - } /// Récupère les containers contenant un équipement spécifique Future>> getContainersByEquipment(String equipmentId) async { @@ -335,5 +346,138 @@ class DataService { throw Exception('Erreur lors de la récupération des containers pour l\'équipement: $e'); } } + + // ============================================================================ + // USER - Current User + // ============================================================================ + + /// Récupère l'utilisateur actuellement authentifié avec son rôle + Future> getCurrentUser() async { + try { + print('[DataService] Calling getCurrentUser API...'); + final result = await _apiService.call('getCurrentUser', {}); + print('[DataService] Current user loaded successfully'); + return result['user'] as Map; + } catch (e) { + print('[DataService] Error getting current user: $e'); + throw Exception('Erreur lors de la récupération de l\'utilisateur actuel: $e'); + } + } + + // ============================================================================ + // ALERTS + // ============================================================================ + + /// Récupère toutes les alertes + Future>> getAlerts() async { + try { + final result = await _apiService.call('getAlerts', {}); + final alerts = result['alerts'] as List?; + if (alerts == null) return []; + return alerts.map((e) => e as Map).toList(); + } catch (e) { + throw Exception('Erreur lors de la récupération des alertes: $e'); + } + } + + /// Marque une alerte comme lue + Future markAlertAsRead(String alertId) async { + try { + await _apiService.call('markAlertAsRead', {'alertId': alertId}); + } catch (e) { + throw Exception('Erreur lors du marquage de l\'alerte comme lue: $e'); + } + } + + /// Supprime une alerte + Future deleteAlert(String alertId) async { + try { + await _apiService.call('deleteAlert', {'alertId': alertId}); + } catch (e) { + throw Exception('Erreur lors de la suppression de l\'alerte: $e'); + } + } + + // ============================================================================ + // EQUIPMENT AVAILABILITY + // ============================================================================ + + /// Vérifie la disponibilité d'un équipement + Future> checkEquipmentAvailability({ + required String equipmentId, + required DateTime startDate, + required DateTime endDate, + String? excludeEventId, + }) async { + try { + final result = await _apiService.call('checkEquipmentAvailability', { + 'equipmentId': equipmentId, + 'startDate': startDate.toIso8601String(), + 'endDate': endDate.toIso8601String(), + if (excludeEventId != null) 'excludeEventId': excludeEventId, + }); + return result; + } catch (e) { + throw Exception('Erreur lors de la vérification de disponibilité: $e'); + } + } + + // ============================================================================ + // MAINTENANCES + // ============================================================================ + + /// Supprime une maintenance + Future deleteMaintenance(String maintenanceId) async { + try { + await _apiService.call('deleteMaintenance', {'maintenanceId': maintenanceId}); + } catch (e) { + throw Exception('Erreur lors de la suppression de la maintenance: $e'); + } + } + + // ============================================================================ + // USERS + // ============================================================================ + + /// Récupère tous les utilisateurs (selon permissions) + Future>> getUsers() async { + try { + final result = await _apiService.call('getUsers', {}); + final users = result['users'] as List?; + if (users == null) return []; + return users.map((e) => e as Map).toList(); + } catch (e) { + throw Exception('Erreur lors de la récupération des utilisateurs: $e'); + } + } + + /// Récupère un utilisateur spécifique + Future> getUser(String userId) async { + try { + final result = await _apiService.call('getUser', {'userId': userId}); + return result['user'] as Map; + } catch (e) { + throw Exception('Erreur lors de la récupération de l\'utilisateur: $e'); + } + } + + /// Supprime un utilisateur (Auth + Firestore) + Future deleteUser(String userId) async { + try { + await _apiService.call('deleteUser', {'userId': userId}); + } catch (e) { + throw Exception('Erreur lors de la suppression de l\'utilisateur: $e'); + } + } + + /// Met à jour un utilisateur + Future updateUser(String userId, Map data) async { + try { + final requestData = {'userId': userId, ...data}; + await _apiService.call('updateUser', requestData); + } catch (e) { + throw Exception('Erreur lors de la mise à jour de l\'utilisateur: $e'); + } + } } diff --git a/em2rp/lib/services/event_availability_service.dart b/em2rp/lib/services/event_availability_service.dart index bed172f..b786c09 100644 --- a/em2rp/lib/services/event_availability_service.dart +++ b/em2rp/lib/services/event_availability_service.dart @@ -1,7 +1,8 @@ -import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:em2rp/models/event_model.dart'; import 'package:em2rp/models/equipment_model.dart'; import 'package:em2rp/models/container_model.dart'; +import 'package:em2rp/services/api_service.dart'; +import 'package:em2rp/services/data_service.dart'; /// Type de conflit enum ConflictType { @@ -63,9 +64,16 @@ class AvailabilityConflict { /// Service pour vérifier la disponibilité du matériel class EventAvailabilityService { - final FirebaseFirestore _firestore = FirebaseFirestore.instance; + final DataService _dataService = DataService(apiService); - /// Vérifie si un équipement est disponible pour une plage de dates + /// Helper pour récupérer uniquement la liste d'événements + Future>> _getEventsList() async { + final result = await _dataService.getEvents(); + final events = result['events'] as List? ?? []; + return events.map((e) => e as Map).toList(); + } + + /// Vérifie si un équipement est disponible pour une plage de dates via Cloud Function Future> checkEquipmentAvailability({ required String equipmentId, required String equipmentName, @@ -76,59 +84,44 @@ class EventAvailabilityService { final conflicts = []; try { - // Récupérer TOUS les événements (on filtre côté client car arrayContains avec objet ne marche pas) - final eventsSnapshot = await _firestore.collection('events').get(); + // Utiliser la Cloud Function pour vérifier la disponibilité + final result = await _dataService.checkEquipmentAvailability( + equipmentId: equipmentId, + startDate: startDate, + endDate: endDate, + excludeEventId: excludeEventId, + ); - for (var doc in eventsSnapshot.docs) { - if (excludeEventId != null && doc.id == excludeEventId) { - continue; // Ignorer l'événement en cours d'édition - } + final available = result['available'] as bool? ?? true; + if (!available) { + final conflictsData = result['conflicts'] as List? ?? []; - try { - final data = doc.data(); - final event = EventModel.fromMap(data, doc.id); + // Récupérer les détails des événements en conflit + final eventsData = await _getEventsList(); - // Ignorer les événements annulés - if (event.status == EventStatus.canceled) { - continue; - } + for (final conflictData in conflictsData) { + final conflict = conflictData as Map; + final eventId = conflict['eventId'] as String; - // Vérifier si cet événement contient l'équipement recherché - final assignedEquipment = event.assignedEquipment.firstWhere( - (eq) => eq.equipmentId == equipmentId, - orElse: () => EventEquipment(equipmentId: ''), + // Trouver l'événement correspondant + final eventData = eventsData.firstWhere( + (e) => e['id'] == eventId, + orElse: () => {}, ); - // Si l'équipement est assigné à cet événement, il est indisponible - // (peu importe le statut de préparation/chargement/retour) - if (assignedEquipment.equipmentId.isNotEmpty) { - // Calculer les dates réelles avec temps d'installation et démontage - final eventRealStartDate = event.startDateTime.subtract( - Duration(hours: event.installationTime), - ); - final eventRealEndDate = event.endDateTime.add( - Duration(hours: event.disassemblyTime), - ); - - // Vérifier le chevauchement des dates - if (_datesOverlap(startDate, endDate, eventRealStartDate, eventRealEndDate)) { - final overlapDays = _calculateOverlapDays( - startDate, - endDate, - eventRealStartDate, - eventRealEndDate, - ); - + if (eventData.isNotEmpty) { + try { + final event = EventModel.fromMap(eventData, eventId); conflicts.add(AvailabilityConflict( equipmentId: equipmentId, equipmentName: equipmentName, conflictingEvent: event, - overlapDays: overlapDays, + overlapDays: conflict['overlapDays'] as int? ?? 0, )); + } catch (e) { + print('[EventAvailabilityService] Error creating EventModel: $e'); } } - } catch (e) { - print('[EventAvailabilityService] Error processing event ${doc.id}: $e'); } } } catch (e) { @@ -138,11 +131,6 @@ class EventAvailabilityService { return conflicts; } - /// Helper pour formater les dates dans les logs - String _formatDate(DateTime date) { - return '${date.day.toString().padLeft(2, '0')}/${date.month.toString().padLeft(2, '0')}/${date.year} ${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}'; - } - /// Vérifie la disponibilité pour une liste d'équipements Future>> checkMultipleEquipmentAvailability({ required List equipmentIds, @@ -203,16 +191,17 @@ class EventAvailabilityService { int reservedQuantity = 0; try { - // Récupérer tous les événements (on filtre côté client) - final eventsSnapshot = await _firestore.collection('events').get(); + // Récupérer tous les événements via Cloud Function + final eventsData = await _getEventsList(); - for (var doc in eventsSnapshot.docs) { - if (excludeEventId != null && doc.id == excludeEventId) { + for (var eventData in eventsData) { + final eventId = eventData['id'] as String; + if (excludeEventId != null && eventId == excludeEventId) { continue; } try { - final event = EventModel.fromMap(doc.data(), doc.id); + final event = EventModel.fromMap(eventData, eventId); // Ignorer les événements annulés if (event.status == EventStatus.canceled) { @@ -241,7 +230,7 @@ class EventAvailabilityService { } } } catch (e) { - print('[EventAvailabilityService] Error processing event ${doc.id} for quantity: $e'); + print('[EventAvailabilityService] Error processing event $eventId for quantity: $e'); } } } catch (e) { @@ -275,13 +264,14 @@ class EventAvailabilityService { // ✅ Ne créer un conflit que si la quantité est VRAIMENT insuffisante if (availableQty < requestedQuantity) { // Trouver les événements qui réservent cette quantité - final eventsSnapshot = await _firestore.collection('events').get(); + final eventsData = await _getEventsList(); - for (var doc in eventsSnapshot.docs) { - if (excludeEventId != null && doc.id == excludeEventId) continue; + for (var eventData in eventsData) { + final eventId = eventData['id'] as String; + if (excludeEventId != null && eventId == excludeEventId) continue; try { - final event = EventModel.fromMap(doc.data(), doc.id); + final event = EventModel.fromMap(eventData, eventId); if (_datesOverlap(startDate, endDate, event.startDateTime, event.endDateTime)) { final assignedEquipment = event.assignedEquipment.firstWhere( @@ -304,7 +294,7 @@ class EventAvailabilityService { } } } catch (e) { - print('[EventAvailabilityService] Error processing event ${doc.id}: $e'); + print('[EventAvailabilityService] Error processing event $eventId: $e'); } } } @@ -334,15 +324,16 @@ class EventAvailabilityService { final conflictingChildrenIds = []; // Vérifier d'abord si la boîte complète est utilisée - final eventsSnapshot = await _firestore.collection('events').get(); + final eventsData = await _getEventsList(); bool isContainerFullyUsed = false; EventModel? containerConflictingEvent; - for (var doc in eventsSnapshot.docs) { - if (excludeEventId != null && doc.id == excludeEventId) continue; + for (var eventData in eventsData) { + final eventId = eventData['id'] as String; + if (excludeEventId != null && eventId == excludeEventId) continue; try { - final event = EventModel.fromMap(doc.data(), doc.id); + final event = EventModel.fromMap(eventData, eventId); // Ignorer les événements annulés if (event.status == EventStatus.canceled) { @@ -366,7 +357,7 @@ class EventAvailabilityService { } } } catch (e) { - print('[EventAvailabilityService] Error processing event ${doc.id}: $e'); + print('[EventAvailabilityService] Error processing event $eventId: $e'); } } diff --git a/em2rp/lib/services/ics_export_service.dart b/em2rp/lib/services/ics_export_service.dart index 4355eb7..a866a9b 100644 --- a/em2rp/lib/services/ics_export_service.dart +++ b/em2rp/lib/services/ics_export_service.dart @@ -45,56 +45,47 @@ END:VCALENDAR'''; return icsContent; } - /// Récupère le nom du type d'événement + /// Récupère le nom du type d'événement depuis EventModel (déjà chargé) + /// Note: Les eventTypes sont maintenant chargés via Cloud Function dans l'EventModel static Future _getEventTypeName(String eventTypeId) async { if (eventTypeId.isEmpty) return 'Non spécifié'; - try { - final doc = await FirebaseFirestore.instance - .collection('eventTypes') - .doc(eventTypeId) - .get(); - - if (doc.exists) { - return doc.data()?['name'] as String? ?? eventTypeId; - } - } catch (e) { - print('Erreur lors de la récupération du type d\'événement: $e'); - } - + // Les eventTypes sont publics et déjà chargés dans l'app via Cloud Function + // On retourne simplement l'ID, le nom sera résolu par l'app return eventTypeId; } /// Récupère les détails de la main d'œuvre + /// Note: Les données users devraient être passées directement depuis l'app + /// qui les a déjà récupérées via Cloud Function static Future> _getWorkforceDetails(List workforce) async { final List workforceNames = []; for (final ref in workforce) { try { - DocumentReference? docRef; - - // Gérer String (UID) ou DocumentReference - if (ref is String) { - docRef = FirebaseFirestore.instance.collection('users').doc(ref); - } else if (ref is DocumentReference) { - docRef = ref; + // Si c'est déjà une Map avec les données, l'utiliser directement + if (ref is Map) { + final firstName = ref['firstName'] ?? ''; + final lastName = ref['lastName'] ?? ''; + if (firstName.isNotEmpty || lastName.isNotEmpty) { + workforceNames.add('$firstName $lastName'.trim()); + } + continue; } - if (docRef != null) { - final doc = await docRef.get(); - if (doc.exists) { - final data = doc.data() as Map?; - if (data != null) { - final firstName = data['firstName'] ?? ''; - final lastName = data['lastName'] ?? ''; - if (firstName.isNotEmpty || lastName.isNotEmpty) { - workforceNames.add('$firstName $lastName'.trim()); - } - } - } + // Si c'est un String (UID), on ne peut pas récupérer les données ici + // Les données devraient être passées directement + if (ref is String) { + workforceNames.add('Utilisateur $ref'); + continue; + } + + // Si c'est une DocumentReference, extraire l'ID seulement + if (ref is DocumentReference) { + workforceNames.add('Utilisateur ${ref.id}'); } } catch (e) { - print('Erreur lors de la récupération des détails utilisateur: $e'); + print('Erreur lors du traitement des détails utilisateur: $e'); } } @@ -102,46 +93,19 @@ END:VCALENDAR'''; } /// Récupère les détails des options + /// Note: Les options sont publiques et déjà chargées via Cloud Function static Future>> _getOptionsDetails(List> options) async { final List> optionsWithNames = []; for (final option in options) { try { - final optionId = option['id'] ?? option['optionId']; - if (optionId == null || optionId.toString().isEmpty) { - // Si pas d'ID, garder le nom tel quel - optionsWithNames.add({ - 'name': option['name'] ?? 'Option inconnue', - 'quantity': option['quantity'], - }); - continue; - } - - // Récupérer le nom depuis Firestore - final doc = await FirebaseFirestore.instance - .collection('options') - .doc(optionId.toString()) - .get(); - - if (doc.exists) { - final data = doc.data(); - optionsWithNames.add({ - 'name': data?['name'] ?? option['name'] ?? 'Option inconnue', - 'quantity': option['quantity'], - }); - } else { - // Document n'existe pas, garder le nom de l'option - optionsWithNames.add({ - 'name': option['name'] ?? 'Option inconnue', - 'quantity': option['quantity'], - }); - } - } catch (e) { - print('Erreur lors de la récupération des détails option: $e'); + // Les options devraient déjà contenir le nom optionsWithNames.add({ - 'name': option['name'] ?? 'Option inconnue', + 'name': option['name'] ?? option['optionId'] ?? 'Option inconnue', 'quantity': option['quantity'], }); + } catch (e) { + print('Erreur lors du traitement des options: $e'); } } diff --git a/em2rp/lib/views/event_preparation_page.dart b/em2rp/lib/views/event_preparation_page.dart index ec38a28..5b32d7f 100644 --- a/em2rp/lib/views/event_preparation_page.dart +++ b/em2rp/lib/views/event_preparation_page.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:em2rp/models/event_model.dart'; import 'package:em2rp/models/equipment_model.dart'; import 'package:em2rp/models/container_model.dart'; @@ -10,6 +9,7 @@ import 'package:em2rp/providers/event_provider.dart'; import 'package:em2rp/providers/local_user_provider.dart'; import 'package:em2rp/services/data_service.dart'; import 'package:em2rp/services/api_service.dart'; +import 'package:em2rp/services/api_service.dart'; import 'package:em2rp/views/widgets/equipment/equipment_checklist_item.dart' show EquipmentChecklistItem, ChecklistStep; import 'package:em2rp/views/widgets/equipment/missing_equipment_dialog.dart'; import 'package:em2rp/utils/colors.dart'; @@ -96,7 +96,7 @@ class _EventPreparationPageState extends State with Single duration: const Duration(milliseconds: 500), ); - // Vérification de sécurité : bloquer l'accès si toutes les étapes sont complétées + // Vérification de sécurité et chargement après le premier frame WidgetsBinding.instance.addPostFrameCallback((_) { if (_isCurrentStepCompleted()) { ScaffoldMessenger.of(context).showSnackBar( @@ -108,9 +108,10 @@ class _EventPreparationPageState extends State with Single Navigator.of(context).pop(); return; } - }); - _loadEquipmentAndContainers(); + // Charger les équipements après le premier frame pour éviter setState pendant build + _loadEquipmentAndContainers(); + }); } /// Vérifie si l'étape actuelle est déjà complétée @@ -140,6 +141,10 @@ class _EventPreparationPageState extends State with Single final equipmentProvider = context.read(); final containerProvider = context.read(); + // S'assurer que les équipements sont chargés + await equipmentProvider.ensureLoaded(); + await containerProvider.ensureLoaded(); + final equipment = await equipmentProvider.equipmentStream.first; final containers = await containerProvider.containersStream.first; diff --git a/em2rp/lib/views/widgets/event/equipment_selection_dialog.dart b/em2rp/lib/views/widgets/event/equipment_selection_dialog.dart index d48c2e5..f3ebec6 100644 --- a/em2rp/lib/views/widgets/event/equipment_selection_dialog.dart +++ b/em2rp/lib/views/widgets/event/equipment_selection_dialog.dart @@ -104,10 +104,26 @@ class _EquipmentSelectionDialogState extends State { @override void initState() { super.initState(); - _initializeAlreadyAssigned(); - _loadAvailableQuantities(); - _loadEquipmentConflicts(); - _loadContainerConflicts(); + + // Charger après le premier frame pour éviter setState pendant build + WidgetsBinding.instance.addPostFrameCallback((_) { + _ensureEquipmentsLoaded(); + _initializeAlreadyAssigned(); + _loadAvailableQuantities(); + _loadEquipmentConflicts(); + _loadContainerConflicts(); + }); + } + + /// S'assure que les équipements sont chargés avant d'utiliser le dialog + Future _ensureEquipmentsLoaded() async { + final equipmentProvider = context.read(); + final containerProvider = context.read(); + await equipmentProvider.ensureLoaded(); + // Charger aussi les conteneurs si nécessaire + if (!containerProvider.isLoading && containerProvider.containers.isEmpty) { + await containerProvider.loadContainers(); + } } /// Initialise la sélection avec le matériel déjà assigné diff --git a/em2rp/lib/views/widgets/event_form/event_assigned_equipment_section.dart b/em2rp/lib/views/widgets/event_form/event_assigned_equipment_section.dart index e3ac694..432a281 100644 --- a/em2rp/lib/views/widgets/event_form/event_assigned_equipment_section.dart +++ b/em2rp/lib/views/widgets/event_form/event_assigned_equipment_section.dart @@ -63,9 +63,16 @@ class _EventAssignedEquipmentSectionState extends State(); final containerProvider = context.read(); - // Charger depuis les streams - final equipment = await equipmentProvider.equipmentStream.first; - final containers = await containerProvider.containersStream.first; + // Extraire les IDs des équipements assignés + final equipmentIds = widget.assignedEquipment + .map((eq) => eq.equipmentId) + .toList(); + + // Charger UNIQUEMENT les équipements nécessaires (optimisé) + final equipment = await equipmentProvider.getEquipmentsByIds(equipmentIds); + + // Charger UNIQUEMENT les conteneurs nécessaires (optimisé) + final containers = await containerProvider.getContainersByIds(widget.assignedContainers); // Créer le cache des équipements for (var eq in widget.assignedEquipment) { diff --git a/em2rp/lib/views/widgets/user_management/edit_user_dialog.dart b/em2rp/lib/views/widgets/user_management/edit_user_dialog.dart index 0162f7e..bab9329 100644 --- a/em2rp/lib/views/widgets/user_management/edit_user_dialog.dart +++ b/em2rp/lib/views/widgets/user_management/edit_user_dialog.dart @@ -42,9 +42,17 @@ class _EditUserDialogState extends State { availableRoles = rolesData .map((data) => RoleModel.fromMap(data, data['id'] as String)) .toList(); - selectedRoleId = widget.user.role.isEmpty - ? (availableRoles.isNotEmpty ? availableRoles.first.id : null) + // Extraire l'ID du rôle depuis le path "roles/ADMIN" -> "ADMIN" + String roleId = widget.user.role.isEmpty + ? (availableRoles.isNotEmpty ? availableRoles.first.id : '') : widget.user.role; + + // Si le rôle contient un slash, extraire seulement l'ID + if (roleId.contains('/')) { + roleId = roleId.split('/').last; + } + + selectedRoleId = roleId; isLoadingRoles = false; }); } catch (e) { diff --git a/em2rp/lib/views/widgets/user_management/user_card.dart b/em2rp/lib/views/widgets/user_management/user_card.dart index b3c296f..fcffe53 100644 --- a/em2rp/lib/views/widgets/user_management/user_card.dart +++ b/em2rp/lib/views/widgets/user_management/user_card.dart @@ -73,6 +73,14 @@ class _UserCardState extends State { } } + /// Extrait le nom du rôle depuis le path "roles/ADMIN" -> "ADMIN" + String _extractRoleName(String rolePath) { + if (rolePath.contains('/')) { + return rolePath.split('/').last; + } + return rolePath; + } + @override Widget build(BuildContext context) { final width = MediaQuery.of(context).size.width; @@ -178,7 +186,7 @@ class _UserCardState extends State { if (widget.user.role.isNotEmpty) ...[ const SizedBox(height: 4), Text( - widget.user.role, + _extractRoleName(widget.user.role), style: Theme.of(context).textTheme.bodySmall!.copyWith( color: AppColors.gris, fontSize: 11,