feat: implement comprehensive Firebase Functions backend for equipment management and migrate core repository services

This commit is contained in:
ElPoyo
2026-05-26 15:35:48 +02:00
parent 323df01afe
commit ea1e1335e3
37 changed files with 6315 additions and 6140 deletions
+60 -60
View File
@@ -1,51 +1,51 @@
const {onCall} = require('firebase-functions/v2/https');
const admin = require('firebase-admin');
const nodemailer = require('nodemailer');
const handlebars = require('handlebars');
const fs = require('fs').promises;
const path = require('path');
const {getSmtpConfig, EMAIL_CONFIG} = require('./utils/emailConfig');
const {onCall} = require("firebase-functions/v2/https");
const admin = require("firebase-admin");
const nodemailer = require("nodemailer");
const handlebars = require("handlebars");
const fs = require("fs").promises;
const path = require("path");
const {getSmtpConfig, EMAIL_CONFIG} = require("./utils/emailConfig");
/**
* Envoie un email d'alerte à un utilisateur
* Appelé par le client Dart via callable function
*/
exports.sendAlertEmail = onCall({
region: 'europe-west9',
cors: true
region: "europe-west9",
cors: true,
}, async (request) => {
// Vérifier l'authentification
if (!request.auth) {
throw new Error('L\'utilisateur doit être authentifié');
throw new Error("L'utilisateur doit être authentifié");
}
const {alertId, userId, templateType} = request.data;
if (!alertId || !userId) {
throw new Error('alertId et userId sont requis');
throw new Error("alertId et userId sont requis");
}
try {
// Récupérer l'alerte depuis Firestore
const alertDoc = await admin.firestore()
.collection('alerts')
.collection("alerts")
.doc(alertId)
.get();
if (!alertDoc.exists) {
throw new Error('Alerte introuvable');
throw new Error("Alerte introuvable");
}
const alert = alertDoc.data();
// Récupérer l'utilisateur
const userDoc = await admin.firestore()
.collection('users')
.collection("users")
.doc(userId)
.get();
if (!userDoc.exists) {
throw new Error('Utilisateur introuvable');
throw new Error("Utilisateur introuvable");
}
const user = userDoc.data();
@@ -54,7 +54,7 @@ exports.sendAlertEmail = onCall({
const prefs = user.notificationPreferences || {};
if (!prefs.emailEnabled) {
console.log(`Email désactivé pour l'utilisateur ${userId}`);
return {success: true, skipped: true, reason: 'email_disabled'};
return {success: true, skipped: true, reason: "email_disabled"};
}
// Vérifier la préférence pour ce type d'alerte
@@ -62,7 +62,7 @@ exports.sendAlertEmail = onCall({
const shouldSend = checkAlertPreference(alertType, prefs);
if (!shouldSend) {
console.log(`Type d'alerte ${alertType} désactivé pour ${userId}`);
return {success: true, skipped: true, reason: 'alert_type_disabled'};
return {success: true, skipped: true, reason: "alert_type_disabled"};
}
// Préparer les données pour le template
@@ -70,7 +70,7 @@ exports.sendAlertEmail = onCall({
// Rendre le template HTML
const html = await renderTemplate(
templateType || 'alert-individual',
templateType || "alert-individual",
templateData,
);
@@ -88,7 +88,7 @@ exports.sendAlertEmail = onCall({
text: alert.message,
});
console.log('Email envoyé:', info.messageId);
console.log("Email envoyé:", info.messageId);
// Marquer l'email comme envoyé dans l'alerte
await alertDoc.ref.update({
@@ -102,7 +102,7 @@ exports.sendAlertEmail = onCall({
skipped: false,
};
} catch (error) {
console.error('Erreur envoi email:', error);
console.error("Erreur envoi email:", error);
throw new Error(`Erreur lors de l'envoi de l'email: ${error.message}`);
}
});
@@ -112,13 +112,13 @@ exports.sendAlertEmail = onCall({
*/
function checkAlertPreference(alertType, preferences) {
const typeMapping = {
'EVENT_CREATED': 'eventsNotifications',
'EVENT_MODIFIED': 'eventsNotifications',
'EVENT_CANCELLED': 'eventsNotifications',
'LOST': 'equipmentNotifications',
'EQUIPMENT_MISSING': 'equipmentNotifications',
'MAINTENANCE_REMINDER': 'maintenanceNotifications',
'STOCK_LOW': 'stockNotifications',
"EVENT_CREATED": "eventsNotifications",
"EVENT_MODIFIED": "eventsNotifications",
"EVENT_CANCELLED": "eventsNotifications",
"LOST": "equipmentNotifications",
"EQUIPMENT_MISSING": "equipmentNotifications",
"MAINTENANCE_REMINDER": "maintenanceNotifications",
"STOCK_LOW": "stockNotifications",
};
const prefKey = typeMapping[alertType];
@@ -130,12 +130,12 @@ function checkAlertPreference(alertType, preferences) {
*/
async function prepareTemplateData(alert, user) {
const data = {
userName: `${user.firstName || ''} ${user.lastName || ''}`.trim() ||
'Utilisateur',
userName: `${user.firstName || ""} ${user.lastName || ""}`.trim() ||
"Utilisateur",
alertTitle: getAlertTitle(alert.type),
alertMessage: alert.message,
isCritical: alert.severity === 'CRITICAL',
actionUrl: `${EMAIL_CONFIG.appUrl}${alert.actionUrl || '/alerts'}`,
isCritical: alert.severity === "CRITICAL",
actionUrl: `${EMAIL_CONFIG.appUrl}${alert.actionUrl || "/alerts"}`,
appUrl: EMAIL_CONFIG.appUrl,
unsubscribeUrl: `${EMAIL_CONFIG.appUrl}/my_account?tab=notifications`,
year: new Date().getFullYear(),
@@ -146,7 +146,7 @@ async function prepareTemplateData(alert, user) {
if (alert.eventId) {
try {
const eventDoc = await admin.firestore()
.collection('events')
.collection("events")
.doc(alert.eventId)
.get();
@@ -155,22 +155,22 @@ async function prepareTemplateData(alert, user) {
data.eventName = event.Name;
if (event.StartDateTime) {
const date = event.StartDateTime.toDate();
data.eventDate = date.toLocaleDateString('fr-FR', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
data.eventDate = date.toLocaleDateString("fr-FR", {
day: "2-digit",
month: "2-digit",
year: "numeric",
});
}
}
} catch (error) {
console.error('Erreur récupération événement:', error);
console.error("Erreur récupération événement:", error);
}
}
if (alert.equipmentId) {
try {
const eqDoc = await admin.firestore()
.collection('equipments')
.collection("equipments")
.doc(alert.equipmentId)
.get();
@@ -178,7 +178,7 @@ async function prepareTemplateData(alert, user) {
data.equipmentName = eqDoc.data().name;
}
} catch (error) {
console.error('Erreur récupération équipement:', error);
console.error("Erreur récupération équipement:", error);
}
}
@@ -190,16 +190,16 @@ async function prepareTemplateData(alert, user) {
*/
function getEmailSubject(alert) {
const subjects = {
'EVENT_CREATED': '📅 Nouvel événement créé',
'EVENT_MODIFIED': '📝 Événement modifié',
'EVENT_CANCELLED': '❌ Événement annulé',
'LOST': '🔴 Alerte critique : Équipement perdu',
'EQUIPMENT_MISSING': '⚠️ Équipement manquant',
'MAINTENANCE_REMINDER': '🔧 Rappel de maintenance',
'STOCK_LOW': '📦 Stock faible',
"EVENT_CREATED": "📅 Nouvel événement créé",
"EVENT_MODIFIED": "📝 Événement modifié",
"EVENT_CANCELLED": "❌ Événement annulé",
"LOST": "🔴 Alerte critique : Équipement perdu",
"EQUIPMENT_MISSING": "⚠️ Équipement manquant",
"MAINTENANCE_REMINDER": "🔧 Rappel de maintenance",
"STOCK_LOW": "📦 Stock faible",
};
return subjects[alert.type] || '🔔 Nouvelle alerte - EM2 Events';
return subjects[alert.type] || "🔔 Nouvelle alerte - EM2 Events";
}
/**
@@ -207,16 +207,16 @@ function getEmailSubject(alert) {
*/
function getAlertTitle(type) {
const titles = {
'EVENT_CREATED': 'Nouvel événement créé',
'EVENT_MODIFIED': 'Événement modifié',
'EVENT_CANCELLED': 'Événement annulé',
'LOST': 'Équipement perdu',
'EQUIPMENT_MISSING': 'Équipement manquant',
'MAINTENANCE_REMINDER': 'Maintenance requise',
'STOCK_LOW': 'Stock faible',
"EVENT_CREATED": "Nouvel événement créé",
"EVENT_MODIFIED": "Événement modifié",
"EVENT_CANCELLED": "Événement annulé",
"LOST": "Équipement perdu",
"EQUIPMENT_MISSING": "Équipement manquant",
"MAINTENANCE_REMINDER": "Maintenance requise",
"STOCK_LOW": "Stock faible",
};
return titles[type] || 'Nouvelle alerte';
return titles[type] || "Nouvelle alerte";
}
/**
@@ -225,16 +225,16 @@ function getAlertTitle(type) {
async function renderTemplate(templateName, data) {
try {
// Lire le template de base
const basePath = path.join(__dirname, 'templates', 'base-template.html');
const baseTemplate = await fs.readFile(basePath, 'utf8');
const basePath = path.join(__dirname, "templates", "base-template.html");
const baseTemplate = await fs.readFile(basePath, "utf8");
// Lire le template de contenu
const contentPath = path.join(
__dirname,
'templates',
"templates",
`${templateName}.html`,
);
const contentTemplate = await fs.readFile(contentPath, 'utf8');
const contentTemplate = await fs.readFile(contentPath, "utf8");
// Compiler les templates
const compileContent = handlebars.compile(contentTemplate);
@@ -249,7 +249,7 @@ async function renderTemplate(templateName, data) {
content: renderedContent,
});
} catch (error) {
console.error('Erreur rendu template:', error);
console.error("Erreur rendu template:", error);
// Fallback vers un template simple
return `
<html>