Cette mise à jour majeure vise à améliorer significativement les performances de l'application, en particulier au démarrage, et à standardiser l'infrastructure backend. Les principaux changements incluent la migration de toutes les Cloud Functions vers une région européenne (`europe-west9`), l'optimisation du chargement des données, et l'introduction d'un moniteur de performance pour le débogage.
**Changements Backend (Cloud Functions) :**
- **Migration de la Région :**
- Toutes les Cloud Functions ont été déplacées de `us-central1` à `europe-west9` (Paris) pour réduire la latence pour les utilisateurs européens. Cela concerne les appels depuis le frontend (ex: `api_config.dart`, `email_service.dart`) et les définitions des fonctions elles-mêmes (`index.js`, etc.).
- **Standardisation des Fonctions :**
- La plupart des fonctions `onCall` (v1) ont été migrées vers le format `onRequest` (v2) avec une gestion d'authentification et de CORS unifiée, améliorant la robustesse et la cohérence.
- Les triggers Firestore (`onDocumentCreated`, `onDocumentUpdated`) et les tâches planifiées (`onSchedule`) ont été mis à jour pour spécifier explicitement la région `europe-west9`.
- **Mise à jour des Index Firestore :**
- Les index `firestore.indexes.json` ont été mis à jour pour supporter les nouvelles requêtes de l'application et optimiser les performances de filtrage.
**Améliorations des Performances Frontend :**
- **Chargement Asynchrone et Mis en Cache :**
- Le chargement des données utilisateur (`LocalUserProvider`) et des événements (`EventProvider`) a été optimisé pour utiliser un cache local à court terme (5 minutes pour l'utilisateur, 30 secondes pour les événements).
- Les données ne sont rechargées que si le cache a expiré ou si un rechargement est forcé, évitant des appels réseau redondants et accélérant la navigation.
- **Démarrage de l'Application Optimisé :**
- Le processus de connexion automatique (`main.dart`) a été revu. L'application navigue désormais immédiatement vers la page demandée sans attendre la fin du chargement des données utilisateur, qui s'effectue en arrière-plan.
- Un écran de chargement plus esthétique avec le logo de l'entreprise a été ajouté, remplaçant l'indicateur de chargement simple.
- **Chargement de la Page Calendrier :**
- Le chargement et la sélection de l'événement par défaut sur la page `CalendarPage` sont maintenant entièrement asynchrones, rendant l'affichage de la page quasi instantané.
**Nouveaux Outils et Améliorations UX :**
- **Moniteur de Performance :**
- Ajout d'un nouvel outil `PerformanceMonitor` (`lib/utils/performance_monitor.dart`) pour mesurer précisément le temps d'exécution des opérations critiques (appels API, parsing, etc.) en mode débogage. Il aide à identifier les goulots d'étranglement.
- **Amélioration du Formulaire de Connexion :**
- Les champs "Email" et "Mot de passe" sur la page de connexion (`LoginPage`) supportent désormais l'autocomplétion du navigateur (`AutofillGroup`).
- Appuyer sur "Entrée" dans l'un des champs déclenche désormais la connexion, améliorant l'ergonomie.
**Mise à jour de la version :**
- La version de l'application a été incrémentée à `1.0.9`.
266 lines
7.4 KiB
JavaScript
266 lines
7.4 KiB
JavaScript
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
|
|
}, async (request) => {
|
|
// Vérifier l'authentification
|
|
if (!request.auth) {
|
|
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');
|
|
}
|
|
|
|
try {
|
|
// Récupérer l'alerte depuis Firestore
|
|
const alertDoc = await admin.firestore()
|
|
.collection('alerts')
|
|
.doc(alertId)
|
|
.get();
|
|
|
|
if (!alertDoc.exists) {
|
|
throw new Error('Alerte introuvable');
|
|
}
|
|
|
|
const alert = alertDoc.data();
|
|
|
|
// Récupérer l'utilisateur
|
|
const userDoc = await admin.firestore()
|
|
.collection('users')
|
|
.doc(userId)
|
|
.get();
|
|
|
|
if (!userDoc.exists) {
|
|
throw new Error('Utilisateur introuvable');
|
|
}
|
|
|
|
const user = userDoc.data();
|
|
|
|
// Vérifier les préférences email de l'utilisateur
|
|
const prefs = user.notificationPreferences || {};
|
|
if (!prefs.emailEnabled) {
|
|
console.log(`Email désactivé pour l'utilisateur ${userId}`);
|
|
return {success: true, skipped: true, reason: 'email_disabled'};
|
|
}
|
|
|
|
// Vérifier la préférence pour ce type d'alerte
|
|
const alertType = alert.type;
|
|
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'};
|
|
}
|
|
|
|
// Préparer les données pour le template
|
|
const templateData = await prepareTemplateData(alert, user);
|
|
|
|
// Rendre le template HTML
|
|
const html = await renderTemplate(
|
|
templateType || 'alert-individual',
|
|
templateData,
|
|
);
|
|
|
|
// Configurer le transporteur SMTP
|
|
const transporter = nodemailer.createTransporter(getSmtpConfig());
|
|
|
|
// Envoyer l'email
|
|
const info = await transporter.sendMail({
|
|
from: `"${EMAIL_CONFIG.from.name}" <${EMAIL_CONFIG.from.address}>`,
|
|
to: user.email,
|
|
replyTo: EMAIL_CONFIG.replyTo,
|
|
subject: getEmailSubject(alert),
|
|
html: html,
|
|
// Fallback texte brut
|
|
text: alert.message,
|
|
});
|
|
|
|
console.log('Email envoyé:', info.messageId);
|
|
|
|
// Marquer l'email comme envoyé dans l'alerte
|
|
await alertDoc.ref.update({
|
|
emailSent: true,
|
|
emailSentAt: admin.firestore.FieldValue.serverTimestamp(),
|
|
});
|
|
|
|
return {
|
|
success: true,
|
|
messageId: info.messageId,
|
|
skipped: false,
|
|
};
|
|
} catch (error) {
|
|
console.error('Erreur envoi email:', error);
|
|
throw new Error(`Erreur lors de l'envoi de l'email: ${error.message}`);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Vérifie si l'utilisateur souhaite recevoir ce type d'alerte
|
|
*/
|
|
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',
|
|
};
|
|
|
|
const prefKey = typeMapping[alertType];
|
|
return prefKey ? (preferences[prefKey] !== false) : true;
|
|
}
|
|
|
|
/**
|
|
* Prépare les données pour le template
|
|
*/
|
|
async function prepareTemplateData(alert, user) {
|
|
const data = {
|
|
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'}`,
|
|
appUrl: EMAIL_CONFIG.appUrl,
|
|
unsubscribeUrl: `${EMAIL_CONFIG.appUrl}/my_account?tab=notifications`,
|
|
year: new Date().getFullYear(),
|
|
subject: getEmailSubject(alert),
|
|
};
|
|
|
|
// Ajouter des détails selon le type d'alerte
|
|
if (alert.eventId) {
|
|
try {
|
|
const eventDoc = await admin.firestore()
|
|
.collection('events')
|
|
.doc(alert.eventId)
|
|
.get();
|
|
|
|
if (eventDoc.exists) {
|
|
const event = eventDoc.data();
|
|
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',
|
|
});
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Erreur récupération événement:', error);
|
|
}
|
|
}
|
|
|
|
if (alert.equipmentId) {
|
|
try {
|
|
const eqDoc = await admin.firestore()
|
|
.collection('equipments')
|
|
.doc(alert.equipmentId)
|
|
.get();
|
|
|
|
if (eqDoc.exists) {
|
|
data.equipmentName = eqDoc.data().name;
|
|
}
|
|
} catch (error) {
|
|
console.error('Erreur récupération équipement:', error);
|
|
}
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
/**
|
|
* Génère le titre de l'email selon le type d'alerte
|
|
*/
|
|
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',
|
|
};
|
|
|
|
return subjects[alert.type] || '🔔 Nouvelle alerte - EM2 Events';
|
|
}
|
|
|
|
/**
|
|
* Génère le titre pour le corps de l'email
|
|
*/
|
|
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',
|
|
};
|
|
|
|
return titles[type] || 'Nouvelle alerte';
|
|
}
|
|
|
|
/**
|
|
* Rend un template HTML avec Handlebars
|
|
*/
|
|
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');
|
|
|
|
// Lire le template de contenu
|
|
const contentPath = path.join(
|
|
__dirname,
|
|
'templates',
|
|
`${templateName}.html`,
|
|
);
|
|
const contentTemplate = await fs.readFile(contentPath, 'utf8');
|
|
|
|
// Compiler les templates
|
|
const compileContent = handlebars.compile(contentTemplate);
|
|
const compileBase = handlebars.compile(baseTemplate);
|
|
|
|
// Rendre le contenu
|
|
const renderedContent = compileContent(data);
|
|
|
|
// Rendre le template de base avec le contenu
|
|
return compileBase({
|
|
...data,
|
|
content: renderedContent,
|
|
});
|
|
} catch (error) {
|
|
console.error('Erreur rendu template:', error);
|
|
// Fallback vers un template simple
|
|
return `
|
|
<html>
|
|
<body>
|
|
<h2>${data.alertTitle}</h2>
|
|
<p>${data.alertMessage}</p>
|
|
<a href="${data.actionUrl}">Voir l'alerte</a>
|
|
</body>
|
|
</html>
|
|
`;
|
|
}
|
|
}
|
|
|