diff --git a/em2rp/functions/index.js b/em2rp/functions/index.js index b132402..2a3d3e7 100644 --- a/em2rp/functions/index.js +++ b/em2rp/functions/index.js @@ -510,6 +510,181 @@ exports.getContainersByIds = onRequest(httpOptions, withCors(async (req, res) => } })); +/** + * Ajouter un équipement à un container + */ +exports.addEquipmentToContainer = onRequest(httpOptions, withCors(async (req, res) => { + try { + const decodedToken = await auth.authenticateUser(req); + const hasAccess = await auth.hasPermission(decodedToken.uid, 'manage_equipment'); + + if (!hasAccess) { + res.status(403).json({ error: 'Forbidden: Requires manage_equipment permission' }); + return; + } + + const { containerId, equipmentId, userId } = req.body.data; + + if (!containerId || !equipmentId) { + res.status(400).json({ error: 'containerId and equipmentId are required' }); + return; + } + + // Récupérer le container + const containerDoc = await db.collection('containers').doc(containerId).get(); + if (!containerDoc.exists) { + res.status(404).json({ success: false, message: 'Container non trouvé' }); + return; + } + + const containerData = containerDoc.data(); + const equipmentIds = containerData.equipmentIds || []; + + // Vérifier si l'équipement n'est pas déjà dans ce container + if (equipmentIds.includes(equipmentId)) { + res.status(400).json({ success: false, message: 'Cet équipement est déjà dans ce container' }); + return; + } + + // Récupérer l'équipement + const equipmentDoc = await db.collection('equipments').doc(equipmentId).get(); + if (!equipmentDoc.exists) { + res.status(404).json({ success: false, message: 'Équipement non trouvé' }); + return; + } + + const equipmentData = equipmentDoc.data(); + const parentBoxIds = equipmentData.parentBoxIds || []; + + // Vérifier les autres containers + const warnings = []; + if (parentBoxIds.length > 0) { + const otherContainersPromises = parentBoxIds.map(boxId => + db.collection('containers').doc(boxId).get() + ); + const otherContainersDocs = await Promise.all(otherContainersPromises); + const otherNames = otherContainersDocs + .filter(doc => doc.exists) + .map(doc => doc.data().name); + + if (otherNames.length > 0) { + warnings.push(`Attention : cet équipement est également dans les boites suivants : ${otherNames.join(", ")}`); + } + } + + // Mettre à jour le container + await db.collection('containers').doc(containerId).update({ + equipmentIds: [...equipmentIds, equipmentId], + updatedAt: admin.firestore.Timestamp.now(), + }); + + // Mettre à jour l'équipement + await db.collection('equipments').doc(equipmentId).update({ + parentBoxIds: [...parentBoxIds, containerId], + updatedAt: admin.firestore.Timestamp.now(), + }); + + // Ajouter une entrée dans l'historique + const history = containerData.history || []; + const historyEntry = { + timestamp: admin.firestore.Timestamp.now(), + action: 'equipment_added', + equipmentId: equipmentId, + newValue: equipmentId, + userId: userId || decodedToken.uid, + }; + + const updatedHistory = [...history, historyEntry].slice(-100); // Garder les 100 dernières entrées + + await db.collection('containers').doc(containerId).update({ + history: updatedHistory, + }); + + res.status(200).json({ + success: true, + message: 'Équipement ajouté avec succès', + warnings: warnings.length > 0 ? warnings[0] : null, + }); + } catch (error) { + logger.error("Error adding equipment to container:", error); + res.status(500).json({ success: false, message: error.message }); + } +})); + +/** + * Retirer un équipement d'un container + */ +exports.removeEquipmentFromContainer = onRequest(httpOptions, withCors(async (req, res) => { + try { + const decodedToken = await auth.authenticateUser(req); + const hasAccess = await auth.hasPermission(decodedToken.uid, 'manage_equipment'); + + if (!hasAccess) { + res.status(403).json({ error: 'Forbidden: Requires manage_equipment permission' }); + return; + } + + const { containerId, equipmentId, userId } = req.body.data; + + if (!containerId || !equipmentId) { + res.status(400).json({ error: 'containerId and equipmentId are required' }); + return; + } + + // Récupérer le container + const containerDoc = await db.collection('containers').doc(containerId).get(); + if (!containerDoc.exists) { + res.status(404).json({ error: 'Container non trouvé' }); + return; + } + + const containerData = containerDoc.data(); + const equipmentIds = containerData.equipmentIds || []; + + // Retirer l'équipement du container + const updatedEquipmentIds = equipmentIds.filter(id => id !== equipmentId); + + await db.collection('containers').doc(containerId).update({ + equipmentIds: updatedEquipmentIds, + updatedAt: admin.firestore.Timestamp.now(), + }); + + // Mettre à jour l'équipement + const equipmentDoc = await db.collection('equipments').doc(equipmentId).get(); + if (equipmentDoc.exists) { + const equipmentData = equipmentDoc.data(); + const parentBoxIds = equipmentData.parentBoxIds || []; + const updatedParentBoxIds = parentBoxIds.filter(id => id !== containerId); + + await db.collection('equipments').doc(equipmentId).update({ + parentBoxIds: updatedParentBoxIds, + updatedAt: admin.firestore.Timestamp.now(), + }); + } + + // Ajouter une entrée dans l'historique + const history = containerData.history || []; + const historyEntry = { + timestamp: admin.firestore.Timestamp.now(), + action: 'equipment_removed', + equipmentId: equipmentId, + previousValue: equipmentId, + userId: userId || decodedToken.uid, + }; + + const updatedHistory = [...history, historyEntry].slice(-100); + + await db.collection('containers').doc(containerId).update({ + history: updatedHistory, + }); + + res.status(200).json({ success: true }); + } catch (error) { + logger.error("Error removing equipment from container:", error); + res.status(500).json({ error: error.message }); + } +})); + // ============================================================================ // EVENTS - CRUD @@ -1702,6 +1877,79 @@ exports.deleteAlert = onRequest(httpOptions, withCors(async (req, res) => { } })); +/** + * Créer une nouvelle alerte + */ +exports.createAlert = onRequest(httpOptions, withCors(async (req, res) => { + try { + const decodedToken = await auth.authenticateUser(req); + const hasAccess = await auth.hasPermission(decodedToken.uid, 'manage_equipment'); + + if (!hasAccess) { + res.status(403).json({ error: 'Forbidden: Requires manage_equipment permission' }); + return; + } + + const { type, title, message, severity, equipmentId } = req.body.data; + + if (!type || !message) { + res.status(400).json({ error: 'type and message are required' }); + return; + } + + // Vérifier si une alerte similaire existe déjà (éviter les doublons) + const existingAlertsQuery = await db.collection('alerts') + .where('type', '==', type) + .where('isRead', '==', false) + .get(); + + let alertExists = false; + if (equipmentId) { + // Pour les alertes liées à un équipement, vérifier aussi l'equipmentId + alertExists = existingAlertsQuery.docs.some(doc => + doc.data().equipmentId === equipmentId + ); + } else { + // Pour les autres alertes, vérifier le message + alertExists = existingAlertsQuery.docs.some(doc => + doc.data().message === message + ); + } + + if (alertExists) { + res.status(200).json({ + success: true, + message: 'Alert already exists', + skipped: true + }); + return; + } + + // Créer la nouvelle alerte + const alertData = { + type: type, + title: title || 'Alerte', + message: message, + severity: severity || 'MEDIUM', + isRead: false, + createdAt: admin.firestore.Timestamp.now(), + }; + + if (equipmentId) { + alertData.equipmentId = equipmentId; + } + + const alertRef = await db.collection('alerts').add(alertData); + + res.status(200).json({ + success: true, + alertId: alertRef.id + }); + } catch (error) { + logger.error("Error creating alert:", error); + res.status(500).json({ error: error.message }); + } +})); // ============================================================================ // USERS - Read with permissions @@ -2793,3 +3041,391 @@ exports.validateAllReturn = onRequest(httpOptions, withCors(async (req, res) => } })); +// ============================================================================ +// AVAILABILITY & STOCK CHECK +// ============================================================================ + +/** + * Vérifier la disponibilité d'un équipement pour une période donnée + */ +exports.checkEquipmentAvailability = onRequest(httpOptions, withCors(async (req, res) => { + try { + const decodedToken = await auth.authenticateUser(req); + const hasAccess = await auth.hasPermission(decodedToken.uid, 'view_equipment'); + + if (!hasAccess) { + res.status(403).json({ error: 'Forbidden: Requires view_equipment permission' }); + return; + } + + const { equipmentId, startDate, endDate } = req.body.data; + + if (!equipmentId || !startDate || !endDate) { + res.status(400).json({ error: 'equipmentId, startDate and endDate are required' }); + return; + } + + const start = admin.firestore.Timestamp.fromDate(new Date(startDate)); + const end = admin.firestore.Timestamp.fromDate(new Date(endDate)); + + // Récupérer les événements qui chevauchent la période + const eventsSnapshot = await db.collection('events') + .where('StartDateTime', '<=', end) + .where('EndDateTime', '>=', start) + .where('status', '!=', 'CANCELLED') + .get(); + + const conflicts = []; + + eventsSnapshot.docs.forEach(doc => { + const eventData = doc.data(); + const assignedEquipment = eventData.assignedEquipment || []; + + for (const eq of assignedEquipment) { + if (eq.equipmentId === equipmentId) { + conflicts.push({ + eventId: doc.id, + eventName: eventData.Name || 'Sans nom', + startDate: eventData.StartDateTime.toDate().toISOString(), + endDate: eventData.EndDateTime.toDate().toISOString(), + }); + break; + } + } + }); + + res.status(200).json({ conflicts }); + } catch (error) { + logger.error("Error checking equipment availability:", error); + res.status(500).json({ error: error.message }); + } +})); + +/** + * Trouver des alternatives (même modèle) disponibles pour une période donnée + */ +exports.findAlternativeEquipment = onRequest(httpOptions, withCors(async (req, res) => { + try { + const decodedToken = await auth.authenticateUser(req); + const hasAccess = await auth.hasPermission(decodedToken.uid, 'view_equipment'); + + if (!hasAccess) { + res.status(403).json({ error: 'Forbidden: Requires view_equipment permission' }); + return; + } + + const { model, startDate, endDate } = req.body.data; + + if (!model || !startDate || !endDate) { + res.status(400).json({ error: 'model, startDate and endDate are required' }); + return; + } + + const start = admin.firestore.Timestamp.fromDate(new Date(startDate)); + const end = admin.firestore.Timestamp.fromDate(new Date(endDate)); + + // Récupérer tous les équipements du même modèle + const equipmentsSnapshot = await db.collection('equipments') + .where('model', '==', model) + .get(); + + // Récupérer tous les événements qui chevauchent la période + const eventsSnapshot = await db.collection('events') + .where('StartDateTime', '<=', end) + .where('EndDateTime', '>=', start) + .where('status', '!=', 'CANCELLED') + .get(); + + // Créer un set des équipements en conflit + const conflictingEquipmentIds = new Set(); + eventsSnapshot.docs.forEach(doc => { + const eventData = doc.data(); + const assignedEquipment = eventData.assignedEquipment || []; + assignedEquipment.forEach(eq => conflictingEquipmentIds.add(eq.equipmentId)); + }); + + // Filtrer les équipements disponibles + const alternatives = []; + equipmentsSnapshot.docs.forEach(doc => { + const data = doc.data(); + if (!conflictingEquipmentIds.has(doc.id) && data.status === 'available') { + alternatives.push({ + id: doc.id, + ...helpers.serializeTimestamps(data, ['purchaseDate', 'nextMaintenanceDate', 'lastMaintenanceDate', 'createdAt', 'updatedAt']) + }); + } + }); + + res.status(200).json({ alternatives }); + } catch (error) { + logger.error("Error finding alternative equipment:", error); + res.status(500).json({ error: error.message }); + } +})); + +/** + * Calculer le statut réel d'un ou plusieurs équipements basé sur les événements en cours + */ +exports.calculateEquipmentStatuses = onRequest(httpOptions, withCors(async (req, res) => { + try { + const decodedToken = await auth.authenticateUser(req); + const hasAccess = await auth.hasPermission(decodedToken.uid, 'view_equipment'); + + if (!hasAccess) { + res.status(403).json({ error: 'Forbidden: Requires view_equipment permission' }); + return; + } + + const { equipmentIds } = req.body.data; + + if (!equipmentIds || !Array.isArray(equipmentIds)) { + res.status(400).json({ error: 'equipmentIds array is required' }); + return; + } + + // Récupérer tous les événements en cours (préparation complétée mais pas encore retournés) + const eventsSnapshot = await db.collection('events') + .where('status', '!=', 'CANCELLED') + .get(); + + const equipmentIdsInUse = new Set(); + const containerIdsInUse = new Set(); + + eventsSnapshot.docs.forEach(doc => { + const event = doc.data(); + + const isPrepared = event.preparationStatus === 'completed' || + event.preparationStatus === 'completedWithMissing'; + const isReturned = event.returnStatus === 'completed' || + event.returnStatus === 'completedWithMissing'; + + if (isPrepared && !isReturned) { + // Ajouter les équipements directs + const assignedEquipment = event.assignedEquipment || []; + assignedEquipment.forEach(eq => equipmentIdsInUse.add(eq.equipmentId)); + + // Ajouter les conteneurs + const assignedContainers = event.assignedContainers || []; + assignedContainers.forEach(containerId => containerIdsInUse.add(containerId)); + } + }); + + // Récupérer les équipements dans les conteneurs en cours d'utilisation + if (containerIdsInUse.size > 0) { + const containersSnapshot = await db.collection('containers') + .where(admin.firestore.FieldPath.documentId(), 'in', Array.from(containerIdsInUse)) + .get(); + + containersSnapshot.docs.forEach(doc => { + const containerData = doc.data(); + const equipmentList = containerData.equipment || []; + equipmentList.forEach(eq => equipmentIdsInUse.add(eq.equipmentId)); + }); + } + + // Récupérer les données des équipements demandés + const statuses = {}; + + for (const equipmentId of equipmentIds) { + const equipmentDoc = await db.collection('equipments').doc(equipmentId).get(); + + if (!equipmentDoc.exists) { + statuses[equipmentId] = null; + continue; + } + + const equipmentData = equipmentDoc.data(); + let calculatedStatus = equipmentData.status; + + // Si l'équipement est perdu ou HS, garder ce statut + if (equipmentData.status === 'lost' || equipmentData.status === 'outOfService') { + calculatedStatus = equipmentData.status; + } else if (equipmentIdsInUse.has(equipmentId)) { + calculatedStatus = 'inUse'; + } else if (equipmentData.status === 'maintenance' || + equipmentData.status === 'rented') { + calculatedStatus = equipmentData.status; + } else { + calculatedStatus = 'available'; + } + + statuses[equipmentId] = calculatedStatus; + } + + res.status(200).json({ statuses }); + } catch (error) { + logger.error("Error calculating equipment statuses:", error); + res.status(500).json({ error: error.message }); + } +})); + +/** + * Récupérer tous les événements en cours (pour le calcul de statuts) + */ +exports.getActiveEvents = onRequest(httpOptions, withCors(async (req, res) => { + try { + const decodedToken = await auth.authenticateUser(req); + const hasAccess = await auth.hasPermission(decodedToken.uid, 'view_events'); + + if (!hasAccess) { + res.status(403).json({ error: 'Forbidden: Requires view_events permission' }); + return; + } + + // Récupérer les événements en cours (préparation complétée mais pas encore retournés) + const eventsSnapshot = await db.collection('events') + .where('status', '!=', 'CANCELLED') + .get(); + + const activeEvents = []; + + eventsSnapshot.docs.forEach(doc => { + const event = doc.data(); + + const isPrepared = event.preparationStatus === 'completed' || + event.preparationStatus === 'completedWithMissing'; + const isReturned = event.returnStatus === 'completed' || + event.returnStatus === 'completedWithMissing'; + + if (isPrepared && !isReturned) { + activeEvents.push({ + id: doc.id, + assignedEquipment: event.assignedEquipment || [], + assignedContainers: event.assignedContainers || [], + preparationStatus: event.preparationStatus, + returnStatus: event.returnStatus, + }); + } + }); + + res.status(200).json({ events: activeEvents }); + } catch (error) { + logger.error("Error fetching active events:", error); + res.status(500).json({ error: error.message }); + } +})); + +/** + * Vérifier les maintenances à venir et créer des alertes + */ +exports.checkUpcomingMaintenances = onRequest(httpOptions, withCors(async (req, res) => { + try { + const decodedToken = await auth.authenticateUser(req); + const hasAccess = await auth.hasPermission(decodedToken.uid, 'manage_equipment'); + + if (!hasAccess) { + res.status(403).json({ error: 'Forbidden: Requires manage_equipment permission' }); + return; + } + + const sevenDaysFromNow = new Date(); + sevenDaysFromNow.setDate(sevenDaysFromNow.getDate() + 7); + + const now = admin.firestore.Timestamp.now(); + const sevenDaysTimestamp = admin.firestore.Timestamp.fromDate(sevenDaysFromNow); + + // Récupérer les maintenances planifiées dans les 7 prochains jours + const maintenancesSnapshot = await db.collection('maintenances') + .where('scheduledDate', '<=', sevenDaysTimestamp) + .where('scheduledDate', '>=', now) + .get(); + + const alertsCreated = []; + + for (const doc of maintenancesSnapshot.docs) { + const maintenance = doc.data(); + + // Vérifier si une alerte existe déjà pour cette maintenance + const existingAlertSnapshot = await db.collection('alerts') + .where('type', '==', 'MAINTENANCE_DUE') + .where('relatedMaintenanceId', '==', doc.id) + .get(); + + if (existingAlertSnapshot.empty) { + // Créer une nouvelle alerte + const alertData = { + type: 'MAINTENANCE_DUE', + title: `Maintenance à venir`, + message: `Une maintenance est prévue pour ${maintenance.equipmentIds?.length || 0} équipement(s)`, + severity: 'MEDIUM', + isRead: false, + relatedMaintenanceId: doc.id, + createdAt: admin.firestore.Timestamp.now(), + }; + + const alertRef = await db.collection('alerts').add(alertData); + alertsCreated.push({ id: alertRef.id, ...alertData }); + } + } + + res.status(200).json({ + success: true, + alertsCreated: alertsCreated.length, + alerts: alertsCreated + }); + } catch (error) { + logger.error("Error checking upcoming maintenances:", error); + res.status(500).json({ error: error.message }); + } +})); + +/** + * Compléter une maintenance + */ +exports.completeMaintenance = onRequest(httpOptions, withCors(async (req, res) => { + try { + const decodedToken = await auth.authenticateUser(req); + const hasAccess = await auth.hasPermission(decodedToken.uid, 'manage_equipment'); + + if (!hasAccess) { + res.status(403).json({ error: 'Forbidden: Requires manage_equipment permission' }); + return; + } + + const { maintenanceId, performedBy, cost } = req.body.data; + + if (!maintenanceId) { + res.status(400).json({ error: 'maintenanceId is required' }); + return; + } + + const now = admin.firestore.Timestamp.now(); + const updateData = { + completedDate: now, + updatedAt: now, + }; + + if (performedBy) { + updateData.performedBy = performedBy; + } + + if (cost !== undefined && cost !== null) { + updateData.cost = cost; + } + + // Mettre à jour la maintenance + await db.collection('maintenances').doc(maintenanceId).update(updateData); + + // Récupérer la maintenance pour mettre à jour les équipements + const maintenanceDoc = await db.collection('maintenances').doc(maintenanceId).get(); + const maintenanceData = maintenanceDoc.data(); + + // Mettre à jour la date de dernière maintenance des équipements + if (maintenanceData && maintenanceData.equipmentIds) { + const updatePromises = maintenanceData.equipmentIds.map(equipmentId => + db.collection('equipments').doc(equipmentId).update({ + lastMaintenanceDate: now, + updatedAt: now, + }) + ); + + await Promise.all(updatePromises); + } + + res.status(200).json({ success: true }); + } catch (error) { + logger.error("Error completing maintenance:", error); + res.status(500).json({ error: error.message }); + } +})); + diff --git a/em2rp/lib/controllers/event_form_controller.dart b/em2rp/lib/controllers/event_form_controller.dart index bf15e5f..0413543 100644 --- a/em2rp/lib/controllers/event_form_controller.dart +++ b/em2rp/lib/controllers/event_form_controller.dart @@ -294,7 +294,7 @@ class EventFormController extends ChangeNotifier { try { final eventTypeRef = _selectedEventTypeId != null - ? FirebaseFirestore.instance.collection('eventTypes').doc(_selectedEventTypeId) + ? null // Les références Firestore ne sont plus nécessaires, l'ID suffit : null; if (existingEvent != null) { diff --git a/em2rp/lib/main.dart b/em2rp/lib/main.dart index c229baf..0ae05a0 100644 --- a/em2rp/lib/main.dart +++ b/em2rp/lib/main.dart @@ -24,7 +24,6 @@ import 'views/my_account_page.dart'; import 'views/user_management_page.dart'; import 'package:provider/provider.dart'; import 'providers/local_user_provider.dart'; -import 'services/user_service.dart'; import 'views/reset_password_page.dart'; import 'config/env.dart'; import 'config/api_config.dart'; diff --git a/em2rp/lib/providers/container_provider.dart b/em2rp/lib/providers/container_provider.dart index fb8358d..352977f 100644 --- a/em2rp/lib/providers/container_provider.dart +++ b/em2rp/lib/providers/container_provider.dart @@ -39,17 +39,16 @@ class ContainerProvider with ChangeNotifier { notifyListeners(); try { - // Pour l'instant, on écoute le stream et on garde la première valeur - _containerService.getContainers( + final containers = await _containerService.getContainers( type: _selectedType, status: _selectedStatus, searchQuery: _searchQuery, - ).listen((containers) { - _containers = containers; - _isLoading = false; - _isInitialized = true; - notifyListeners(); - }); + ); + + _containers = containers; + _isLoading = false; + _isInitialized = true; + notifyListeners(); } catch (e) { print('Error loading containers: $e'); _isLoading = false; @@ -57,15 +56,32 @@ class ContainerProvider with ChangeNotifier { } } - /// Stream des containers avec filtres appliqués - Stream> get containersStream { - return _containerService.getContainers( + /// Récupérer les containers avec filtres appliqués + Future> getContainers() async { + return await _containerService.getContainers( type: _selectedType, status: _selectedStatus, searchQuery: _searchQuery, ); } + /// Stream des containers - retourne un stream depuis les données en cache + /// Pour compatibilité avec les widgets existants qui utilisent StreamBuilder + Stream> get containersStream async* { + // Si les données ne sont pas chargées, charger d'abord + if (!_isInitialized) { + await loadContainers(); + } + + // Émettre les données actuelles + yield _containers; + + // Continuer à émettre les mises à jour du cache + // Note: Pour un vrai temps réel, il faudrait implémenter un StreamController + // et notifier quand les données changent + } + + /// Définir le type sélectionné /// Définir le type sélectionné void setSelectedType(ContainerType? type) { _selectedType = type; diff --git a/em2rp/lib/providers/maintenance_provider.dart b/em2rp/lib/providers/maintenance_provider.dart index 9ef0912..68c1a83 100644 --- a/em2rp/lib/providers/maintenance_provider.dart +++ b/em2rp/lib/providers/maintenance_provider.dart @@ -10,14 +10,14 @@ class MaintenanceProvider extends ChangeNotifier { // Getters List get maintenances => _maintenances; - /// Stream des maintenances pour un équipement spécifique - Stream> getMaintenancesStream(String equipmentId) { - return _service.getMaintenances(equipmentId); + /// Récupérer les maintenances pour un équipement spécifique + Future> getMaintenances(String equipmentId) async { + return await _service.getMaintenancesByEquipment(equipmentId); } - /// Stream de toutes les maintenances - Stream> get allMaintenancesStream { - return _service.getAllMaintenances(); + /// Récupérer toutes les maintenances + Future> getAllMaintenances() async { + return await _service.getAllMaintenances(); } /// Créer une nouvelle maintenance diff --git a/em2rp/lib/services/container_service.dart b/em2rp/lib/services/container_service.dart index 58d0835..0a3e049 100644 --- a/em2rp/lib/services/container_service.dart +++ b/em2rp/lib/services/container_service.dart @@ -1,15 +1,11 @@ -import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:em2rp/models/container_model.dart'; import 'package:em2rp/models/equipment_model.dart'; import 'package:em2rp/services/api_service.dart'; +import 'package:em2rp/services/data_service.dart'; class ContainerService { - final FirebaseFirestore _firestore = FirebaseFirestore.instance; final ApiService _apiService = apiService; - - // Collection references - CollectionReference get _containersCollection => _firestore.collection('containers'); - CollectionReference get _equipmentCollection => _firestore.collection('equipments'); + final DataService _dataService = DataService(apiService); // ============================================================================ // CRUD Operations - Utilise le backend sécurisé @@ -52,11 +48,10 @@ class ContainerService { /// Récupérer un container par ID Future getContainerById(String id) async { try { - final doc = await _containersCollection.doc(id).get(); - if (doc.exists) { - return ContainerModel.fromMap(doc.data() as Map, doc.id); - } - return null; + final containersData = await _dataService.getContainersByIds([id]); + if (containersData.isEmpty) return null; + + return ContainerModel.fromMap(containersData.first, id); } catch (e) { print('Error getting container: $e'); rethrow; @@ -64,40 +59,40 @@ class ContainerService { } /// Récupérer tous les containers - Stream> getContainers({ + Future> getContainers({ ContainerType? type, EquipmentStatus? status, String? searchQuery, - }) { + }) async { try { - Query query = _containersCollection; + final containersData = await _dataService.getContainers(); - // Filtre par type + var containerList = containersData + .map((data) => ContainerModel.fromMap(data, data['id'] as String)) + .toList(); + + // Filtres côté client if (type != null) { - query = query.where('type', isEqualTo: containerTypeToString(type)); - } - - // Filtre par statut - if (status != null) { - query = query.where('status', isEqualTo: equipmentStatusToString(status)); - } - - return query.snapshots().map((snapshot) { - List containerList = snapshot.docs - .map((doc) => ContainerModel.fromMap(doc.data() as Map, doc.id)) + containerList = containerList + .where((c) => c.type == type) .toList(); + } - // Filtre par recherche texte (côté client) - if (searchQuery != null && searchQuery.isNotEmpty) { - final lowerSearch = searchQuery.toLowerCase(); - containerList = containerList.where((container) { - return container.name.toLowerCase().contains(lowerSearch) || - container.id.toLowerCase().contains(lowerSearch); - }).toList(); - } + if (status != null) { + containerList = containerList + .where((c) => c.status == status) + .toList(); + } - return containerList; - }); + if (searchQuery != null && searchQuery.isNotEmpty) { + final lowerSearch = searchQuery.toLowerCase(); + containerList = containerList.where((container) { + return container.name.toLowerCase().contains(lowerSearch) || + container.id.toLowerCase().contains(lowerSearch); + }).toList(); + } + + return containerList; } catch (e) { print('Error getting containers: $e'); rethrow; @@ -111,67 +106,16 @@ class ContainerService { String? userId, }) async { try { - // Récupérer le container - final container = await getContainerById(containerId); - if (container == null) { - return {'success': false, 'message': 'Container non trouvé'}; - } - - // Vérifier si l'équipement n'est pas déjà dans ce container - if (container.equipmentIds.contains(equipmentId)) { - return {'success': false, 'message': 'Cet équipement est déjà dans ce container'}; - } - - // Récupérer l'équipement pour vérifier s'il est déjà dans d'autres containers - final equipmentDoc = await _equipmentCollection.doc(equipmentId).get(); - if (!equipmentDoc.exists) { - return {'success': false, 'message': 'Équipement non trouvé'}; - } - - final equipment = EquipmentModel.fromMap( - equipmentDoc.data() as Map, - equipmentDoc.id, - ); - - // Avertir si l'équipement est déjà dans d'autres containers - List otherContainers = []; - if (equipment.parentBoxIds.isNotEmpty) { - for (final boxId in equipment.parentBoxIds) { - final box = await getContainerById(boxId); - if (box != null) { - otherContainers.add(box.name); - } - } - } - - // Mettre à jour le container - final updatedEquipmentIds = [...container.equipmentIds, equipmentId]; - await updateContainer(containerId, { - 'equipmentIds': updatedEquipmentIds, + final response = await _apiService.call('addEquipmentToContainer', { + 'containerId': containerId, + 'equipmentId': equipmentId, + if (userId != null) 'userId': userId, }); - // Mettre à jour l'équipement - final updatedParentBoxIds = [...equipment.parentBoxIds, containerId]; - await _equipmentCollection.doc(equipmentId).update({ - 'parentBoxIds': updatedParentBoxIds, - 'updatedAt': Timestamp.fromDate(DateTime.now()), - }); - - // Ajouter une entrée dans l'historique - await _addHistoryEntry( - containerId: containerId, - action: 'equipment_added', - equipmentId: equipmentId, - newValue: equipmentId, - userId: userId, - ); - return { - 'success': true, - 'message': 'Équipement ajouté avec succès', - 'warnings': otherContainers.isNotEmpty - ? 'Attention : cet équipement est également dans les boites suivants : ${otherContainers.join(", ")}' - : null, + 'success': response['success'] ?? false, + 'message': response['message'] ?? '', + 'warnings': response['warnings'], }; } catch (e) { print('Error adding equipment to container: $e'); @@ -186,38 +130,11 @@ class ContainerService { String? userId, }) async { try { - // Récupérer le container - final container = await getContainerById(containerId); - if (container == null) throw Exception('Container non trouvé'); - - // Mettre à jour le container - final updatedEquipmentIds = container.equipmentIds.where((id) => id != equipmentId).toList(); - await updateContainer(containerId, { - 'equipmentIds': updatedEquipmentIds, + await _apiService.call('removeEquipmentFromContainer', { + 'containerId': containerId, + 'equipmentId': equipmentId, + if (userId != null) 'userId': userId, }); - - // Mettre à jour l'équipement - final equipmentDoc = await _equipmentCollection.doc(equipmentId).get(); - if (equipmentDoc.exists) { - final equipment = EquipmentModel.fromMap( - equipmentDoc.data() as Map, - equipmentDoc.id, - ); - final updatedParentBoxIds = equipment.parentBoxIds.where((id) => id != containerId).toList(); - await _equipmentCollection.doc(equipmentId).update({ - 'parentBoxIds': updatedParentBoxIds, - 'updatedAt': Timestamp.fromDate(DateTime.now()), - }); - } - - // Ajouter une entrée dans l'historique - await _addHistoryEntry( - containerId: containerId, - action: 'equipment_removed', - equipmentId: equipmentId, - previousValue: equipmentId, - userId: userId, - ); } catch (e) { print('Error removing equipment from container: $e'); rethrow; @@ -247,14 +164,12 @@ class ContainerService { // Vérifier la disponibilité de chaque équipement dans le container List unavailableEquipment = []; - for (final equipmentId in container.equipmentIds) { - final equipmentDoc = await _equipmentCollection.doc(equipmentId).get(); - if (equipmentDoc.exists) { - final equipment = EquipmentModel.fromMap( - equipmentDoc.data() as Map, - equipmentDoc.id, - ); + if (container.equipmentIds.isNotEmpty) { + final equipmentsData = await _dataService.getEquipmentsByIds(container.equipmentIds); + + for (var data in equipmentsData) { + final equipment = EquipmentModel.fromMap(data, data['id'] as String); if (equipment.status != EquipmentStatus.available) { unavailableEquipment.add('${equipment.name} (${equipment.status})'); } @@ -282,15 +197,13 @@ class ContainerService { final container = await getContainerById(containerId); if (container == null) return []; - List equipment = []; - for (final equipmentId in container.equipmentIds) { - final doc = await _equipmentCollection.doc(equipmentId).get(); - if (doc.exists) { - equipment.add(EquipmentModel.fromMap(doc.data() as Map, doc.id)); - } - } + if (container.equipmentIds.isEmpty) return []; - return equipment; + final equipmentsData = await _dataService.getEquipmentsByIds(container.equipmentIds); + + return equipmentsData + .map((data) => EquipmentModel.fromMap(data, data['id'] as String)) + .toList(); } catch (e) { print('Error getting container equipment: $e'); rethrow; @@ -300,12 +213,10 @@ class ContainerService { /// Trouver tous les containers contenant un équipement spécifique Future> findContainersWithEquipment(String equipmentId) async { try { - final snapshot = await _containersCollection - .where('equipmentIds', arrayContains: equipmentId) - .get(); + final containersData = await _dataService.getContainersByEquipment(equipmentId); - return snapshot.docs - .map((doc) => ContainerModel.fromMap(doc.data() as Map, doc.id)) + return containersData + .map((data) => ContainerModel.fromMap(data, data['id'] as String)) .toList(); } catch (e) { print('Error finding containers with equipment: $e'); @@ -354,8 +265,8 @@ class ContainerService { /// Vérifier si un ID de container existe déjà Future checkContainerIdExists(String id) async { try { - final doc = await _containersCollection.doc(id).get(); - return doc.exists; + final container = await getContainerById(id); + return container != null; } catch (e) { print('Error checking container ID: $e'); return false; diff --git a/em2rp/lib/services/equipment_service.dart b/em2rp/lib/services/equipment_service.dart index 887ecf5..a0e89f0 100644 --- a/em2rp/lib/services/equipment_service.dart +++ b/em2rp/lib/services/equipment_service.dart @@ -1,19 +1,14 @@ -import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:em2rp/models/equipment_model.dart'; -import 'package:em2rp/models/container_model.dart'; -import 'package:em2rp/models/alert_model.dart'; import 'package:em2rp/models/maintenance_model.dart'; +import 'package:em2rp/models/container_model.dart'; import 'package:em2rp/services/api_service.dart'; import 'package:em2rp/services/data_service.dart'; +import 'package:em2rp/services/maintenance_service.dart'; class EquipmentService { - final FirebaseFirestore _firestore = FirebaseFirestore.instance; final ApiService _apiService = apiService; final DataService _dataService = DataService(apiService); - // Collection references (utilisées seulement pour les lectures) - CollectionReference get _equipmentCollection => _firestore.collection('equipments'); - // ============================================================================ // CRUD Operations - Utilise le backend sécurisé // ============================================================================ @@ -58,61 +53,61 @@ class EquipmentService { /// Récupérer un équipement par ID Future getEquipmentById(String id) async { try { - final doc = await _equipmentCollection.doc(id).get(); - if (doc.exists) { - return EquipmentModel.fromMap(doc.data() as Map, doc.id); - } - return null; + final equipmentsData = await _dataService.getEquipmentsByIds([id]); + if (equipmentsData.isEmpty) return null; + + return EquipmentModel.fromMap(equipmentsData.first, id); } catch (e) { print('Error getting equipment: $e'); rethrow; } } - /// Récupérer les équipements avec filtres (stream temps réel) - Stream> getEquipment({ + /// Récupérer les équipements avec filtres + Future> getEquipment({ EquipmentCategory? category, EquipmentStatus? status, String? model, String? searchQuery, - }) { + }) async { try { - Query query = _equipmentCollection; + final equipmentsData = await _dataService.getEquipments(); - // Filtre par catégorie + var equipmentList = equipmentsData + .map((data) => EquipmentModel.fromMap(data, data['id'] as String)) + .toList(); + + // Filtres côté client if (category != null) { - query = query.where('category', isEqualTo: equipmentCategoryToString(category)); - } - - // Filtre par statut - if (status != null) { - query = query.where('status', isEqualTo: equipmentStatusToString(status)); - } - - // Filtre par modèle - if (model != null && model.isNotEmpty) { - query = query.where('model', isEqualTo: model); - } - - return query.snapshots().map((snapshot) { - List equipmentList = snapshot.docs - .map((doc) => EquipmentModel.fromMap(doc.data() as Map, doc.id)) + equipmentList = equipmentList + .where((e) => e.category == category) .toList(); + } - // Filtre par recherche texte (côté client car Firestore ne supporte pas les recherches texte complexes) - if (searchQuery != null && searchQuery.isNotEmpty) { - final lowerSearch = searchQuery.toLowerCase(); - equipmentList = equipmentList.where((equipment) { - return equipment.name.toLowerCase().contains(lowerSearch) || - (equipment.model?.toLowerCase().contains(lowerSearch) ?? false) || - equipment.id.toLowerCase().contains(lowerSearch); - }).toList(); - } + if (status != null) { + equipmentList = equipmentList + .where((e) => e.status == status) + .toList(); + } - return equipmentList; - }); + if (model != null && model.isNotEmpty) { + equipmentList = equipmentList + .where((e) => e.model == model) + .toList(); + } + + if (searchQuery != null && searchQuery.isNotEmpty) { + final lowerSearch = searchQuery.toLowerCase(); + equipmentList = equipmentList.where((equipment) { + return equipment.name.toLowerCase().contains(lowerSearch) || + (equipment.model?.toLowerCase().contains(lowerSearch) ?? false) || + equipment.id.toLowerCase().contains(lowerSearch); + }).toList(); + } + + return equipmentList; } catch (e) { - print('Error streaming equipment: $e'); + print('Error getting equipment: $e'); rethrow; } } @@ -122,33 +117,21 @@ class EquipmentService { // ============================================================================ /// Vérifier la disponibilité d'un équipement pour une période donnée - Future> checkAvailability( + Future>> checkAvailability( String equipmentId, DateTime startDate, DateTime endDate, ) async { try { - final conflicts = []; + final response = await _apiService.call('checkEquipmentAvailability', { + 'equipmentId': equipmentId, + 'startDate': startDate.toIso8601String(), + 'endDate': endDate.toIso8601String(), + }); - // Récupérer tous les événements qui chevauchent la période - final eventsQuery = await _firestore.collection('events') - .where('StartDateTime', isLessThanOrEqualTo: Timestamp.fromDate(endDate)) - .where('EndDateTime', isGreaterThanOrEqualTo: Timestamp.fromDate(startDate)) - .get(); - - for (var eventDoc in eventsQuery.docs) { - final eventData = eventDoc.data(); - final assignedEquipmentRaw = eventData['assignedEquipment'] ?? []; - - if (assignedEquipmentRaw is List) { - for (var eq in assignedEquipmentRaw) { - if (eq is Map && eq['equipmentId'] == equipmentId) { - conflicts.add(eventDoc.id); - break; - } - } - } - } + final conflicts = (response['conflicts'] as List?) + ?.map((c) => c as Map) + .toList() ?? []; return conflicts; } catch (e) { @@ -164,26 +147,15 @@ class EquipmentService { DateTime endDate, ) async { try { - // Récupérer tous les équipements du même modèle - final equipmentQuery = await _firestore.collection('equipments') - .where('model', isEqualTo: model) - .get(); + final response = await _apiService.call('findAlternativeEquipment', { + 'model': model, + 'startDate': startDate.toIso8601String(), + 'endDate': endDate.toIso8601String(), + }); - final alternatives = []; - - for (var doc in equipmentQuery.docs) { - final equipment = EquipmentModel.fromMap( - doc.data(), - doc.id, - ); - - // Vérifier la disponibilité - final conflicts = await checkAvailability(equipment.id, startDate, endDate); - - if (conflicts.isEmpty && equipment.status == EquipmentStatus.available) { - alternatives.add(equipment); - } - } + final alternatives = (response['alternatives'] as List?) + ?.map((a) => EquipmentModel.fromMap(a as Map, a['id'] as String)) + .toList() ?? []; return alternatives; } catch (e) { @@ -224,20 +196,15 @@ class EquipmentService { /// Vérifier les stocks critiques et créer des alertes Future checkCriticalStock() async { try { - final equipmentQuery = await _firestore.collection('equipments') - .where('category', whereIn: [ - equipmentCategoryToString(EquipmentCategory.consumable), - equipmentCategoryToString(EquipmentCategory.cable), - ]) - .get(); + final equipmentsData = await _dataService.getEquipments(); - for (var doc in equipmentQuery.docs) { - final equipment = EquipmentModel.fromMap( - doc.data(), - doc.id, - ); + for (var data in equipmentsData) { + final equipment = EquipmentModel.fromMap(data, data['id'] as String); - if (equipment.isCriticalStock) { + // Filtrer uniquement les consommables et câbles + if ((equipment.category == EquipmentCategory.consumable || + equipment.category == EquipmentCategory.cable) && + equipment.isCriticalStock) { await _createLowStockAlert(equipment); } } @@ -250,27 +217,19 @@ class EquipmentService { /// Créer une alerte de stock faible Future _createLowStockAlert(EquipmentModel equipment) async { try { - // Vérifier si une alerte existe déjà pour cet équipement - final existingAlerts = await _firestore.collection('alerts') - .where('equipmentId', isEqualTo: equipment.id) - .where('type', isEqualTo: alertTypeToString(AlertType.lowStock)) - .where('isRead', isEqualTo: false) - .get(); - - if (existingAlerts.docs.isEmpty) { - final alert = AlertModel( - id: _firestore.collection('alerts').doc().id, - type: AlertType.lowStock, - message: 'Stock critique pour ${equipment.name} (${equipment.model ?? ""}): ${equipment.availableQuantity}/${equipment.criticalThreshold}', - equipmentId: equipment.id, - createdAt: DateTime.now(), - ); - - await _firestore.collection('alerts').doc(alert.id).set(alert.toMap()); - } + // Note: Cette fonction pourrait utiliser une Cloud Function dédiée dans le futur + // Pour l'instant, on utilise l'API directement pour éviter de créer trop de fonctions + // Cette méthode est appelée rarement et en arrière-plan + await _apiService.call('createAlert', { + 'type': 'LOW_STOCK', + 'title': 'Stock critique', + 'message': 'Stock critique pour ${equipment.name} (${equipment.model ?? ""}): ${equipment.availableQuantity}/${equipment.criticalThreshold}', + 'severity': 'HIGH', + 'equipmentId': equipment.id, + }); } catch (e) { print('Error creating low stock alert: $e'); - rethrow; + // Ne pas rethrow pour ne pas bloquer le processus } } @@ -284,11 +243,10 @@ class EquipmentService { /// Récupérer tous les modèles uniques (pour l'indexation/autocomplete) Future> getAllModels() async { try { - final equipmentQuery = await _firestore.collection('equipments').get(); + final equipmentsData = await _dataService.getEquipments(); final models = {}; - for (var doc in equipmentQuery.docs) { - final data = doc.data(); + for (var data in equipmentsData) { final model = data['model'] as String?; if (model != null && model.isNotEmpty) { models.add(model); @@ -305,11 +263,10 @@ class EquipmentService { /// Récupérer toutes les marques uniques (pour l'indexation/autocomplete) Future> getAllBrands() async { try { - final equipmentQuery = await _firestore.collection('equipments').get(); + final equipmentsData = await _dataService.getEquipments(); final brands = {}; - for (var doc in equipmentQuery.docs) { - final data = doc.data(); + for (var data in equipmentsData) { final brand = data['brand'] as String?; if (brand != null && brand.isNotEmpty) { brands.add(brand); @@ -326,16 +283,15 @@ class EquipmentService { /// Récupérer les modèles filtrés par marque Future> getModelsByBrand(String brand) async { try { - final equipmentQuery = await _firestore.collection('equipments') - .where('brand', isEqualTo: brand) - .get(); + final equipmentsData = await _dataService.getEquipments(); final models = {}; - for (var doc in equipmentQuery.docs) { - final data = doc.data(); - final model = data['model'] as String?; - if (model != null && model.isNotEmpty) { - models.add(model); + for (var data in equipmentsData) { + if (data['brand'] == brand) { + final model = data['model'] as String?; + if (model != null && model.isNotEmpty) { + models.add(model); + } } } @@ -349,8 +305,8 @@ class EquipmentService { /// Vérifier si un ID existe déjà Future isIdUnique(String id) async { try { - final doc = await _firestore.collection('equipments').doc(id).get(); - return !doc.exists; + final equipment = await getEquipmentById(id); + return equipment == null; } catch (e) { print('Error checking ID uniqueness: $e'); rethrow; @@ -381,27 +337,11 @@ class EquipmentService { try { if (ids.isEmpty) return []; - final equipments = []; + final equipmentsData = await _dataService.getEquipmentsByIds(ids); - // Firestore limite les requêtes whereIn à 10 éléments - // On doit donc diviser en plusieurs requêtes si nécessaire - for (int i = 0; i < ids.length; i += 10) { - final batch = ids.skip(i).take(10).toList(); - final query = await _firestore.collection('equipments') - .where(FieldPath.documentId, whereIn: batch) - .get(); - - for (var doc in query.docs) { - equipments.add( - EquipmentModel.fromMap( - doc.data(), - doc.id, - ), - ); - } - } - - return equipments; + return equipmentsData + .map((data) => EquipmentModel.fromMap(data, data['id'] as String)) + .toList(); } catch (e) { print('Error getting equipments by IDs: $e'); rethrow; @@ -409,25 +349,13 @@ class EquipmentService { } /// Récupérer les maintenances pour un équipement + /// Note: Cette méthode est maintenant déléguée au MaintenanceService + /// pour éviter la duplication de code Future> getMaintenancesForEquipment(String equipmentId) async { try { - final maintenanceQuery = await _firestore - .collection('maintenances') - .where('equipmentIds', arrayContains: equipmentId) - .orderBy('scheduledDate', descending: true) - .get(); - - final maintenances = []; - for (var doc in maintenanceQuery.docs) { - maintenances.add( - MaintenanceModel.fromMap( - doc.data(), - doc.id, - ), - ); - } - - return maintenances; + // Déléguer au MaintenanceService qui utilise déjà les Cloud Functions + final maintenanceService = MaintenanceService(); + return await maintenanceService.getMaintenancesByEquipment(equipmentId); } catch (e) { print('Error getting maintenances for equipment: $e'); rethrow; diff --git a/em2rp/lib/services/equipment_status_calculator.dart b/em2rp/lib/services/equipment_status_calculator.dart index ce823dc..caa6d3b 100644 --- a/em2rp/lib/services/equipment_status_calculator.dart +++ b/em2rp/lib/services/equipment_status_calculator.dart @@ -1,14 +1,13 @@ -import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:em2rp/models/equipment_model.dart'; -import 'package:em2rp/models/event_model.dart'; +import 'package:em2rp/services/api_service.dart'; /// Service pour calculer dynamiquement le statut réel d'un équipement /// basé sur les événements en cours class EquipmentStatusCalculator { - final FirebaseFirestore _firestore = FirebaseFirestore.instance; + final ApiService _apiService = apiService; - /// Cache des événements pour éviter de multiples requêtes - List? _cachedEvents; + /// Cache des statuts pour éviter de multiples requêtes + Map? _cachedStatuses; DateTime? _cacheTime; static const _cacheDuration = Duration(minutes: 1); @@ -25,205 +24,57 @@ class EquipmentStatusCalculator { Future calculateRealStatus(EquipmentModel equipment) async { print('[StatusCalculator] Calculating status for: ${equipment.id}'); - // Si l'équipement est marqué comme perdu ou HS, on garde ce statut - // car c'est une information métier importante - if (equipment.status == EquipmentStatus.lost || - equipment.status == EquipmentStatus.outOfService) { - print('[StatusCalculator] ${equipment.id} is lost/outOfService -> keeping status'); + try { + final statuses = await calculateMultipleStatuses([equipment]); + return statuses[equipment.id] ?? equipment.status; + } catch (e) { + print('[StatusCalculator] Error calculating status: $e'); return equipment.status; } - - // Charger les événements (avec cache) - await _loadEventsIfNeeded(); - print('[StatusCalculator] Events loaded: ${_cachedEvents?.length ?? 0}'); - - // Vérifier si l'équipement est utilisé dans un événement en cours - final isInUse = await _isEquipmentInUse(equipment.id); - print('[StatusCalculator] ${equipment.id} isInUse: $isInUse'); - - if (isInUse) { - return EquipmentStatus.inUse; - } - - // Vérifier si l'équipement est en maintenance - if (equipment.status == EquipmentStatus.maintenance) { - // On pourrait vérifier si la maintenance est toujours valide - // Pour l'instant on garde le statut - return EquipmentStatus.maintenance; - } - - // Vérifier si l'équipement est loué - if (equipment.status == EquipmentStatus.rented) { - // On pourrait vérifier une date de retour prévue - // Pour l'instant on garde le statut - return EquipmentStatus.rented; - } - - // Par défaut, l'équipement est disponible - print('[StatusCalculator] ${equipment.id} -> AVAILABLE'); - return EquipmentStatus.available; } /// Calcule les statuts pour une liste d'équipements (optimisé) Future> calculateMultipleStatuses( List equipments, ) async { - await _loadEventsIfNeeded(); - - final statuses = {}; - - // Trouver tous les équipements en cours d'utilisation - final equipmentIdsInUse = {}; - final containerIdsInUse = {}; - - for (var event in _cachedEvents ?? []) { - // Un équipement est "en prestation" dès que la préparation est complétée - // et jusqu'à ce que le retour soit complété - final isPrepared = event.preparationStatus == PreparationStatus.completed || - event.preparationStatus == PreparationStatus.completedWithMissing; - - final isReturned = event.returnStatus == ReturnStatus.completed || - event.returnStatus == ReturnStatus.completedWithMissing; - - final isInProgress = isPrepared && !isReturned; - - if (isInProgress) { - // Ajouter les équipements directs - for (var eq in event.assignedEquipment) { - equipmentIdsInUse.add(eq.equipmentId); - } - // Ajouter les conteneurs - containerIdsInUse.addAll(event.assignedContainers); - } - } - - // Récupérer les équipements dans les conteneurs en cours d'utilisation - if (containerIdsInUse.isNotEmpty) { - final containersSnapshot = await _firestore - .collection('containers') - .where(FieldPath.documentId, whereIn: containerIdsInUse.toList()) - .get(); - - for (var doc in containersSnapshot.docs) { - final data = doc.data(); - final equipmentIds = List.from(data['equipmentIds'] ?? []); - equipmentIdsInUse.addAll(equipmentIds); - } - } - - // Calculer le statut pour chaque équipement - for (var equipment in equipments) { - // Si perdu ou HS, on garde le statut - if (equipment.status == EquipmentStatus.lost || - equipment.status == EquipmentStatus.outOfService) { - statuses[equipment.id] = equipment.status; - continue; - } - - // Si en cours d'utilisation - if (equipmentIdsInUse.contains(equipment.id)) { - statuses[equipment.id] = EquipmentStatus.inUse; - continue; - } - - // Si en maintenance ou loué, on garde le statut - if (equipment.status == EquipmentStatus.maintenance || - equipment.status == EquipmentStatus.rented) { - statuses[equipment.id] = equipment.status; - continue; - } - - // Par défaut, disponible - statuses[equipment.id] = EquipmentStatus.available; - } - - return statuses; - } - - /// Vérifie si un équipement est actuellement en cours d'utilisation - Future _isEquipmentInUse(String equipmentId) async { - print('[StatusCalculator] Checking if $equipmentId is in use...'); - - // Vérifier dans les événements directs - for (var event in _cachedEvents ?? []) { - // Un équipement est "en prestation" dès que la préparation est complétée - // et jusqu'à ce que le retour soit complété - final isPrepared = event.preparationStatus == PreparationStatus.completed || - event.preparationStatus == PreparationStatus.completedWithMissing; - - final isReturned = event.returnStatus == ReturnStatus.completed || - event.returnStatus == ReturnStatus.completedWithMissing; - - final isInProgress = isPrepared && !isReturned; - - if (!isInProgress) continue; - - print('[StatusCalculator] Event ${event.name} is IN PROGRESS (prepared and not returned)'); - - // Vérifier si l'équipement est directement assigné - if (event.assignedEquipment.any((eq) => eq.equipmentId == equipmentId)) { - print('[StatusCalculator] $equipmentId found DIRECTLY in event ${event.name}'); - return true; - } - - // Vérifier si l'équipement est dans un conteneur assigné - if (event.assignedContainers.isNotEmpty) { - print('[StatusCalculator] Checking containers for event ${event.name}: ${event.assignedContainers}'); - final containersSnapshot = await _firestore - .collection('containers') - .where(FieldPath.documentId, whereIn: event.assignedContainers) - .get(); - - for (var doc in containersSnapshot.docs) { - final data = doc.data(); - final equipmentIds = List.from(data['equipmentIds'] ?? []); - print('[StatusCalculator] Container ${doc.id} contains: $equipmentIds'); - if (equipmentIds.contains(equipmentId)) { - print('[StatusCalculator] $equipmentId found in CONTAINER ${doc.id}'); - return true; - } - } - } - } - - print('[StatusCalculator] $equipmentId is NOT in use'); - return false; - } - - /// Charge les événements si le cache est expiré - Future _loadEventsIfNeeded() async { - if (_cachedEvents != null && - _cacheTime != null && - DateTime.now().difference(_cacheTime!) < _cacheDuration) { - return; // Cache encore valide - } - try { - final eventsSnapshot = await _firestore.collection('events').get(); + final equipmentIds = equipments.map((e) => e.id).toList(); - _cachedEvents = eventsSnapshot.docs - .map((doc) { - try { - return EventModel.fromMap(doc.data(), doc.id); - } catch (e) { - print('[EquipmentStatusCalculator] Error parsing event ${doc.id}: $e'); - return null; - } - }) - .whereType() - .where((event) => event.status != EventStatus.canceled) // Ignorer les événements annulés - .toList(); + final response = await _apiService.call('calculateEquipmentStatuses', { + 'equipmentIds': equipmentIds, + }); + final statusesMap = response['statuses'] as Map?; + if (statusesMap == null) { + throw Exception('Invalid response from calculateEquipmentStatuses'); + } + + final statuses = {}; + statusesMap.forEach((equipmentId, statusString) { + if (statusString != null) { + statuses[equipmentId] = equipmentStatusFromString(statusString as String); + } + }); + + // Mise en cache + _cachedStatuses = statuses; _cacheTime = DateTime.now(); + + return statuses; } catch (e) { - print('[EquipmentStatusCalculator] Error loading events: $e'); - _cachedEvents = []; + print('[StatusCalculator] Error calculating multiple statuses: $e'); + // En cas d'erreur, retourner les statuts actuels + final fallbackStatuses = {}; + for (var equipment in equipments) { + fallbackStatuses[equipment.id] = equipment.status; + } + return fallbackStatuses; } } /// Invalide le cache (à appeler après une modification d'événement) void invalidateCache() { - _cachedEvents = null; + _cachedStatuses = null; _cacheTime = null; } diff --git a/em2rp/lib/services/event_preparation_service.dart b/em2rp/lib/services/event_preparation_service.dart index 616ac23..f5a8150 100644 --- a/em2rp/lib/services/event_preparation_service.dart +++ b/em2rp/lib/services/event_preparation_service.dart @@ -1,17 +1,9 @@ -import 'package:cloud_firestore/cloud_firestore.dart'; -import 'package:em2rp/models/event_model.dart'; -import 'package:em2rp/models/equipment_model.dart'; import 'package:em2rp/services/equipment_status_calculator.dart'; import 'package:em2rp/services/api_service.dart'; class EventPreparationService { - final FirebaseFirestore _firestore = FirebaseFirestore.instance; final ApiService _apiService = apiService; - // Collection references (utilisées uniquement pour les lectures) - CollectionReference get _eventsCollection => _firestore.collection('events'); - CollectionReference get _equipmentCollection => _firestore.collection('equipments'); - // === PRÉPARATION === /// Valider un équipement individuel en préparation @@ -42,37 +34,18 @@ class EventPreparationService { } } - /// Finaliser la préparation avec des équipements manquants + // Ces méthodes ne sont plus utilisées et devraient être remplacées par des Cloud Functions + // si nécessaire dans le futur + + /* + @Deprecated('Use Cloud Functions instead') Future completePreparationWithMissing( String eventId, List missingEquipmentIds, ) async { - try { - final event = await _getEvent(eventId); - if (event == null) { - throw Exception('Event not found'); - } - - // Marquer comme complété avec manquants - await _eventsCollection.doc(eventId).update({ - 'preparationStatus': preparationStatusToString(PreparationStatus.completedWithMissing), - }); - - // Mettre à jour le statut des équipements préparés à "inUse" - for (var equipment in event.assignedEquipment) { - if (equipment.isPrepared && !missingEquipmentIds.contains(equipment.equipmentId)) { - // Vérifier si l'équipement existe avant de mettre à jour son statut - final doc = await _equipmentCollection.doc(equipment.equipmentId).get(); - if (doc.exists) { - await updateEquipmentStatus(equipment.equipmentId, EquipmentStatus.inUse); - } - } - } - } catch (e) { - print('Error completing preparation with missing: $e'); - rethrow; - } + throw UnimplementedError('This method is deprecated. Use Cloud Functions instead.'); } + */ // === RETOUR === @@ -114,186 +87,18 @@ class EventPreparationService { } } - /// Finaliser le retour avec des équipements manquants + /* + @Deprecated('Use Cloud Functions instead') Future completeReturnWithMissing( String eventId, List missingEquipmentIds, ) async { - try { - final event = await _getEvent(eventId); - if (event == null) { - throw Exception('Event not found'); - } - - // Marquer comme complété avec manquants - await _eventsCollection.doc(eventId).update({ - 'returnStatus': returnStatusToString(ReturnStatus.completedWithMissing), - }); - - // Mettre à jour le statut des équipements retournés à "available" - for (var equipment in event.assignedEquipment) { - // Vérifier si le document existe - final equipmentDoc = await _equipmentCollection.doc(equipment.equipmentId).get(); - if (!equipmentDoc.exists) { - continue; // Passer cet équipement s'il n'existe pas - } - - final equipmentData = EquipmentModel.fromMap( - equipmentDoc.data() as Map, - equipmentDoc.id, - ); - - if (equipment.isReturned && !missingEquipmentIds.contains(equipment.equipmentId)) { - // Mettre à jour le statut uniquement pour les équipements non quantifiables - if (!equipmentData.hasQuantity) { - await updateEquipmentStatus(equipment.equipmentId, EquipmentStatus.available); - } - - // Restaurer le stock pour les consommables - if (equipmentData.hasQuantity && equipment.returnedQuantity != null) { - final currentAvailable = equipmentData.availableQuantity ?? 0; - await _equipmentCollection.doc(equipment.equipmentId).update({ - 'availableQuantity': currentAvailable + equipment.returnedQuantity!, - 'updatedAt': Timestamp.fromDate(DateTime.now()), - }); - } - } else if (missingEquipmentIds.contains(equipment.equipmentId)) { - // Marquer comme perdu uniquement pour les équipements non quantifiables - if (!equipmentData.hasQuantity) { - await updateEquipmentStatus(equipment.equipmentId, EquipmentStatus.lost); - } - } - } - } catch (e) { - print('Error completing return with missing: $e'); - rethrow; - } + throw UnimplementedError('This method is deprecated. Use Cloud Functions instead.'); } - // === HELPERS === - - /// Mettre à jour le statut d'un équipement - Future updateEquipmentStatus(String equipmentId, EquipmentStatus status) async { - try { - // Vérifier que le document existe avant de le mettre à jour - final doc = await _equipmentCollection.doc(equipmentId).get(); - if (!doc.exists) { - print('Warning: Equipment document $equipmentId does not exist, skipping status update'); - return; - } - - await _equipmentCollection.doc(equipmentId).update({ - 'status': equipmentStatusToString(status), - 'updatedAt': Timestamp.fromDate(DateTime.now()), - }); - } catch (e) { - print('Error updating equipment status for $equipmentId: $e'); - // Ne pas rethrow pour ne pas bloquer le processus si un équipement n'existe pas - } - } - - /// Récupérer un événement - Future _getEvent(String eventId) async { - try { - final doc = await _eventsCollection.doc(eventId).get(); - if (doc.exists) { - return EventModel.fromMap(doc.data() as Map, doc.id); - } - return null; - } catch (e) { - print('Error getting event: $e'); - rethrow; - } - } - - /// Ajouter un équipement à un événement - Future addEquipmentToEvent( - String eventId, - String equipmentId, { - int quantity = 1, - }) async { - try { - final event = await _getEvent(eventId); - if (event == null) { - throw Exception('Event not found'); - } - - // Vérifier que l'équipement n'est pas déjà ajouté - final alreadyAdded = event.assignedEquipment.any((eq) => eq.equipmentId == equipmentId); - if (alreadyAdded) { - throw Exception('Equipment already added to event'); - } - - final newEquipment = EventEquipment( - equipmentId: equipmentId, - quantity: quantity, - ); - - final updatedEquipment = [...event.assignedEquipment, newEquipment]; - - await _eventsCollection.doc(eventId).update({ - 'assignedEquipment': updatedEquipment.map((e) => e.toMap()).toList(), - }); - - // Décrémenter le stock pour les consommables - final equipmentDoc = await _equipmentCollection.doc(equipmentId).get(); - if (equipmentDoc.exists) { - final equipmentData = EquipmentModel.fromMap( - equipmentDoc.data() as Map, - equipmentDoc.id, - ); - - if (equipmentData.hasQuantity) { - final currentAvailable = equipmentData.availableQuantity ?? 0; - await _equipmentCollection.doc(equipmentId).update({ - 'availableQuantity': currentAvailable - quantity, - }); - } - } - } catch (e) { - print('Error adding equipment to event: $e'); - rethrow; - } - } - - /// Retirer un équipement d'un événement - Future removeEquipmentFromEvent(String eventId, String equipmentId) async { - try { - final event = await _getEvent(eventId); - if (event == null) { - throw Exception('Event not found'); - } - - final equipmentToRemove = event.assignedEquipment.firstWhere( - (eq) => eq.equipmentId == equipmentId, - ); - - final updatedEquipment = event.assignedEquipment - .where((eq) => eq.equipmentId != equipmentId) - .toList(); - - await _eventsCollection.doc(eventId).update({ - 'assignedEquipment': updatedEquipment.map((e) => e.toMap()).toList(), - }); - - // Restaurer le stock pour les consommables - final equipmentDoc = await _equipmentCollection.doc(equipmentId).get(); - if (equipmentDoc.exists) { - final equipmentData = EquipmentModel.fromMap( - equipmentDoc.data() as Map, - equipmentDoc.id, - ); - - if (equipmentData.hasQuantity) { - final currentAvailable = equipmentData.availableQuantity ?? 0; - await _equipmentCollection.doc(equipmentId).update({ - 'availableQuantity': currentAvailable + equipmentToRemove.quantity, - }); - } - } - } catch (e) { - print('Error removing equipment from event: $e'); - rethrow; - } - } + // Les méthodes helper suivantes étaient uniquement utilisées par les méthodes deprecated ci-dessus. + // Elles ont été supprimées car elles accédaient directement à Firestore. + // Si ces fonctionnalités sont nécessaires à l'avenir, elles doivent être implémentées + // via des Cloud Functions pour respecter l'architecture. + */ } diff --git a/em2rp/lib/services/maintenance_service.dart b/em2rp/lib/services/maintenance_service.dart index e3ae454..07674ff 100644 --- a/em2rp/lib/services/maintenance_service.dart +++ b/em2rp/lib/services/maintenance_service.dart @@ -1,16 +1,9 @@ -import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:em2rp/models/maintenance_model.dart'; -import 'package:em2rp/models/alert_model.dart'; import 'package:em2rp/services/api_service.dart'; class MaintenanceService { - final FirebaseFirestore _firestore = FirebaseFirestore.instance; final ApiService _apiService = apiService; - // Collection references - CollectionReference get _maintenancesCollection => _firestore.collection('maintenances'); - CollectionReference get _equipmentCollection => _firestore.collection('equipments'); - CollectionReference get _alertsCollection => _firestore.collection('alerts'); // ============================================================================ // CRUD Operations - Utilise le backend sécurisé @@ -56,54 +49,54 @@ class MaintenanceService { /// Récupérer une maintenance par ID Future getMaintenanceById(String id) async { try { - final doc = await _maintenancesCollection.doc(id).get(); - if (doc.exists) { - return MaintenanceModel.fromMap(doc.data() as Map, doc.id); - } - return null; + final response = await _apiService.call('getMaintenances', { + 'maintenanceId': id, + }); + + final maintenances = (response['maintenances'] as List?) + ?.map((m) => MaintenanceModel.fromMap(m as Map, m['id'] as String)) + .toList(); + + return maintenances?.firstWhere( + (m) => m.id == id, + orElse: () => throw Exception('Maintenance not found'), + ); } catch (e) { print('Error getting maintenance: $e'); - rethrow; + return null; } } /// Récupérer l'historique des maintenances pour un équipement - Stream> getMaintenances(String equipmentId) { + Future> getMaintenancesByEquipment(String equipmentId) async { try { - return _maintenancesCollection - .where('equipmentIds', arrayContains: equipmentId) - .orderBy('scheduledDate', descending: true) - .snapshots() - .map((snapshot) { - return snapshot.docs - .map((doc) => MaintenanceModel.fromMap( - doc.data() as Map, - doc.id, - )) - .toList(); + final response = await _apiService.call('getMaintenances', { + 'equipmentId': equipmentId, }); + + final maintenances = (response['maintenances'] as List?) + ?.map((m) => MaintenanceModel.fromMap(m as Map, m['id'] as String)) + .toList() ?? []; + + return maintenances; } catch (e) { - print('Error streaming maintenances: $e'); + print('Error getting maintenances: $e'); rethrow; } } /// Récupérer toutes les maintenances - Stream> getAllMaintenances() { + Future> getAllMaintenances() async { try { - return _maintenancesCollection - .orderBy('scheduledDate', descending: true) - .snapshots() - .map((snapshot) { - return snapshot.docs - .map((doc) => MaintenanceModel.fromMap( - doc.data() as Map, - doc.id, - )) - .toList(); - }); + final response = await _apiService.call('getMaintenances', {}); + + final maintenances = (response['maintenances'] as List?) + ?.map((m) => MaintenanceModel.fromMap(m as Map, m['id'] as String)) + .toList() ?? []; + + return maintenances; } catch (e) { - print('Error streaming all maintenances: $e'); + print('Error getting all maintenances: $e'); rethrow; } } @@ -111,30 +104,11 @@ class MaintenanceService { /// Marquer une maintenance comme complétée Future completeMaintenance(String id, {String? performedBy, double? cost}) async { try { - final updateData = { - 'completedDate': Timestamp.fromDate(DateTime.now()), - 'updatedAt': Timestamp.fromDate(DateTime.now()), - }; - - if (performedBy != null) { - updateData['performedBy'] = performedBy; - } - - if (cost != null) { - updateData['cost'] = cost; - } - - await updateMaintenance(id, updateData); - - // Mettre à jour la date de dernière maintenance des équipements - final maintenance = await getMaintenanceById(id); - if (maintenance != null) { - for (String equipmentId in maintenance.equipmentIds) { - await _equipmentCollection.doc(equipmentId).update({ - 'lastMaintenanceDate': Timestamp.fromDate(DateTime.now()), - }); - } - } + await _apiService.call('completeMaintenance', { + 'maintenanceId': id, + if (performedBy != null) 'performedBy': performedBy, + if (cost != null) 'cost': cost, + }); } catch (e) { print('Error completing maintenance: $e'); rethrow; @@ -144,73 +118,10 @@ class MaintenanceService { /// Vérifier les maintenances à venir et créer des alertes Future checkUpcomingMaintenances() async { try { - final sevenDaysFromNow = DateTime.now().add(const Duration(days: 7)); - - // Récupérer les maintenances planifiées dans les 7 prochains jours - final maintenancesQuery = await _maintenancesCollection - .where('scheduledDate', isLessThanOrEqualTo: Timestamp.fromDate(sevenDaysFromNow)) - .where('completedDate', isNull: true) - .get(); - - for (var doc in maintenancesQuery.docs) { - final maintenance = MaintenanceModel.fromMap( - doc.data() as Map, - doc.id, - ); - - for (String equipmentId in maintenance.equipmentIds) { - await _createMaintenanceAlert(equipmentId, maintenance); - } - } + await _apiService.call('checkUpcomingMaintenances', {}); } catch (e) { print('Error checking upcoming maintenances: $e'); rethrow; } } - - /// Créer une alerte de maintenance à venir - Future _createMaintenanceAlert(String equipmentId, MaintenanceModel maintenance) async { - try { - // Vérifier si une alerte existe déjà - final existingAlerts = await _alertsCollection - .where('equipmentId', isEqualTo: equipmentId) - .where('type', isEqualTo: alertTypeToString(AlertType.maintenanceDue)) - .where('isRead', isEqualTo: false) - .get(); - - // Vérifier si l'alerte concerne la même maintenance - bool alertExists = false; - for (var alertDoc in existingAlerts.docs) { - final alertData = alertDoc.data() as Map; - if (alertData['message']?.contains(maintenance.name) ?? false) { - alertExists = true; - break; - } - } - - if (!alertExists) { - // Récupérer l'équipement pour le nom - final equipmentDoc = await _equipmentCollection.doc(equipmentId).get(); - String equipmentName = equipmentId; - if (equipmentDoc.exists) { - final equipmentData = equipmentDoc.data() as Map; - equipmentName = equipmentData['name'] ?? equipmentId; - } - - final daysUntil = maintenance.scheduledDate.difference(DateTime.now()).inDays; - final alert = AlertModel( - id: _alertsCollection.doc().id, - type: AlertType.maintenanceDue, - message: 'Maintenance "${maintenance.name}" prévue dans $daysUntil jour(s) pour $equipmentName', - equipmentId: equipmentId, - createdAt: DateTime.now(), - ); - - await _alertsCollection.doc(alert.id).set(alert.toMap()); - } - } catch (e) { - print('Error creating maintenance alert: $e'); - rethrow; - } - } }