meilleure gestion des templates d'email (allégés)
This commit is contained in:
39
README.md
39
README.md
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
120
src/__tests__/lib/footer-email.test.ts
Normal file
120
src/__tests__/lib/footer-email.test.ts
Normal 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.');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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}".
|
||||
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
|
||||
@@ -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}".
|
||||
|
||||
|
||||
Reference in New Issue
Block a user