Files
formation-manager-itinova/server/emailService.ts
Manus 95606294dc Checkpoint: Modification de l'envoi d'attestation pour joindre le PDF directement en pièce jointe :
- Modification de la fonction sendAttestationEmail dans server/emailService.ts
- Le PDF est maintenant téléchargé depuis l'URL et converti en Buffer
- Le PDF est joint à l'email en pièce jointe (base64) au lieu d'un lien de téléchargement
- Nom du fichier joint : Attestation_Nom_Prenom.pdf
- Texte de l'email adapté : "Vous trouverez votre attestation en pièce jointe de cet email"
- Déployé sur le VPS de production (https://formations.itinova.org)
- Les attestations sont maintenant envoyées directement en pièce jointe
2026-01-18 04:55:11 -05:00

1190 lines
42 KiB
TypeScript

/**
* Service d'envoi d'emails pour les formations
* Utilise le système de notification Manus pour envoyer des emails
*/
import { generateFormationICS } from "./icsGenerator";
import { sendEmail as sendEmailViaService, isEmailServiceConfigured } from "./_core/emailSender";
import { generateEmailFromTemplate } from "./emailTemplateGenerator";
import { EmailVariables } from "./emailTemplateUtils";
interface EmailParams {
to: string;
subject: string;
html: string;
attachments?: Array<{
filename: string;
content: string;
contentType: string;
}>;
}
/**
* Envoie un email via le service d'envoi configuré
* Utilise Resend si RESEND_API_KEY est configuré, sinon simule l'envoi
*/
async function sendEmail(params: EmailParams): Promise<boolean> {
return await sendEmailViaService(params);
}
/**
* Template HTML de base pour les emails (avec template personnalisé)
*/
async function getEmailTemplate(
content: string,
templateType: string = 'inscription',
variables?: EmailVariables
): Promise<string> {
return await generateEmailFromTemplate(templateType, content, variables);
}
/**
* Envoie un email de confirmation d'inscription avec invitations Outlook pour toutes les dates
*/
export async function sendInscriptionConfirmation(params: {
apprenantEmail: string;
apprenantNom: string;
apprenantPrenom: string;
apprenantFonction: string;
formationNom: string;
sequenceNom: string;
dates: Array<{ dateDebut: Date; dateFin: Date; ordre: number }>;
lieu: string;
statut: 'confirmee' | 'liste_attente';
}): Promise<boolean> {
const isConfirmed = params.statut === 'confirmee';
const fonctionLabel = params.apprenantFonction === 'directeur' ? 'Directeur' :
params.apprenantFonction === 'chef_service' ? 'Chef de service' : 'Autre';
// Générer la liste des dates
const datesHTML = params.dates.map(date => `
<div class="date-item">
<strong>Date ${date.ordre} :</strong><br/>
Du ${date.dateDebut.toLocaleDateString('fr-FR', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})}<br/>
au ${date.dateFin.toLocaleDateString('fr-FR', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})}
</div>
`).join('');
const content = `
<h2>Confirmation d'inscription</h2>
<p>Bonjour ${fonctionLabel} ${params.apprenantPrenom} ${params.apprenantNom},</p>
${isConfirmed
? '<p><strong>Votre inscription a été confirmée !</strong></p>'
: '<p><strong>Vous avez été ajouté à la liste d\'attente.</strong></p>'
}
<div class="info-box">
<h3>Détails de la formation</h3>
<p><strong>Formation :</strong> ${params.formationNom}</p>
<p><strong>Séquence :</strong> ${params.sequenceNom}</p>
<p><strong>Lieu :</strong> ${params.lieu}</p>
<h4>Dates de formation (${params.dates.length} séance${params.dates.length > 1 ? 's' : ''}) :</h4>
${datesHTML}
</div>
${isConfirmed
? `<p>Des invitations Outlook sont jointes à cet email pour chaque date de formation. Merci de les ajouter à votre calendrier pour bloquer votre agenda.</p>
<p><strong>Important :</strong> Les inscriptions seront bloquées 15 jours avant le début de la séquence. Après cette date, aucune modification ne sera possible.</p>`
: '<p>Nous vous contacterons dès qu\'une place se libère.</p>'
}
<p>À bientôt pour cette formation !</p>
`;
// Générer une invitation ICS pour chaque date
const attachments = isConfirmed ? params.dates.map((date, index) => ({
filename: `invitation_date_${date.ordre}.ics`,
content: generateFormationICS({
formationNom: params.formationNom,
sessionNom: `${params.sequenceNom} - Date ${date.ordre}`,
dateDebut: date.dateDebut,
dateFin: date.dateFin,
lieu: params.lieu,
apprenantNom: params.apprenantNom,
apprenantPrenom: params.apprenantPrenom,
apprenantEmail: params.apprenantEmail,
}),
contentType: 'text/calendar',
})) : undefined;
// Préparer les variables pour le remplacement (avec alias pour compatibilité)
const variables: EmailVariables = {
nomApprenant: params.apprenantNom,
prenomApprenant: params.apprenantPrenom,
nomFormation: params.formationNom,
nomSequence: params.sequenceNom,
dateDebut: params.dates[0]?.dateDebut.toLocaleDateString('fr-FR') || '',
dateFin: params.dates[params.dates.length - 1]?.dateFin.toLocaleDateString('fr-FR') || '',
lieu: params.lieu,
datesHTML: datesHTML, // HTML formaté de toutes les dates
// Alias pour compatibilité avec les templates utilisant les noms courts
nom: params.apprenantNom,
prenom: params.apprenantPrenom,
formation: params.formationNom,
sequence: params.sequenceNom,
};
return sendEmail({
to: params.apprenantEmail,
subject: isConfirmed
? `Confirmation d'inscription - ${params.formationNom}`
: `Liste d'attente - ${params.formationNom}`,
html: await getEmailTemplate(content, 'inscription', variables),
attachments,
});
}
/**
* Envoie un email teaser pour une séquence
*/
export async function sendTeaserEmail(params: {
apprenantEmail: string;
apprenantPrenom: string;
apprenantNom: string;
apprenantFonction: string;
formationNom: string;
sequenceNom: string;
dates: Array<{ dateDebut: Date; dateFin: Date; ordre: number }>;
lieu?: string;
formateur?: string;
}): Promise<boolean> {
const fonctionLabel = params.apprenantFonction === 'directeur' ? 'Directeur' :
params.apprenantFonction === 'chef_service' ? 'Chef de service' : '';
const salutation = fonctionLabel ? `${fonctionLabel} ${params.apprenantPrenom} ${params.apprenantNom}` : params.apprenantPrenom;
const datesHTML = params.dates.map(date => `
<div class="date-item">
<strong>Date ${date.ordre} :</strong> ${date.dateDebut.toLocaleDateString('fr-FR', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
})}
</div>
`).join('');
const content = `
<h2>Votre formation approche !</h2>
<p>Bonjour ${salutation},</p>
<p>Nous sommes ravis de vous accueillir prochainement pour la formation <strong>${params.formationNom}</strong>.</p>
<div class="info-box">
<p><strong>Séquence :</strong> ${params.sequenceNom}</p>
<h4>Dates :</h4>
${datesHTML}
${params.lieu ? `<p><strong>Lieu :</strong> ${params.lieu}</p>` : ''}
${params.formateur ? `<p><strong>Formateur :</strong> ${params.formateur}</p>` : ''}
</div>
<p>Préparez-vous à vivre une expérience enrichissante qui vous permettra de développer vos compétences managériales.</p>
<p>À très bientôt !</p>
`;
const variables = {
nomApprenant: params.apprenantNom,
prenomApprenant: params.apprenantPrenom,
nomFormation: params.formationNom,
nomSequence: params.sequenceNom,
dateDebut: params.dates[0]?.dateDebut.toLocaleDateString('fr-FR') || '',
dateFin: params.dates[params.dates.length - 1]?.dateFin.toLocaleDateString('fr-FR') || '',
datesHTML: datesHTML, // Ajouter le HTML des dates
lieu: params.lieu || '',
formateur: params.formateur || '',
};
return sendEmail({
to: params.apprenantEmail,
subject: `Votre formation ${params.formationNom} approche !`,
html: await getEmailTemplate(content, 'teaser', variables),
});
}
/**
* Envoie un email de rappel générique avec le template spécifié
*/
export async function sendRappelEmail(params: {
apprenantEmail: string;
apprenantPrenom: string;
apprenantNom: string;
apprenantFonction: string;
formationNom: string;
sequenceNom: string;
dates: Array<{ dateDebut: Date; dateFin: Date; ordre: number }>;
lieu: string;
formateur?: string;
templateType: string;
attachmentUrl?: string;
attachmentFilename?: string;
attachmentMimeType?: string;
}): Promise<boolean> {
const fonctionLabel = params.apprenantFonction === 'directeur' ? 'Directeur' :
params.apprenantFonction === 'chef_service' ? 'Chef de service' : '';
const salutation = fonctionLabel ? `${fonctionLabel} ${params.apprenantPrenom} ${params.apprenantNom}` : params.apprenantPrenom;
const datesHTML = params.dates.map(date => `
<div class="date-item">
<strong>Date ${date.ordre} :</strong><br/>
${date.dateDebut.toLocaleDateString('fr-FR', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})}
</div>
`).join('');
const content = `
<h2>Rappel : Votre formation</h2>
<p>Bonjour ${salutation},</p>
<p>Nous vous rappelons votre formation <strong>${params.formationNom}</strong>.</p>
<div class="info-box">
<h3>Informations pratiques</h3>
<p><strong>Séquence :</strong> ${params.sequenceNom}</p>
<p><strong>Lieu :</strong> ${params.lieu}</p>
${params.formateur ? `<p><strong>Formateur :</strong> ${params.formateur}</p>` : ''}
<h4>Dates :</h4>
${datesHTML}
</div>
<p><strong>Merci de vous présenter à l'heure indiquée.</strong></p>
<p>Si vous avez des questions, n'hésitez pas à contacter le service RH.</p>
<p>À très bientôt !</p>
`;
const variables = {
nomApprenant: params.apprenantNom,
prenomApprenant: params.apprenantPrenom,
nomFormation: params.formationNom,
nomSequence: params.sequenceNom,
dateDebut: params.dates[0]?.dateDebut.toLocaleDateString('fr-FR') || '',
dateFin: params.dates[params.dates.length - 1]?.dateFin.toLocaleDateString('fr-FR') || '',
datesHTML: datesHTML,
lieu: params.lieu,
formateur: params.formateur || '',
};
// Préparer les pièces jointes si nécessaire
const attachments: Array<{ filename: string; content: string; contentType: string }> = [];
if (params.attachmentUrl && params.attachmentFilename && params.attachmentMimeType) {
try {
let base64: string;
// Vérifier si c'est une URL locale (commence par /uploads/)
if (params.attachmentUrl.startsWith('/uploads/')) {
// Lire le fichier depuis le système de fichiers
const fs = await import('fs/promises');
const path = await import('path');
const filePath = path.join(process.cwd(), params.attachmentUrl);
const fileBuffer = await fs.readFile(filePath);
base64 = fileBuffer.toString('base64');
} else {
// Télécharger le fichier depuis une URL externe (S3)
const response = await fetch(params.attachmentUrl);
const arrayBuffer = await response.arrayBuffer();
base64 = Buffer.from(arrayBuffer).toString('base64');
}
attachments.push({
filename: params.attachmentFilename,
content: base64,
contentType: params.attachmentMimeType,
});
} catch (error) {
console.error('[Email] Erreur lors du chargement de la pièce jointe:', error);
// Continuer l'envoi sans la pièce jointe
}
}
return sendEmail({
to: params.apprenantEmail,
subject: `Rappel : ${params.formationNom}`,
html: await getEmailTemplate(content, params.templateType, variables),
attachments: attachments.length > 0 ? attachments : undefined,
});
}
/**
* Envoie un email de rappel J-7
*/
export async function sendRappelJ7Email(params: {
apprenantEmail: string;
apprenantPrenom: string;
apprenantNom: string;
apprenantFonction: string;
formationNom: string;
sequenceNom: string;
dates: Array<{ dateDebut: Date; dateFin: Date; ordre: number }>;
lieu: string;
formateur?: string;
attachmentUrl?: string;
attachmentFilename?: string;
attachmentMimeType?: string;
}): Promise<boolean> {
const fonctionLabel = params.apprenantFonction === 'directeur' ? 'Directeur' :
params.apprenantFonction === 'chef_service' ? 'Chef de service' : '';
const salutation = fonctionLabel ? `${fonctionLabel} ${params.apprenantPrenom} ${params.apprenantNom}` : params.apprenantPrenom;
const datesHTML = params.dates.map(date => `
<div class="date-item">
<strong>Date ${date.ordre} :</strong><br/>
${date.dateDebut.toLocaleDateString('fr-FR', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})}
</div>
`).join('');
const content = `
<h2>Rappel : Votre formation dans 7 jours</h2>
<p>Bonjour ${salutation},</p>
<p>Nous vous rappelons que votre formation <strong>${params.formationNom}</strong> commence dans 7 jours.</p>
<div class="info-box">
<h3>Informations pratiques</h3>
<p><strong>Séquence :</strong> ${params.sequenceNom}</p>
<p><strong>Lieu :</strong> ${params.lieu}</p>
${params.formateur ? `<p><strong>Formateur :</strong> ${params.formateur}</p>` : ''}
<h4>Dates :</h4>
${datesHTML}
</div>
<p><strong>Merci de vous présenter à l'heure indiquée.</strong></p>
<p>Si vous avez des questions, n'hésitez pas à contacter le service RH.</p>
<p>À très bientôt !</p>
`;
const variables = {
nomApprenant: params.apprenantNom,
prenomApprenant: params.apprenantPrenom,
nomFormation: params.formationNom,
nomSequence: params.sequenceNom,
dateDebut: params.dates[0]?.dateDebut.toLocaleDateString('fr-FR') || '',
dateFin: params.dates[params.dates.length - 1]?.dateFin.toLocaleDateString('fr-FR') || '',
datesHTML: datesHTML, // Ajouter le HTML des dates
lieu: params.lieu,
formateur: params.formateur || '',
};
// Préparer les pièces jointes si nécessaire
const attachments: Array<{ filename: string; content: string; contentType: string }> = [];
if (params.attachmentUrl && params.attachmentFilename && params.attachmentMimeType) {
try {
let base64: string;
// Vérifier si c'est une URL locale (commence par /uploads/)
if (params.attachmentUrl.startsWith('/uploads/')) {
// Lire le fichier depuis le système de fichiers
const fs = await import('fs/promises');
const path = await import('path');
const filePath = path.join(process.cwd(), params.attachmentUrl);
const fileBuffer = await fs.readFile(filePath);
base64 = fileBuffer.toString('base64');
} else {
// Télécharger le fichier depuis une URL externe (S3)
const response = await fetch(params.attachmentUrl);
const arrayBuffer = await response.arrayBuffer();
base64 = Buffer.from(arrayBuffer).toString('base64');
}
attachments.push({
filename: params.attachmentFilename,
content: base64,
contentType: params.attachmentMimeType,
});
} catch (error) {
console.error('[Email] Erreur lors du chargement de la pièce jointe:', error);
// Continuer l'envoi sans la pièce jointe
}
}
return sendEmail({
to: params.apprenantEmail,
subject: `Rappel J-7 : ${params.formationNom}`,
html: await getEmailTemplate(content, 'rappel', variables),
attachments: attachments.length > 0 ? attachments : undefined,
});
}
/**
* Envoie un email de rappel J-1 à un apprenant
*/
export async function sendRappelJ1Email(params: {
apprenantEmail: string;
apprenantNom: string;
apprenantPrenom: string;
apprenantFonction: string;
formationNom: string;
sequenceNom: string;
dates: Array<{ dateDebut: Date; dateFin: Date; ordre: number }>;
lieu?: string;
formateur?: string;
attachmentUrl?: string;
attachmentFilename?: string;
attachmentMimeType?: string;
}): Promise<boolean> {
const fonctionLabel = params.apprenantFonction === 'directeur' ? 'Directeur' :
params.apprenantFonction === 'chef_service' ? 'Chef de service' : '';
const salutation = fonctionLabel ? `${fonctionLabel} ${params.apprenantPrenom} ${params.apprenantNom}` : params.apprenantPrenom;
const datesHTML = params.dates.map(date => `
<div class="date-item">
<strong>Date ${date.ordre} :</strong> ${date.dateDebut.toLocaleDateString('fr-FR', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
})} - ${date.dateFin.toLocaleDateString('fr-FR', {
hour: '2-digit',
minute: '2-digit'
})}
</div>
`).join('');
const content = `
<h2>Rappel : Votre formation commence demain !</h2>
<p>Bonjour ${salutation},</p>
<p>Nous vous rappelons que votre formation <strong>${params.formationNom}</strong> commence <strong>demain</strong>.</p>
<div class="info-box">
<h3>Informations pratiques</h3>
<p><strong>Séquence :</strong> ${params.sequenceNom}</p>
${params.lieu ? `<p><strong>Lieu :</strong> ${params.lieu}</p>` : ''}
${params.formateur ? `<p><strong>Formateur :</strong> ${params.formateur}</p>` : ''}
<h4>Dates :</h4>
${datesHTML}
</div>
<p><strong>Merci de vous présenter à l'heure indiquée.</strong></p>
<p>N'oubliez pas d'apporter le matériel nécessaire.</p>
<p>Si vous avez des questions de dernière minute, n'hésitez pas à contacter le service RH.</p>
<p>À demain !</p>
`;
const variables = {
nomApprenant: params.apprenantNom,
prenomApprenant: params.apprenantPrenom,
nomFormation: params.formationNom,
nomSequence: params.sequenceNom,
dateDebut: params.dates[0]?.dateDebut.toLocaleDateString('fr-FR') || '',
dateFin: params.dates[params.dates.length - 1]?.dateFin.toLocaleDateString('fr-FR') || '',
datesHTML: datesHTML, // Ajouter le HTML des dates
lieu: params.lieu || '',
formateur: params.formateur || '',
};
// Préparer les pièces jointes si nécessaire
const attachments: Array<{ filename: string; content: string; contentType: string }> = [];
if (params.attachmentUrl && params.attachmentFilename && params.attachmentMimeType) {
try {
let base64: string;
// Vérifier si c'est une URL locale (commence par /uploads/)
if (params.attachmentUrl.startsWith('/uploads/')) {
// Lire le fichier depuis le système de fichiers
const fs = await import('fs/promises');
const path = await import('path');
const filePath = path.join(process.cwd(), params.attachmentUrl);
const fileBuffer = await fs.readFile(filePath);
base64 = fileBuffer.toString('base64');
} else {
// Télécharger le fichier depuis une URL externe (S3)
const response = await fetch(params.attachmentUrl);
const arrayBuffer = await response.arrayBuffer();
base64 = Buffer.from(arrayBuffer).toString('base64');
}
attachments.push({
filename: params.attachmentFilename,
content: base64,
contentType: params.attachmentMimeType,
});
} catch (error) {
console.error('[Email] Erreur lors du chargement de la pièce jointe:', error);
// Continuer l'envoi sans la pièce jointe
}
}
return sendEmail({
to: params.apprenantEmail,
subject: `Rappel J-1 : ${params.formationNom} - C'est demain !`,
html: await getEmailTemplate(content, 'rappel', variables),
attachments: attachments.length > 0 ? attachments : undefined,
});
}
/**
* Envoie des emails groupés à tous les inscrits d'une séquence
*/
export async function sendGroupEmail(params: {
recipients: Array<{ email: string; prenom: string; nom: string; fonction: string }>;
formationNom: string;
sequenceNom: string;
type: 'teaser' | 'rappel' | 'rappel_j1';
dates: Array<{ dateDebut: Date; dateFin: Date; ordre: number }>;
lieu?: string;
formateur?: string;
}): Promise<{ sent: number; failed: number }> {
let sent = 0;
let failed = 0;
for (const recipient of params.recipients) {
try {
if (params.type === 'teaser') {
await sendTeaserEmail({
apprenantEmail: recipient.email,
apprenantPrenom: recipient.prenom,
apprenantNom: recipient.nom,
apprenantFonction: recipient.fonction,
formationNom: params.formationNom,
sequenceNom: params.sequenceNom,
dates: params.dates,
lieu: params.lieu,
formateur: params.formateur,
});
} else if (params.type === 'rappel') {
await sendRappelJ7Email({
apprenantEmail: recipient.email,
apprenantPrenom: recipient.prenom,
apprenantNom: recipient.nom,
apprenantFonction: recipient.fonction,
formationNom: params.formationNom,
sequenceNom: params.sequenceNom,
dates: params.dates,
lieu: params.lieu || '',
formateur: params.formateur,
});
} else {
// rappel_j1
await sendRappelJ1Email({
apprenantEmail: recipient.email,
apprenantPrenom: recipient.prenom,
apprenantNom: recipient.nom,
apprenantFonction: recipient.fonction,
formationNom: params.formationNom,
sequenceNom: params.sequenceNom,
dates: params.dates,
lieu: params.lieu,
formateur: params.formateur,
});
}
sent++;
} catch (error) {
console.error(`Erreur envoi email à ${recipient.email}:`, error);
failed++;
}
}
return { sent, failed };
}
/**
* Envoie un email de réinitialisation de mot de passe
*/
export async function sendPasswordResetEmail(params: {
apprenantEmail: string;
apprenantNom: string;
apprenantPrenom: string;
resetLink: string;
}): Promise<boolean> {
const content = `
<h2>Réinitialisation de mot de passe</h2>
<p>Bonjour ${params.apprenantPrenom} ${params.apprenantNom},</p>
<p>Vous avez demandé la réinitialisation de votre mot de passe pour votre compte Manager Itinova.</p>
<p>Cliquez sur le bouton ci-dessous pour créer un nouveau mot de passe :</p>
<div style="text-align: center; margin: 30px 0;">
<a href="${params.resetLink}" class="button">Réinitialiser mon mot de passe</a>
</div>
<p>Ou copiez ce lien dans votre navigateur :</p>
<div style="background-color: white; padding: 15px; border-radius: 4px; word-break: break-all; font-family: monospace; font-size: 12px; border: 1px solid #e5e7eb;">
${params.resetLink}
</div>
<div class="info-box" style="background-color: #fef3c7; border-left-color: #f59e0b;">
<p><strong>⚠️ Important :</strong></p>
<ul>
<li>Ce lien est valide pendant <strong>24 heures</strong> seulement</li>
<li>Il ne peut être utilisé qu'<strong>une seule fois</strong></li>
<li>Si vous n'avez pas demandé cette réinitialisation, ignorez cet email</li>
</ul>
</div>
<p>Pour toute question, contactez le service RH.</p>
`;
return sendEmail({
to: params.apprenantEmail,
subject: 'Réinitialisation de votre mot de passe - Manager Itinova',
html: await getEmailTemplate(content, 'reset_password'),
});
}
/**
* ========================================
* SYSTÈME DE NOTIFICATIONS ENRICHI
* ========================================
*/
/**
* Envoie un email de remerciement post-formation à un apprenant
*/
export async function sendRemerciementPostFormation(params: {
apprenantEmail: string;
apprenantNom: string;
apprenantPrenom: string;
apprenantFonction: string;
formationNom: string;
sequenceNom: string;
formateurNom?: string;
lienQuestionnaire?: string;
}): Promise<boolean> {
const fonctionLabel = params.apprenantFonction === 'directeur' ? 'Directeur' :
params.apprenantFonction === 'chef_service' ? 'Chef de service' : '';
const salutation = fonctionLabel ? `${fonctionLabel} ${params.apprenantPrenom} ${params.apprenantNom}` : params.apprenantPrenom;
const content = `
<h2>Merci pour votre participation !</h2>
<p>Bonjour ${salutation},</p>
<p>Nous tenons à vous remercier pour votre participation à la formation <strong>${params.formationNom}</strong> (${params.sequenceNom}).</p>
${params.formateurNom ? `<p>Nous espérons que les sessions animées par <strong>${params.formateurNom}</strong> ont répondu à vos attentes.</p>` : ''}
<div class="info-box">
<h3>Votre avis compte !</h3>
<p>Afin d'améliorer continuellement nos formations, nous vous invitons à partager votre retour d'expérience.</p>
${params.lienQuestionnaire ? `
<div style="text-align: center; margin: 20px 0;">
<a href="${params.lienQuestionnaire}" class="button">Donner mon avis</a>
</div>
` : '<p>Un questionnaire de satisfaction vous sera envoyé prochainement.</p>'}
</div>
<p>Nous vous souhaitons une excellente continuation dans vos fonctions et espérons vous revoir lors de prochaines formations.</p>
<p>Cordialement,<br/>L'équipe Formation</p>
`;
const variables = {
nomApprenant: params.apprenantNom,
prenomApprenant: params.apprenantPrenom,
nomFormation: params.formationNom,
nomSequence: params.sequenceNom,
formateur: params.formateurNom || '',
};
return sendEmail({
to: params.apprenantEmail,
subject: `Merci pour votre participation - ${params.formationNom}`,
html: await getEmailTemplate(content, 'remerciement', variables),
});
}
/**
* Envoie une notification au formateur pour une nouvelle inscription
*/
export async function sendNotificationFormateurNouvelleInscription(params: {
formateurEmail: string;
formateurNom: string;
apprenantNom: string;
apprenantPrenom: string;
apprenantFonction: string;
apprenantEtablissement?: string;
formationNom: string;
sequenceNom: string;
nbInscrits: number;
capaciteMax: number;
dates: Array<{ dateDebut: Date; dateFin: Date; ordre: number }>;
}): Promise<boolean> {
const fonctionLabel = params.apprenantFonction === 'directeur' ? 'Directeur' :
params.apprenantFonction === 'chef_service' ? 'Chef de service' : 'Autre';
const datesHTML = params.dates.map(date => `
<li>Date ${date.ordre} : ${date.dateDebut.toLocaleDateString('fr-FR', {
weekday: 'long',
day: 'numeric',
month: 'long',
year: 'numeric'
})}</li>
`).join('');
const content = `
<h2>🎓 Nouvelle inscription à votre formation</h2>
<p>Bonjour ${params.formateurNom},</p>
<p>Un nouvel apprenant vient de s'inscrire à votre formation.</p>
<div class="info-box">
<h3>Détails de l'inscription</h3>
<p><strong>Apprenant :</strong> ${params.apprenantPrenom} ${params.apprenantNom}</p>
<p><strong>Fonction :</strong> ${fonctionLabel}</p>
${params.apprenantEtablissement ? `<p><strong>Établissement :</strong> ${params.apprenantEtablissement}</p>` : ''}
<p><strong>Formation :</strong> ${params.formationNom}</p>
<p><strong>Séquence :</strong> ${params.sequenceNom}</p>
</div>
<div class="info-box" style="background-color: #dbeafe; border-left-color: #3b82f6;">
<h3>📊 État des inscriptions</h3>
<p><strong>${params.nbInscrits} / ${params.capaciteMax}</strong> places occupées</p>
<div style="background-color: #e5e7eb; border-radius: 4px; height: 20px; margin: 10px 0;">
<div style="background-color: #3b82f6; border-radius: 4px; height: 20px; width: ${Math.min(100, (params.nbInscrits / params.capaciteMax) * 100)}%;"></div>
</div>
</div>
<h4>Dates de la formation :</h4>
<ul>${datesHTML}</ul>
<p>Cordialement,<br/>L'équipe Formation</p>
`;
return sendEmail({
to: params.formateurEmail,
subject: `Nouvelle inscription - ${params.formationNom} (${params.sequenceNom})`,
html: await getEmailTemplate(content, 'notification_formateur'),
});
}
/**
* Envoie une notification au formateur pour une annulation d'inscription
*/
export async function sendNotificationFormateurAnnulation(params: {
formateurEmail: string;
formateurNom: string;
apprenantNom: string;
apprenantPrenom: string;
apprenantFonction: string;
formationNom: string;
sequenceNom: string;
nbInscrits: number;
capaciteMax: number;
raisonAnnulation?: string;
}): Promise<boolean> {
const fonctionLabel = params.apprenantFonction === 'directeur' ? 'Directeur' :
params.apprenantFonction === 'chef_service' ? 'Chef de service' : 'Autre';
const content = `
<h2>⚠️ Annulation d'inscription</h2>
<p>Bonjour ${params.formateurNom},</p>
<p>Un apprenant a annulé son inscription à votre formation.</p>
<div class="info-box" style="background-color: #fef3c7; border-left-color: #f59e0b;">
<h3>Détails de l'annulation</h3>
<p><strong>Apprenant :</strong> ${params.apprenantPrenom} ${params.apprenantNom}</p>
<p><strong>Fonction :</strong> ${fonctionLabel}</p>
<p><strong>Formation :</strong> ${params.formationNom}</p>
<p><strong>Séquence :</strong> ${params.sequenceNom}</p>
${params.raisonAnnulation ? `<p><strong>Raison :</strong> ${params.raisonAnnulation}</p>` : ''}
</div>
<div class="info-box" style="background-color: #dbeafe; border-left-color: #3b82f6;">
<h3>📊 État des inscriptions après annulation</h3>
<p><strong>${params.nbInscrits} / ${params.capaciteMax}</strong> places occupées</p>
<p><strong>${params.capaciteMax - params.nbInscrits}</strong> place(s) disponible(s)</p>
</div>
<p>Cordialement,<br/>L'équipe Formation</p>
`;
return sendEmail({
to: params.formateurEmail,
subject: `Annulation d'inscription - ${params.formationNom} (${params.sequenceNom})`,
html: await getEmailTemplate(content, 'notification_formateur'),
});
}
/**
* Envoie une alerte aux admins quand la capacité maximale est atteinte
*/
export async function sendAlerteCapaciteAtteinte(params: {
adminEmails: string[];
formationNom: string;
sequenceNom: string;
capaciteMax: number;
nbInscrits: number;
nbListeAttente: number;
formateurNom?: string;
dates: Array<{ dateDebut: Date; dateFin: Date; ordre: number }>;
}): Promise<{ sent: number; failed: number }> {
const datesHTML = params.dates.map(date => `
<li>${date.dateDebut.toLocaleDateString('fr-FR', {
weekday: 'long',
day: 'numeric',
month: 'long',
year: 'numeric'
})}</li>
`).join('');
const content = `
<h2>🔔 Alerte : Capacité maximale atteinte</h2>
<div class="info-box" style="background-color: #fee2e2; border-left-color: #ef4444;">
<h3>⚠️ La séquence est complète</h3>
<p>La capacité maximale de la séquence <strong>${params.sequenceNom}</strong> a été atteinte.</p>
</div>
<div class="info-box">
<h3>Détails de la séquence</h3>
<p><strong>Formation :</strong> ${params.formationNom}</p>
<p><strong>Séquence :</strong> ${params.sequenceNom}</p>
${params.formateurNom ? `<p><strong>Formateur :</strong> ${params.formateurNom}</p>` : ''}
<p><strong>Capacité :</strong> ${params.nbInscrits} / ${params.capaciteMax} (100%)</p>
${params.nbListeAttente > 0 ? `<p><strong>Liste d'attente :</strong> ${params.nbListeAttente} personne(s)</p>` : ''}
</div>
<h4>Dates prévues :</h4>
<ul>${datesHTML}</ul>
<div class="info-box" style="background-color: #dbeafe; border-left-color: #3b82f6;">
<h3>Actions possibles</h3>
<ul>
<li>Augmenter la capacité maximale de la séquence</li>
<li>Créer une nouvelle séquence pour cette formation</li>
<li>Contacter les personnes en liste d'attente pour les informer</li>
</ul>
</div>
<p>Cordialement,<br/>Le système de gestion des formations</p>
`;
let sent = 0;
let failed = 0;
for (const adminEmail of params.adminEmails) {
try {
await sendEmail({
to: adminEmail,
subject: `🔔 Capacité atteinte - ${params.formationNom} (${params.sequenceNom})`,
html: await getEmailTemplate(content, 'alerte_admin'),
});
sent++;
} catch (error) {
console.error(`Erreur envoi alerte à ${adminEmail}:`, error);
failed++;
}
}
return { sent, failed };
}
/**
* Envoie une notification aux apprenants en liste d'attente quand une place se libère
*/
export async function sendNotificationPlaceDisponible(params: {
apprenantEmail: string;
apprenantNom: string;
apprenantPrenom: string;
apprenantFonction: string;
formationNom: string;
sequenceNom: string;
positionListeAttente: number;
lienConfirmation?: string;
delaiReponse?: number; // en heures
}): Promise<boolean> {
const fonctionLabel = params.apprenantFonction === 'directeur' ? 'Directeur' :
params.apprenantFonction === 'chef_service' ? 'Chef de service' : '';
const salutation = fonctionLabel ? `${fonctionLabel} ${params.apprenantPrenom} ${params.apprenantNom}` : params.apprenantPrenom;
const delai = params.delaiReponse || 48;
const content = `
<h2>🎉 Une place s'est libérée !</h2>
<p>Bonjour ${salutation},</p>
<p>Bonne nouvelle ! Une place vient de se libérer pour la formation à laquelle vous êtes inscrit(e) en liste d'attente.</p>
<div class="info-box" style="background-color: #d1fae5; border-left-color: #10b981;">
<h3>✅ Vous pouvez maintenant confirmer votre inscription</h3>
<p><strong>Formation :</strong> ${params.formationNom}</p>
<p><strong>Séquence :</strong> ${params.sequenceNom}</p>
<p><strong>Votre position :</strong> ${params.positionListeAttente}${params.positionListeAttente === 1 ? 'er' : 'ème'} sur la liste d'attente</p>
</div>
${params.lienConfirmation ? `
<div style="text-align: center; margin: 30px 0;">
<a href="${params.lienConfirmation}" class="button">Confirmer mon inscription</a>
</div>
` : '<p>Veuillez contacter le service RH pour confirmer votre inscription.</p>'}
<div class="info-box" style="background-color: #fef3c7; border-left-color: #f59e0b;">
<h3>⏰ Important</h3>
<p>Vous avez <strong>${delai} heures</strong> pour confirmer votre inscription.</p>
<p>Passé ce délai, la place sera proposée à la personne suivante sur la liste d'attente.</p>
</div>
<p>Cordialement,<br/>L'équipe Formation</p>
`;
const variables = {
nomApprenant: params.apprenantNom,
prenomApprenant: params.apprenantPrenom,
nomFormation: params.formationNom,
nomSequence: params.sequenceNom,
};
return sendEmail({
to: params.apprenantEmail,
subject: `🎉 Place disponible - ${params.formationNom}`,
html: await getEmailTemplate(content, 'notification_liste_attente', variables),
});
}
/**
* Envoie une notification à un apprenant en liste d'attente pour l'informer d'une annulation de séquence
*/
export async function sendNotificationAnnulationListeAttente(params: {
apprenantEmail: string;
apprenantNom: string;
apprenantPrenom: string;
apprenantFonction: string;
formationNom: string;
sequenceNom: string;
raisonAnnulation?: string;
alternativesDisponibles?: Array<{ sequenceNom: string; nbPlaces: number }>;
}): Promise<boolean> {
const fonctionLabel = params.apprenantFonction === 'directeur' ? 'Directeur' :
params.apprenantFonction === 'chef_service' ? 'Chef de service' : '';
const salutation = fonctionLabel ? `${fonctionLabel} ${params.apprenantPrenom} ${params.apprenantNom}` : params.apprenantPrenom;
const alternativesHTML = params.alternativesDisponibles && params.alternativesDisponibles.length > 0
? `
<div class="info-box" style="background-color: #dbeafe; border-left-color: #3b82f6;">
<h3>📋 Séquences alternatives disponibles</h3>
<ul>
${params.alternativesDisponibles.map(alt => `
<li><strong>${alt.sequenceNom}</strong> - ${alt.nbPlaces} place(s) disponible(s)</li>
`).join('')}
</ul>
<p>Contactez le service RH pour vous inscrire à une autre séquence.</p>
</div>
` : '';
const content = `
<h2>Information : Annulation de séquence</h2>
<p>Bonjour ${salutation},</p>
<p>Nous vous informons que la séquence de formation pour laquelle vous étiez en liste d'attente a été annulée.</p>
<div class="info-box" style="background-color: #fee2e2; border-left-color: #ef4444;">
<h3>Séquence annulée</h3>
<p><strong>Formation :</strong> ${params.formationNom}</p>
<p><strong>Séquence :</strong> ${params.sequenceNom}</p>
${params.raisonAnnulation ? `<p><strong>Raison :</strong> ${params.raisonAnnulation}</p>` : ''}
</div>
${alternativesHTML}
<p>Nous nous excusons pour ce désagrément et restons à votre disposition pour toute question.</p>
<p>Cordialement,<br/>L'équipe Formation</p>
`;
return sendEmail({
to: params.apprenantEmail,
subject: `Information : Annulation de séquence - ${params.formationNom}`,
html: await getEmailTemplate(content, 'notification_annulation'),
});
}
/**
* Envoie une notification au formateur quand la capacité maximale est atteinte
*/
export async function sendNotificationFormateurCapaciteAtteinte(params: {
formateurEmail: string;
formateurNom: string;
formationNom: string;
sequenceNom: string;
capaciteMax: number;
nbInscrits: number;
dates: Array<{ dateDebut: Date; dateFin: Date; ordre: number }>;
}): Promise<boolean> {
const datesHTML = params.dates.map(date => `
<li>Date ${date.ordre} : ${date.dateDebut.toLocaleDateString('fr-FR', {
weekday: 'long',
day: 'numeric',
month: 'long',
year: 'numeric'
})}</li>
`).join('');
const content = `
<h2>⚠️ Capacité maximale atteinte</h2>
<p>Bonjour ${params.formateurNom},</p>
<p>La capacité maximale de votre formation a été atteinte.</p>
<div class="info-box" style="background-color: #fee2e2; border-left-color: #ef4444;">
<h3>Détails de la séquence</h3>
<p><strong>Formation :</strong> ${params.formationNom}</p>
<p><strong>Séquence :</strong> ${params.sequenceNom}</p>
<p><strong>Capacité :</strong> ${params.nbInscrits} / ${params.capaciteMax}</p>
</div>
<h4>Dates de la formation :</h4>
<ul>${datesHTML}</ul>
<p>Les nouvelles inscriptions seront automatiquement placées en liste d'attente.</p>
<p>Cordialement,<br/>L'équipe Formation</p>
`;
return sendEmail({
to: params.formateurEmail,
subject: `⚠️ Capacité maximale atteinte - ${params.formationNom} (${params.sequenceNom})`,
html: await getEmailTemplate(content, 'notification_formateur'),
});
}
/**
* Envoie une notification au formateur quand une place se libère
*/
export async function sendNotificationFormateurPlaceDisponible(params: {
formateurEmail: string;
formateurNom: string;
formationNom: string;
sequenceNom: string;
capaciteMax: number;
nbInscrits: number;
placesDisponibles: number;
}): Promise<boolean> {
const content = `
<h2>✅ Place disponible</h2>
<p>Bonjour ${params.formateurNom},</p>
<p>Une place s'est libérée dans votre formation suite à une annulation.</p>
<div class="info-box" style="background-color: #d1fae5; border-left-color: #10b981;">
<h3>Détails de la séquence</h3>
<p><strong>Formation :</strong> ${params.formationNom}</p>
<p><strong>Séquence :</strong> ${params.sequenceNom}</p>
<p><strong>Places occupées :</strong> ${params.nbInscrits} / ${params.capaciteMax}</p>
<p><strong>Places disponibles :</strong> ${params.placesDisponibles}</p>
</div>
<p>De nouvelles inscriptions peuvent maintenant être acceptées.</p>
<p>Cordialement,<br/>L'équipe Formation</p>
`;
return sendEmail({
to: params.formateurEmail,
subject: `✅ Place disponible - ${params.formationNom} (${params.sequenceNom})`,
html: await getEmailTemplate(content, 'notification_formateur'),
});
}
/**
* Envoie une attestation de formation par email
*/
export async function sendAttestationEmail(params: {
to: string;
apprenantNom: string;
apprenantPrenom: string;
formationNom: string;
pdfUrl: string;
}): Promise<boolean> {
const content = `
<h2>Votre attestation de formation</h2>
<p>Bonjour ${params.apprenantPrenom} ${params.apprenantNom},</p>
<p>Nous avons le plaisir de vous transmettre votre attestation de formation pour :</p>
<div class="info-box" style="background-color: #dbeafe; border-left-color: #3b82f6;">
<h3>${params.formationNom}</h3>
</div>
<p>Vous trouverez votre attestation en pièce jointe de cet email.</p>
<p>Cette attestation certifie votre participation à la formation et peut être utilisée pour votre dossier professionnel.</p>
<p>Nous vous remercions pour votre participation et vous souhaitons une excellente continuation.</p>
<p>Cordialement,<br/>L'équipe Formation</p>
`;
// Télécharger le PDF depuis l'URL
let pdfBuffer: Buffer;
try {
const response = await fetch(params.pdfUrl);
if (!response.ok) {
throw new Error(`Failed to download PDF: ${response.statusText}`);
}
const arrayBuffer = await response.arrayBuffer();
pdfBuffer = Buffer.from(arrayBuffer);
} catch (error) {
console.error('[sendAttestationEmail] Error downloading PDF:', error);
throw error;
}
return sendEmail({
to: params.to,
subject: `Votre attestation de formation - ${params.formationNom}`,
html: await getEmailTemplate(content, 'attestation'),
attachments: [{
filename: `Attestation_${params.apprenantNom}_${params.apprenantPrenom}.pdf`,
content: pdfBuffer.toString('base64'),
contentType: 'application/pdf',
}],
});
}
/**
* Alias pour sendPasswordResetEmail avec des paramètres plus simples
*/
export async function sendResetPasswordEmail(params: {
email: string;
prenom: string;
nom: string;
lienReinitialisation: string;
}): Promise<boolean> {
return sendPasswordResetEmail({
apprenantEmail: params.email,
apprenantNom: params.nom,
apprenantPrenom: params.prenom,
resetLink: params.lienReinitialisation,
});
}