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
+94 -94
View File
@@ -1,8 +1,8 @@
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');
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
@@ -10,14 +10,14 @@ const {getSmtpConfig, EMAIL_CONFIG} = require('./utils/emailConfig');
*/
exports.processEquipmentValidation = onCall({
cors: true,
region: 'europe-west9'
region: "europe-west9",
}, async (request) => {
try {
// L'authentification est automatique avec onCall
const {auth, data} = request;
if (!auth) {
throw new Error('L\'utilisateur doit être authentifié');
throw new Error("L'utilisateur doit être authentifié");
}
const {
@@ -28,22 +28,22 @@ exports.processEquipmentValidation = onCall({
// Validation
if (!eventId || !equipmentList || !validationType) {
throw new Error('eventId, equipmentList et validationType sont requis');
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 eventRef = db.collection("events").doc(eventId);
const eventDoc = await eventRef.get();
if (!eventDoc.exists) {
throw new Error('Événement introuvable');
throw new Error("Événement introuvable");
}
const event = eventDoc.data();
const eventName = event.Name || event.name || 'Événement inconnu';
const eventName = event.Name || event.name || "Événement inconnu";
const eventDate = formatEventDate(event);
// 2. Analyser les équipements et détecter les problèmes
@@ -51,16 +51,16 @@ exports.processEquipmentValidation = onCall({
const {equipmentId, status, quantity, expectedQuantity} = equipment;
// Équipement non emporté: pas d'alerte de perte/manquant au retour.
if (status === 'NOT_TAKEN') {
if (status === "NOT_TAKEN") {
continue;
}
// Cas 1: Équipement PERDU
if (status === 'LOST') {
if (status === "LOST") {
const alertData = await createAlertInFirestore({
type: 'LOST',
severity: 'CRITICAL',
title: 'Équipement perdu',
type: "LOST",
severity: "CRITICAL",
title: "Équipement perdu",
message: `Équipement "${equipment.name || equipmentId}" perdu lors de l'événement "${eventName}" (${eventDate})`,
equipmentId,
eventId,
@@ -76,11 +76,11 @@ exports.processEquipmentValidation = onCall({
}
// Cas 2: Équipement MANQUANT
if (status === 'MISSING') {
if (status === "MISSING") {
const alertData = await createAlertInFirestore({
type: 'EQUIPMENT_MISSING',
severity: 'WARNING',
title: 'Équipement manquant',
type: "EQUIPMENT_MISSING",
severity: "WARNING",
title: "Équipement manquant",
message: `Équipement "${equipment.name || equipmentId}" manquant pour l'événement "${eventName}" (${eventDate})`,
equipmentId,
eventId,
@@ -96,13 +96,13 @@ exports.processEquipmentValidation = onCall({
}
// Cas 3: Quantité incorrecte
const hasExpectedQuantity = typeof expectedQuantity === 'number';
const hasActualQuantity = typeof quantity === 'number';
const hasExpectedQuantity = typeof expectedQuantity === "number";
const hasActualQuantity = typeof quantity === "number";
if (hasExpectedQuantity && hasActualQuantity && quantity !== expectedQuantity) {
const alertData = await createAlertInFirestore({
type: 'QUANTITY_MISMATCH',
severity: 'INFO',
title: 'Quantité incorrecte',
type: "QUANTITY_MISMATCH",
severity: "INFO",
title: "Quantité incorrecte",
message: `Quantité incorrecte pour "${equipment.name || equipmentId}": ${quantity} au lieu de ${expectedQuantity} attendus`,
equipmentId,
eventId,
@@ -120,11 +120,11 @@ exports.processEquipmentValidation = onCall({
}
// Cas 4: Équipement endommagé
if (status === 'DAMAGED') {
if (status === "DAMAGED") {
const alertData = await createAlertInFirestore({
type: 'DAMAGED',
severity: 'WARNING',
title: 'Équipement endommagé',
type: "DAMAGED",
severity: "WARNING",
title: "Équipement endommagé",
message: `Équipement "${equipment.name || equipmentId}" endommagé durant l'événement "${eventName}" (${eventDate})`,
equipmentId,
eventId,
@@ -151,7 +151,7 @@ exports.processEquipmentValidation = onCall({
});
// 4. Envoyer les notifications pour les alertes critiques
const criticalAlerts = alerts.filter((a) => a.severity === 'CRITICAL');
const criticalAlerts = alerts.filter((a) => a.severity === "CRITICAL");
if (criticalAlerts.length > 0) {
for (const alert of criticalAlerts) {
try {
@@ -169,7 +169,7 @@ exports.processEquipmentValidation = onCall({
alertIds: alerts.map((a) => a.id),
};
} catch (error) {
logger.error('[processEquipmentValidation] Erreur:', error);
logger.error("[processEquipmentValidation] Erreur:", error);
throw error;
}
});
@@ -179,14 +179,14 @@ exports.processEquipmentValidation = onCall({
*/
async function createAlertInFirestore(alertData) {
const db = admin.firestore();
const alertRef = db.collection('alerts').doc();
const alertRef = db.collection("alerts").doc();
const fullAlertData = {
id: alertRef.id,
...alertData,
createdAt: admin.firestore.FieldValue.serverTimestamp(),
isRead: false,
status: 'ACTIVE',
status: "ACTIVE",
emailSent: false,
assignedTo: [],
};
@@ -206,7 +206,7 @@ async function sendAlertNotifications(alert, eventId) {
try {
// 1. Récupérer TOUS les utilisateurs et leurs permissions
const allUsersSnapshot = await db.collection('users').get();
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();
@@ -219,17 +219,17 @@ async function sendAlertNotifications(alert, eventId) {
}
// Extraire le chemin du rôle
let rolePath = '';
let roleId = '';
let rolePath = "";
let roleId = "";
if (typeof user.role === 'string') {
if (typeof user.role === "string") {
rolePath = user.role;
roleId = user.role.split('/').pop();
roleId = user.role.split("/").pop();
} else if (user.role.path) {
rolePath = user.role.path;
roleId = user.role.path.split('/').pop();
roleId = user.role.path.split("/").pop();
} else if (user.role._path && user.role._path.segments) {
rolePath = user.role._path.segments.join('/');
rolePath = user.role._path.segments.join("/");
roleId = user.role._path.segments[user.role._path.segments.length - 1];
}
@@ -245,14 +245,14 @@ async function sendAlertNotifications(alert, eventId) {
// 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();
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')) {
if (permissions.includes("view_all_events")) {
users.forEach((userId) => {
usersWithPermission.add(userId);
targetUserIds.add(userId);
@@ -266,7 +266,7 @@ async function sendAlertNotifications(alert, eventId) {
// 3. Ajouter la workforce de l'événement
if (eventId) {
const eventDoc = await db.collection('events').doc(eventId).get();
const eventDoc = await db.collection("events").doc(eventId).get();
if (eventDoc.exists) {
const event = eventDoc.data();
@@ -276,14 +276,14 @@ async function sendAlertNotifications(alert, eventId) {
// Extraire l'userId selon différentes structures possibles
let userId = null;
if (typeof member === 'string') {
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') {
if (typeof member.user === "string") {
userId = member.user;
} else if (member.user.id) {
userId = member.user.id;
@@ -300,18 +300,18 @@ async function sendAlertNotifications(alert, eventId) {
const userIds = Array.from(targetUserIds);
// 4. Mettre à jour l'alerte avec la liste des utilisateurs
await db.collection('alerts').doc(alert.id).update({
await db.collection("alerts").doc(alert.id).update({
assignedTo: userIds,
});
// 5. Envoyer les emails si alerte critique
if (alert.severity === 'CRITICAL') {
if (alert.severity === "CRITICAL") {
await sendAlertEmails(alert, userIds);
}
return userIds;
} catch (error) {
logger.error('[sendAlertNotifications] Erreur:', error);
logger.error("[sendAlertNotifications] Erreur:", error);
throw error;
}
}
@@ -321,12 +321,12 @@ async function sendAlertNotifications(alert, eventId) {
*/
async function sendAlertEmails(alert, userIds) {
try {
const {renderTemplate, getEmailSubject, prepareTemplateData} = require('./utils/emailTemplates');
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é');
logger.error("[sendAlertEmails] EMAIL_CONFIG non configuré");
return 0;
}
@@ -343,7 +343,7 @@ async function sendAlertEmails(alert, userIds) {
const promises = batch.map(async (userId) => {
try {
// Récupérer l'utilisateur
const userDoc = await db.collection('users').doc(userId).get();
const userDoc = await db.collection("users").doc(userId).get();
if (!userDoc.exists) {
return false;
@@ -351,55 +351,55 @@ async function sendAlertEmails(alert, userIds) {
const user = userDoc.data();
// Vérifier les préférences email
const prefs = user.notificationPreferences || {};
if (!prefs.emailEnabled) {
return false;
}
// Vérifier les préférences email
const prefs = user.notificationPreferences || {};
if (!prefs.emailEnabled) {
return false;
}
if (!user.email) {
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 = `
// 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 = `
<html>
<body>
<h2>${alert.title || 'Nouvelle alerte'}</h2>
<h2>${alert.title || "Nouvelle alerte"}</h2>
<p>${alert.message}</p>
<a href="${EMAIL_CONFIG.appUrl}/alerts">Voir l'alerte</a>
</body>
</html>
`;
}
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;
}
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({
await db.collection("alerts").doc(alert.id).update({
emailSent: true,
emailSentAt: admin.firestore.FieldValue.serverTimestamp(),
emailsSentCount: successCount,
@@ -407,7 +407,7 @@ async function sendAlertEmails(alert, userIds) {
return successCount;
} catch (error) {
logger.error('[sendAlertEmails] Erreur globale:', error);
logger.error("[sendAlertEmails] Erreur globale:", error);
return 0;
}
}
@@ -425,10 +425,10 @@ function formatEventDate(event) {
const parsedDate = parseFirestoreDate(rawDate);
const safeDate = parsedDate || new Date();
return safeDate.toLocaleDateString('fr-FR', {
day: 'numeric',
month: 'numeric',
year: 'numeric',
return safeDate.toLocaleDateString("fr-FR", {
day: "numeric",
month: "numeric",
year: "numeric",
});
}
@@ -437,7 +437,7 @@ function parseFirestoreDate(value) {
return null;
}
if (typeof value.toDate === 'function') {
if (typeof value.toDate === "function") {
return value.toDate();
}
@@ -445,16 +445,16 @@ function parseFirestoreDate(value) {
return value;
}
if (typeof value === 'string' || typeof value === 'number') {
if (typeof value === "string" || typeof value === "number") {
const date = new Date(value);
return Number.isNaN(date.getTime()) ? null : date;
}
if (typeof value === 'object' && typeof value.seconds === 'number') {
if (typeof value === "object" && typeof value.seconds === "number") {
return new Date(value.seconds * 1000);
}
if (typeof value === 'object' && typeof value._seconds === 'number') {
if (typeof value === "object" && typeof value._seconds === "number") {
return new Date(value._seconds * 1000);
}