const {onCall} = require('firebase-functions/v2/https'); const admin = require('firebase-admin'); const logger = require('firebase-functions/logger'); const nodemailer = require('nodemailer'); const {getSmtpConfig, EMAIL_CONFIG} = require('./utils/emailConfig'); /** * Traite la validation du matériel d'un événement * Appelée par le client lors du chargement/déchargement * Crée automatiquement les alertes nécessaires */ exports.processEquipmentValidation = onCall({cors: true}, async (request) => { try { // L'authentification est automatique avec onCall const {auth, data} = request; if (!auth) { throw new Error('L\'utilisateur doit être authentifié'); } const { eventId, equipmentList, // [{equipmentId, status, quantity, etc.}] validationType, // 'LOADING', 'UNLOADING', 'CHECK_OUT', 'CHECK_IN' } = data; // Validation if (!eventId || !equipmentList || !validationType) { throw new Error('eventId, equipmentList et validationType sont requis'); } const db = admin.firestore(); const alerts = []; // 1. Récupérer les détails de l'événement const eventRef = db.collection('events').doc(eventId); const eventDoc = await eventRef.get(); if (!eventDoc.exists) { throw new Error('Événement introuvable'); } const event = eventDoc.data(); const eventName = event.Name || event.name || 'Événement inconnu'; const eventDate = formatEventDate(event); // 2. Analyser les équipements et détecter les problèmes for (const equipment of equipmentList) { const {equipmentId, status, quantity, expectedQuantity} = equipment; // Cas 1: Équipement PERDU if (status === 'LOST') { const alertData = await createAlertInFirestore({ type: 'LOST', severity: 'CRITICAL', title: 'Équipement perdu', message: `Équipement "${equipment.name || equipmentId}" perdu lors de l'événement "${eventName}" (${eventDate})`, equipmentId, eventId, eventName, eventDate, createdBy: auth.uid, metadata: { validationType, equipment, }, }); alerts.push(alertData); } // Cas 2: Équipement MANQUANT if (status === 'MISSING') { const alertData = await createAlertInFirestore({ type: 'EQUIPMENT_MISSING', severity: 'WARNING', title: 'Équipement manquant', message: `Équipement "${equipment.name || equipmentId}" manquant pour l'événement "${eventName}" (${eventDate})`, equipmentId, eventId, eventName, eventDate, createdBy: auth.uid, metadata: { validationType, equipment, }, }); alerts.push(alertData); } // Cas 3: Quantité incorrecte if (expectedQuantity && quantity !== expectedQuantity) { const alertData = await createAlertInFirestore({ type: 'QUANTITY_MISMATCH', severity: 'INFO', title: 'Quantité incorrecte', message: `Quantité incorrecte pour "${equipment.name || equipmentId}": ${quantity} au lieu de ${expectedQuantity} attendus`, equipmentId, eventId, eventName, eventDate, createdBy: auth.uid, metadata: { validationType, equipment, expected: expectedQuantity, actual: quantity, }, }); alerts.push(alertData); } // Cas 4: Équipement endommagé if (status === 'DAMAGED') { const alertData = await createAlertInFirestore({ type: 'DAMAGED', severity: 'WARNING', title: 'Équipement endommagé', message: `Équipement "${equipment.name || equipmentId}" endommagé durant l'événement "${eventName}" (${eventDate})`, equipmentId, eventId, eventName, eventDate, createdBy: auth.uid, metadata: { validationType, equipment, }, }); alerts.push(alertData); } } // 3. Mettre à jour les équipements de l'événement await eventRef.update({ equipment: equipmentList, lastValidation: { type: validationType, timestamp: admin.firestore.FieldValue.serverTimestamp(), by: auth.uid, }, }); // 4. Envoyer les notifications pour les alertes critiques const criticalAlerts = alerts.filter((a) => a.severity === 'CRITICAL'); if (criticalAlerts.length > 0) { for (const alert of criticalAlerts) { try { await sendAlertNotifications(alert, eventId); } catch (notificationError) { logger.error(`[processEquipmentValidation] Erreur notification alerte ${alert.id}:`, notificationError); } } } return { success: true, alertsCreated: alerts.length, criticalAlertsCount: criticalAlerts.length, alertIds: alerts.map((a) => a.id), }; } catch (error) { logger.error('[processEquipmentValidation] Erreur:', error); throw error; } }); /** * Crée une alerte dans Firestore */ async function createAlertInFirestore(alertData) { const db = admin.firestore(); const alertRef = db.collection('alerts').doc(); const fullAlertData = { id: alertRef.id, ...alertData, createdAt: admin.firestore.FieldValue.serverTimestamp(), isRead: false, status: 'ACTIVE', emailSent: false, assignedTo: [], }; await alertRef.set(fullAlertData); return {...fullAlertData, id: alertRef.id}; } /** * Détermine les utilisateurs à notifier et envoie les notifications */ async function sendAlertNotifications(alert, eventId) { const db = admin.firestore(); const targetUserIds = new Set(); const usersWithPermission = new Set(); try { // 1. Récupérer TOUS les utilisateurs et leurs permissions const allUsersSnapshot = await db.collection('users').get(); // Créer un map pour stocker les références de rôles à récupérer const roleRefs = new Map(); for (const doc of allUsersSnapshot.docs) { const user = doc.data(); if (!user.role) { continue; } // Extraire le chemin du rôle let rolePath = ''; let roleId = ''; if (typeof user.role === 'string') { rolePath = user.role; roleId = user.role.split('/').pop(); } else if (user.role.path) { rolePath = user.role.path; roleId = user.role.path.split('/').pop(); } else if (user.role._path && user.role._path.segments) { rolePath = user.role._path.segments.join('/'); roleId = user.role._path.segments[user.role._path.segments.length - 1]; } if (roleId && !roleRefs.has(roleId)) { roleRefs.set(roleId, {users: [], rolePath}); } if (roleId) { roleRefs.get(roleId).users.push(doc.id); } } // 2. Récupérer les permissions de chaque rôle unique for (const [roleId, {users, rolePath}] of roleRefs.entries()) { try { const roleDoc = await db.collection('roles').doc(roleId).get(); if (roleDoc.exists) { const roleData = roleDoc.data(); const permissions = roleData.permissions || []; // Vérifier si le rôle a la permission view_all_events if (permissions.includes('view_all_events')) { users.forEach((userId) => { usersWithPermission.add(userId); targetUserIds.add(userId); }); } } } catch (error) { logger.error(`[sendAlertNotifications] Erreur récupération rôle ${roleId}:`, error); } } // 3. Ajouter la workforce de l'événement if (eventId) { const eventDoc = await db.collection('events').doc(eventId).get(); if (eventDoc.exists) { const event = eventDoc.data(); const workforce = event.workforce || []; workforce.forEach((member) => { // Extraire l'userId selon différentes structures possibles let userId = null; if (typeof member === 'string') { userId = member; } else if (member.userId) { userId = member.userId; } else if (member.id) { userId = member.id; } else if (member.user) { if (typeof member.user === 'string') { userId = member.user; } else if (member.user.id) { userId = member.user.id; } } if (userId) { targetUserIds.add(userId); } }); } } const userIds = Array.from(targetUserIds); // 4. Mettre à jour l'alerte avec la liste des utilisateurs await db.collection('alerts').doc(alert.id).update({ assignedTo: userIds, }); // 5. Envoyer les emails si alerte critique if (alert.severity === 'CRITICAL') { await sendAlertEmails(alert, userIds); } return userIds; } catch (error) { logger.error('[sendAlertNotifications] Erreur:', error); throw error; } } /** * Envoie les emails d'alerte */ async function sendAlertEmails(alert, userIds) { try { const {renderTemplate, getEmailSubject, prepareTemplateData} = require('./utils/emailTemplates'); const db = admin.firestore(); // Vérifier que EMAIL_CONFIG est disponible if (!EMAIL_CONFIG || !EMAIL_CONFIG.from) { logger.error('[sendAlertEmails] EMAIL_CONFIG non configuré'); return 0; } const transporter = nodemailer.createTransport(getSmtpConfig()); let successCount = 0; // Envoyer les emails par lots de 5 const batches = []; for (let i = 0; i < userIds.length; i += 5) { batches.push(userIds.slice(i, i + 5)); } for (const batch of batches) { const promises = batch.map(async (userId) => { try { // Récupérer l'utilisateur const userDoc = await db.collection('users').doc(userId).get(); if (!userDoc.exists) { return false; } const user = userDoc.data(); // Vérifier les préférences email const prefs = user.notificationPreferences || {}; if (!prefs.emailEnabled) { return false; } if (!user.email) { return false; } // Préparer et envoyer l'email let html; try { const templateData = await prepareTemplateData(alert, user); html = await renderTemplate('alert-individual', templateData); } catch (templateError) { logger.error(`[sendAlertEmails] Erreur template pour ${userId}:`, templateError); html = `

${alert.title || 'Nouvelle alerte'}

${alert.message}

Voir l'alerte `; } await transporter.sendMail({ from: `"${EMAIL_CONFIG.from.name}" <${EMAIL_CONFIG.from.address}>`, to: user.email, replyTo: EMAIL_CONFIG.replyTo, subject: getEmailSubject(alert), html: html, text: alert.message, }); return true; } catch (error) { logger.error(`[sendAlertEmails] Erreur email ${userId}:`, error); return false; } }); const results = await Promise.all(promises); successCount += results.filter((r) => r).length; } // Mettre à jour l'alerte await db.collection('alerts').doc(alert.id).update({ emailSent: true, emailSentAt: admin.firestore.FieldValue.serverTimestamp(), emailsSentCount: successCount, }); return successCount; } catch (error) { logger.error('[sendAlertEmails] Erreur globale:', error); return 0; } } /** * Formate la date d'un événement */ function formatEventDate(event) { if (event.startDate) { const date = event.startDate.toDate ? event.startDate.toDate() : new Date(event.startDate); return date.toLocaleDateString('fr-FR', {day: 'numeric', month: 'numeric', year: 'numeric'}); } return 'Date inconnue'; }