meilleure gestion des templates d'email (allégés)

This commit is contained in:
Yannick Le Duc
2025-09-16 16:47:14 +02:00
parent b20c88b05d
commit 17deb72834
6 changed files with 227 additions and 19 deletions

View File

@@ -75,11 +75,15 @@ Une application web moderne et éthique pour gérer des campagnes de budgets par
- Affichage des descriptions avec support Markdown
- Sauvegarde des votes
#### 📧 **Système d'email**
#### 📧 **Système d'email avancé**
- **Configuration SMTP** : Interface d'administration pour configurer les paramètres email
- **Envoi d'emails** : Notifications aux participants
- **Envoi d'emails personnalisés** : Envoi d'emails individuels aux participants avec liens de vote
- **Templates personnalisables** : Messages d'email configurables avec placeholders [PRENOM] et [NOM]
- **Envoi en masse** : Envoi d'emails à tous les participants d'une campagne
- **Test d'envoi** : Fonctionnalité de test des paramètres SMTP
- **Templates personnalisables** : Messages d'email configurables
- **Footer personnalisable** : Messages de pied de page avec liens cliquables
- **HTML responsive** : Emails avec design moderne et boutons d'action
- **Gestion d'erreurs** : Messages d'erreur détaillés pour les problèmes SMTP
#### 📊 **Export des données**
- **Export ODS** : Export des statistiques de vote en format tableur
@@ -106,6 +110,10 @@ Une application web moderne et éthique pour gérer des campagnes de budgets par
- **Validation en temps réel** : Vérification des budgets lors du vote
- **Gestion d'erreurs** : Messages d'erreur informatifs
- **États de chargement** : Feedback visuel pendant les opérations
- **Personnalisation des emails** : Placeholders [PRENOM] et [NOM] dans les messages
- **Footer dynamique** : Messages de pied de page avec liens cliquables vers le projet
- **Interface d'envoi d'emails** : Modales dédiées pour l'envoi personnalisé
- **Suivi des envois** : Indicateurs de progression pour les envois en masse
## 🛠️ Installation
@@ -397,9 +405,11 @@ npm run test:watch
```
### Couverture des tests
- **Tests unitaires** : Utilitaires, validation, formatage
- **Tests d'intégration** : Services et API
- **Tests unitaires** : Utilitaires, validation, formatage, parsing de messages
- **Tests d'intégration** : Services et API, système d'email
- **Tests E2E** : Flux complets (Playwright)
- **Tests de sécurité** : Vérification des politiques RLS et authentification
- **Tests de composants** : Interface utilisateur et modales
## 📚 Documentation
@@ -408,11 +418,14 @@ Pour une documentation complète, consultez le dossier [docs/](docs/) :
- **[Guide de démarrage](docs/README.md)** - Vue d'ensemble de la documentation
- **[Configuration](docs/SETUP.md)** - Installation et configuration détaillée
- **[Sécurité](docs/SECURITY-SUMMARY.md)** - Résumé de la sécurisation
- **[Paramètres](docs/SETTINGS.md)** - Configuration avancée
- **[Gestion des administrateurs](docs/ADMIN-MANAGEMENT.md)** - Configuration des utilisateurs admin
- **[Paramètres](docs/SETTINGS.md)** - Configuration avancée et SMTP
- **[Tests](docs/TESTING.md)** - Guide complet des tests
- **[Tests - Résumé](docs/TESTING_SUMMARY.md)** - Résumé de la suite de tests
- **[Tests - Démarrage rapide](docs/README-TESTS.md)** - Démarrage rapide des tests
- **[Export ODS](docs/EXPORT-FEATURE.md)** - Fonctionnalité d'export des statistiques
- **[Architecture](docs/NEW-ARCHITECTURE.md)** - Nouvelle architecture simplifiée
- **[Structure du projet](docs/PROJECT-STRUCTURE.md)** - Organisation du code
## 🤝 Contribution
@@ -452,4 +465,16 @@ Cette application est développée avec des valeurs éthiques :
**Développé avec ❤️ pour faciliter la démocratie participative**
*Application complète et prête pour la production avec authentification, interface moderne, système d'email et toutes les fonctionnalités de gestion de budgets participatifs.*
*Application complète et prête pour la production avec authentification, interface moderne, système d'email avancé, tests complets et toutes les fonctionnalités de gestion de budgets participatifs.*
---
## 📈 **Version actuelle : 0.2.0**
### 🆕 **Dernières améliorations**
- **Système d'email avancé** : Envoi personnalisé avec templates et placeholders
- **Interface d'envoi d'emails** : Modales dédiées pour l'envoi individuel et en masse
- **Footer personnalisable** : Messages de pied de page avec liens cliquables
- **Tests étendus** : Couverture complète des fonctionnalités email
- **Gestion d'erreurs améliorée** : Messages d'erreur détaillés pour SMTP
- **HTML responsive** : Emails avec design moderne et boutons d'action

View File

@@ -63,3 +63,34 @@ global.IntersectionObserver = jest.fn().mockImplementation(() => ({
unobserve: jest.fn(),
disconnect: jest.fn(),
}));
// Mock NextRequest and NextResponse for API routes
global.Request = global.Request || class Request {
constructor(input, init) {
this.url = typeof input === 'string' ? input : input.url;
this.method = init?.method || 'GET';
this.headers = new Map(Object.entries(init?.headers || {}));
this._body = init?.body;
}
async json() {
return JSON.parse(this._body);
}
async text() {
return this._body;
}
};
global.Response = global.Response || class Response {
constructor(body, init) {
this.body = body;
this.status = init?.status || 200;
this.statusText = init?.statusText || 'OK';
this.headers = new Map(Object.entries(init?.headers || {}));
}
async json() {
return JSON.parse(this.body);
}
};

View File

@@ -0,0 +1,120 @@
import { parseFooterMessage } from '../../lib/utils';
import { PROJECT_CONFIG } from '../../lib/project.config';
describe('Footer Email Integration', () => {
describe('parseFooterMessage', () => {
it('should parse footer message with GITURL link', () => {
const footerMessage = 'Développé avec ❤️ pour faciliter la démocratie participative - [Logiciel libre et open source](GITURL)';
const repositoryUrl = PROJECT_CONFIG.repository.url;
const result = parseFooterMessage(footerMessage, repositoryUrl);
expect(result.text).toBe('Développé avec ❤️ pour faciliter la démocratie participative - Logiciel libre et open source');
expect(result.links).toHaveLength(1);
expect(result.links[0]).toMatchObject({
text: 'Logiciel libre et open source',
url: repositoryUrl
});
expect(result.links[0].start).toBeGreaterThan(0);
expect(result.links[0].end).toBeGreaterThan(result.links[0].start);
});
it('should handle footer message without links', () => {
const footerMessage = 'Simple footer message without links';
const repositoryUrl = PROJECT_CONFIG.repository.url;
const result = parseFooterMessage(footerMessage, repositoryUrl);
expect(result.text).toBe('Simple footer message without links');
expect(result.links).toHaveLength(0);
});
it('should handle multiple links in footer message', () => {
const footerMessage = 'Check our [docs](GITURL) and [code](GITURL)';
const repositoryUrl = PROJECT_CONFIG.repository.url;
const result = parseFooterMessage(footerMessage, repositoryUrl);
expect(result.text).toBe('Check our docs and code');
expect(result.links).toHaveLength(2);
expect(result.links[0].text).toBe('docs');
expect(result.links[1].text).toBe('code');
expect(result.links[0].url).toBe(repositoryUrl);
expect(result.links[1].url).toBe(repositoryUrl);
});
it('should handle empty footer message', () => {
const footerMessage = '';
const repositoryUrl = PROJECT_CONFIG.repository.url;
const result = parseFooterMessage(footerMessage, repositoryUrl);
expect(result.text).toBe('');
expect(result.links).toHaveLength(0);
});
});
describe('Footer message integration in emails', () => {
it('should generate correct footer text for email HTML', () => {
const footerMessage = 'Développé avec ❤️ pour faciliter la démocratie participative - [Logiciel libre et open source](GITURL)';
const repositoryUrl = PROJECT_CONFIG.repository.url;
const { text: processedFooterText, links } = parseFooterMessage(footerMessage, repositoryUrl);
// Vérifier que le texte traité peut être utilisé dans du HTML
expect(processedFooterText).toBe('Développé avec ❤️ pour faciliter la démocratie participative - Logiciel libre et open source');
expect(processedFooterText).not.toContain('[Logiciel libre et open source](GITURL)');
expect(processedFooterText).toContain('Logiciel libre et open source');
// Vérifier que les liens sont disponibles pour générer le HTML
expect(links).toHaveLength(1);
expect(links[0].text).toBe('Logiciel libre et open source');
expect(links[0].url).toBe(repositoryUrl);
});
it('should generate HTML with clickable links', () => {
const footerMessage = 'Développé avec ❤️ pour faciliter la démocratie participative - [Logiciel libre et open source](GITURL)';
const repositoryUrl = PROJECT_CONFIG.repository.url;
const { text: processedFooterText, links } = parseFooterMessage(footerMessage, repositoryUrl);
// Simuler la génération du HTML avec les liens
let footerHtml = processedFooterText;
if (links.length > 0) {
links.forEach(link => {
const linkHtml = `<a href="${link.url}" style="color: #6b7280; text-decoration: underline;" target="_blank" rel="noopener noreferrer">${link.text}</a>`;
footerHtml = footerHtml.replace(link.text, linkHtml);
});
}
// Vérifier que le HTML contient les liens cliquables
expect(footerHtml).toContain('<a href="' + repositoryUrl + '"');
expect(footerHtml).toContain('target="_blank"');
expect(footerHtml).toContain('rel="noopener noreferrer"');
expect(footerHtml).toContain('Logiciel libre et open source');
expect(footerHtml).not.toContain('[Logiciel libre et open source](GITURL)');
});
it('should handle special characters in footer message', () => {
const footerMessage = 'Footer with special chars: @#$%^&*() and [link](GITURL)';
const repositoryUrl = PROJECT_CONFIG.repository.url;
const { text: processedFooterText } = parseFooterMessage(footerMessage, repositoryUrl);
expect(processedFooterText).toBe('Footer with special chars: @#$%^&*() and link');
expect(processedFooterText).toContain('@#$%^&*()');
});
it('should handle personalized message placeholders', () => {
const message = 'Bonjour [PRENOM], votre nom est [NOM].';
const firstName = 'Jean';
const lastName = 'Dupont';
const personalizedMessage = message
.replace(/\[PRENOM\]/g, firstName)
.replace(/\[NOM\]/g, lastName);
expect(personalizedMessage).toBe('Bonjour Jean, votre nom est Dupont.');
});
});
});

View File

@@ -57,7 +57,7 @@ function SendEmailsPageContent() {
// Initialiser le message par défaut
if (campaignData) {
setDefaultSubject(`Votez pour la campagne "${campaignData.title}"`);
setDefaultMessage(`Bonjour,
setDefaultMessage(`Bonjour [PRENOM],
Vous êtes invité(e) à participer au vote pour la campagne "${campaignData.title}".

View File

@@ -1,6 +1,9 @@
import { NextRequest, NextResponse } from 'next/server';
import * as nodemailer from 'nodemailer';
import { SmtpSettings } from '@/types';
import { settingsService } from '@/lib/services';
import { parseFooterMessage } from '@/lib/utils';
import { PROJECT_CONFIG } from '@/lib/project.config';
export async function POST(request: NextRequest) {
try {
@@ -59,19 +62,44 @@ export async function POST(request: NextRequest) {
// Vérifier la connexion
await transporter.verify();
// Récupérer le message du footer depuis les paramètres
let footerMessage = '';
try {
footerMessage = await settingsService.getStringValue(
'footer_message',
'Développé avec ❤️ pour faciliter la démocratie participative - [Logiciel libre et open source](GITURL)'
);
} catch (error) {
console.warn('Erreur lors de la récupération du message du footer:', error);
footerMessage = 'Développé avec ❤️ pour faciliter la démocratie participative - [Logiciel libre et open source](GITURL)';
}
// Traiter le message du footer pour remplacer les liens
const { text: processedFooterText, links } = parseFooterMessage(footerMessage, PROJECT_CONFIG.repository.url);
// Générer le HTML du footer avec les liens cliquables
let footerHtml = processedFooterText;
if (links.length > 0) {
// Remplacer les liens par des balises <a> HTML
links.forEach(link => {
const linkHtml = `<a href="${link.url}" style="color: #6b7280; text-decoration: underline;" target="_blank" rel="noopener noreferrer">${link.text}</a>`;
footerHtml = footerHtml.replace(link.text, linkHtml);
});
}
// Traiter le message pour remplacer les placeholders [NOM] et [PRENOM]
const firstName = toName.split(' ')[0];
const lastName = toName.split(' ').slice(1).join(' ');
let personalizedMessage = message
.replace(/\[PRENOM\]/g, firstName)
.replace(/\[NOM\]/g, lastName);
// Créer le contenu HTML de l'email
const htmlContent = `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; line-height: 1.6;">
<div style="background-color: #2563eb; color: white; padding: 20px; text-align: center; border-radius: 8px 8px 0 0;">
<h1 style="margin: 0; font-size: 24px;">Mes Budgets Participatifs</h1>
</div>
<div style="background-color: #ffffff; padding: 30px; border: 1px solid #e5e7eb; border-top: none;">
<h2 style="color: #1f2937; margin-top: 0;">Bonjour ${toName},</h2>
<div style="background-color: #f3f4f6; padding: 20px; border-radius: 8px; margin: 20px 0;">
<h3 style="margin-top: 0; color: #374151;">Campagne : ${campaignTitle}</h3>
<p style="margin-bottom: 0; color: #6b7280;">${message.replace(/\n/g, '<br>')}</p>
<div style="background-color: #ffffff; padding: 30px; border: 1px solid #e5e7eb; border-radius: 8px;">
<div style="color: #374151; font-size: 16px; margin-bottom: 30px;">
${personalizedMessage.replace(/\n/g, '<br>')}
</div>
<div style="text-align: center; margin: 30px 0;">
@@ -102,6 +130,10 @@ export async function POST(request: NextRequest) {
Cet email a été envoyé automatiquement par Mes Budgets Participatifs.<br>
Si vous avez des questions, contactez l'administrateur de la campagne.
</p>
<hr style="border: none; border-top: 1px solid #e5e7eb; margin: 15px 0;">
<p style="color: #9ca3af; font-size: 11px; margin: 0;">
${footerHtml}
</p>
</div>
</div>
`;

View File

@@ -38,7 +38,7 @@ export default function SendParticipantEmailModal({
useEffect(() => {
if (isOpen && campaign && participant) {
setSubject(`Votez pour la campagne "${campaign.title}"`);
setMessage(`Bonjour ${participant.first_name},
setMessage(`Bonjour [PRENOM],
Vous êtes invité(e) à participer au vote pour la campagne "${campaign.title}".