268 lines
8.6 KiB
JavaScript
268 lines
8.6 KiB
JavaScript
/**
|
||
* Fonction schedulée : Envoie quotidienne d'un résumé des alertes non lues
|
||
* S'exécute tous les jours à 8h00 (Europe/Paris)
|
||
*/
|
||
|
||
const admin = require("firebase-admin");
|
||
const logger = require("firebase-functions/logger");
|
||
const nodemailer = require("nodemailer");
|
||
const {getSmtpConfig} = require("./utils/emailConfig");
|
||
|
||
/**
|
||
* Fonction principale : envoie le digest quotidien
|
||
*/
|
||
async function sendDailyDigest() {
|
||
const db = admin.firestore();
|
||
|
||
logger.info("[sendDailyDigest] ===== DÉBUT ENVOI DIGEST QUOTIDIEN =====");
|
||
|
||
try {
|
||
// 1. Récupérer tous les utilisateurs avec email activé
|
||
const usersSnapshot = await db.collection("users").get();
|
||
const eligibleUsers = [];
|
||
|
||
usersSnapshot.forEach((doc) => {
|
||
const user = doc.data();
|
||
const prefs = user.notificationPreferences || {};
|
||
|
||
// Vérifier si l'utilisateur a activé les emails
|
||
if (prefs.emailEnabled !== false && user.email) {
|
||
eligibleUsers.push({
|
||
uid: doc.id,
|
||
email: user.email,
|
||
firstName: user.firstName || "Utilisateur",
|
||
lastName: user.lastName || "",
|
||
});
|
||
}
|
||
});
|
||
|
||
logger.info(`[sendDailyDigest] ${eligibleUsers.length} utilisateurs éligibles`);
|
||
|
||
// 2. Pour chaque utilisateur, récupérer ses alertes non lues des dernières 24h
|
||
const now = admin.firestore.Timestamp.now();
|
||
const yesterday = admin.firestore.Timestamp.fromMillis(now.toMillis() - 24 * 60 * 60 * 1000);
|
||
|
||
const transporter = nodemailer.createTransport(getSmtpConfig());
|
||
let emailsSent = 0;
|
||
|
||
for (const user of eligibleUsers) {
|
||
try {
|
||
// Récupérer les alertes non lues de l'utilisateur créées dans les dernières 24h
|
||
const alertsSnapshot = await db.collection("alerts")
|
||
.where("assignedTo", "array-contains", user.uid)
|
||
.where("isRead", "==", false)
|
||
.where("createdAt", ">=", yesterday)
|
||
.orderBy("createdAt", "desc")
|
||
.get();
|
||
|
||
if (alertsSnapshot.empty) {
|
||
continue; // Pas d'alertes non lues pour cet utilisateur
|
||
}
|
||
|
||
const alerts = [];
|
||
alertsSnapshot.forEach((doc) => {
|
||
alerts.push({id: doc.id, ...doc.data()});
|
||
});
|
||
|
||
logger.info(`[sendDailyDigest] ${user.email}: ${alerts.length} alertes non lues`);
|
||
|
||
// 3. Envoyer l'email de digest
|
||
const sent = await sendDigestEmail(transporter, user, alerts);
|
||
if (sent) {
|
||
emailsSent++;
|
||
}
|
||
} catch (error) {
|
||
logger.error(`[sendDailyDigest] Erreur pour ${user.email}:`, error);
|
||
}
|
||
}
|
||
|
||
logger.info(`[sendDailyDigest] ✓ ${emailsSent}/${eligibleUsers.length} emails envoyés`);
|
||
logger.info("[sendDailyDigest] ===== FIN DIGEST QUOTIDIEN =====");
|
||
|
||
return {success: true, emailsSent};
|
||
} catch (error) {
|
||
logger.error("[sendDailyDigest] Erreur globale:", error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Envoie l'email de digest à un utilisateur
|
||
*/
|
||
async function sendDigestEmail(transporter, user, alerts) {
|
||
try {
|
||
// Grouper les alertes par sévérité
|
||
const criticalAlerts = alerts.filter((a) => a.severity === "CRITICAL");
|
||
const warningAlerts = alerts.filter((a) => a.severity === "WARNING");
|
||
const infoAlerts = alerts.filter((a) => a.severity === "INFO");
|
||
|
||
// Construire le HTML
|
||
const html = buildDigestHtml(user, {
|
||
critical: criticalAlerts,
|
||
warning: warningAlerts,
|
||
info: infoAlerts,
|
||
});
|
||
|
||
// Envoyer l'email
|
||
await transporter.sendMail({
|
||
from: `"EM2RP Notifications" <${process.env.SMTP_USER}>`,
|
||
to: user.email,
|
||
subject: `📬 ${alerts.length} nouvelle(s) alerte(s) EM2RP`,
|
||
html,
|
||
});
|
||
|
||
logger.info(`[sendDigestEmail] ✓ Email envoyé à ${user.email}`);
|
||
return true;
|
||
} catch (error) {
|
||
logger.error(`[sendDigestEmail] Erreur pour ${user.email}:`, error);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Construit le HTML du digest
|
||
*/
|
||
function buildDigestHtml(user, alertsByType) {
|
||
const totalAlerts = alertsByType.critical.length + alertsByType.warning.length + alertsByType.info.length;
|
||
|
||
let alertsHtml = "";
|
||
|
||
// Alertes critiques
|
||
if (alertsByType.critical.length > 0) {
|
||
alertsHtml += `
|
||
<div style="margin-bottom: 24px;">
|
||
<h3 style="color: #dc2626; margin: 0 0 12px 0;">
|
||
🔴 Alertes critiques (${alertsByType.critical.length})
|
||
</h3>
|
||
${alertsByType.critical.map((alert) => formatAlertItem(alert)).join("")}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// Alertes warning
|
||
if (alertsByType.warning.length > 0) {
|
||
alertsHtml += `
|
||
<div style="margin-bottom: 24px;">
|
||
<h3 style="color: #f59e0b; margin: 0 0 12px 0;">
|
||
⚠️ Avertissements (${alertsByType.warning.length})
|
||
</h3>
|
||
${alertsByType.warning.map((alert) => formatAlertItem(alert)).join("")}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// Alertes info
|
||
if (alertsByType.info.length > 0) {
|
||
alertsHtml += `
|
||
<div style="margin-bottom: 24px;">
|
||
<h3 style="color: #3b82f6; margin: 0 0 12px 0;">
|
||
ℹ️ Informations (${alertsByType.info.length})
|
||
</h3>
|
||
${alertsByType.info.map((alert) => formatAlertItem(alert)).join("")}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
return `
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
</head>
|
||
<body style="margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;">
|
||
<div style="max-width: 600px; margin: 0 auto; padding: 20px; background-color: #f9fafb;">
|
||
<!-- En-tête -->
|
||
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 32px; border-radius: 12px 12px 0 0; text-align: center;">
|
||
<h1 style="color: white; margin: 0; font-size: 28px;">📬 Résumé quotidien</h1>
|
||
<p style="color: rgba(255,255,255,0.9); margin: 8px 0 0 0; font-size: 16px;">
|
||
Bonjour ${user.firstName},
|
||
</p>
|
||
</div>
|
||
|
||
<!-- Contenu -->
|
||
<div style="background-color: white; padding: 32px; border-radius: 0 0 12px 12px; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">
|
||
<p style="color: #374151; font-size: 16px; line-height: 1.6; margin: 0 0 24px 0;">
|
||
Vous avez <strong>${totalAlerts} nouvelle(s) alerte(s)</strong> dans les dernières 24 heures.
|
||
</p>
|
||
|
||
${alertsHtml}
|
||
|
||
<div style="margin-top: 32px; padding-top: 24px; border-top: 1px solid #e5e7eb; text-align: center;">
|
||
<a href="https://app.em2event.fr/#/alerts"
|
||
style="display: inline-block; background-color: #667eea; color: white; padding: 12px 32px; text-decoration: none; border-radius: 8px; font-weight: 600;">
|
||
Voir toutes les alertes
|
||
</a>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Pied de page -->
|
||
<div style="text-align: center; padding: 24px; color: #6b7280; font-size: 14px;">
|
||
<p style="margin: 0 0 8px 0;">EM2RP - Gestion d'événements</p>
|
||
<p style="margin: 0;">
|
||
<a href="https://app.em2event.fr/#/settings" style="color: #667eea; text-decoration: none;">
|
||
Gérer mes préférences de notification
|
||
</a>
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</body>
|
||
</html>
|
||
`;
|
||
}
|
||
|
||
/**
|
||
* Formate un item d'alerte pour l'email
|
||
*/
|
||
function formatAlertItem(alert) {
|
||
const date = alert.createdAt?.toDate ?
|
||
new Date(alert.createdAt.toDate()).toLocaleString("fr-FR", {
|
||
day: "2-digit",
|
||
month: "2-digit",
|
||
year: "numeric",
|
||
hour: "2-digit",
|
||
minute: "2-digit",
|
||
}) :
|
||
"Date inconnue";
|
||
|
||
// Type d'alerte en français
|
||
const typeLabels = {
|
||
"EQUIPMENT_MISSING": "Équipement manquant",
|
||
"LOST": "Équipement perdu",
|
||
"DAMAGED": "Équipement endommagé",
|
||
"QUANTITY_MISMATCH": "Écart de quantité",
|
||
"EVENT_CREATED": "Événement créé",
|
||
"EVENT_MODIFIED": "Événement modifié",
|
||
"WORKFORCE_ADDED": "Ajout à la workforce",
|
||
};
|
||
|
||
const typeLabel = typeLabels[alert.type] || alert.type;
|
||
|
||
return `
|
||
<div style="background-color: #f9fafb; padding: 16px; border-radius: 8px; margin-bottom: 12px; border-left: 4px solid ${getSeverityColor(alert.severity)};">
|
||
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 8px;">
|
||
<strong style="color: #111827; font-size: 15px;">${typeLabel}</strong>
|
||
<span style="color: #6b7280; font-size: 13px;">${date}</span>
|
||
</div>
|
||
<p style="color: #4b5563; margin: 0; font-size: 14px; line-height: 1.5;">
|
||
${alert.message || "Aucun message"}
|
||
</p>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
/**
|
||
* Retourne la couleur selon la sévérité
|
||
*/
|
||
function getSeverityColor(severity) {
|
||
switch (severity) {
|
||
case "CRITICAL": return "#dc2626";
|
||
case "WARNING": return "#f59e0b";
|
||
case "INFO": return "#3b82f6";
|
||
default: return "#6b7280";
|
||
}
|
||
}
|
||
|
||
module.exports = {sendDailyDigest};
|
||
|