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
|
- Affichage des descriptions avec support Markdown
|
||||||
- Sauvegarde des votes
|
- Sauvegarde des votes
|
||||||
|
|
||||||
#### 📧 **Système d'email**
|
#### 📧 **Système d'email avancé**
|
||||||
- **Configuration SMTP** : Interface d'administration pour configurer les paramètres email
|
- **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
|
- **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 des données**
|
||||||
- **Export ODS** : Export des statistiques de vote en format tableur
|
- **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
|
- **Validation en temps réel** : Vérification des budgets lors du vote
|
||||||
- **Gestion d'erreurs** : Messages d'erreur informatifs
|
- **Gestion d'erreurs** : Messages d'erreur informatifs
|
||||||
- **États de chargement** : Feedback visuel pendant les opérations
|
- **É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
|
## 🛠️ Installation
|
||||||
|
|
||||||
@@ -397,9 +405,11 @@ npm run test:watch
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Couverture des tests
|
### Couverture des tests
|
||||||
- **Tests unitaires** : Utilitaires, validation, formatage
|
- **Tests unitaires** : Utilitaires, validation, formatage, parsing de messages
|
||||||
- **Tests d'intégration** : Services et API
|
- **Tests d'intégration** : Services et API, système d'email
|
||||||
- **Tests E2E** : Flux complets (Playwright)
|
- **Tests E2E** : Flux complets (Playwright)
|
||||||
|
- **Tests de sécurité** : Vérification des politiques RLS et authentification
|
||||||
|
- **Tests de composants** : Interface utilisateur et modales
|
||||||
|
|
||||||
## 📚 Documentation
|
## 📚 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
|
- **[Guide de démarrage](docs/README.md)** - Vue d'ensemble de la documentation
|
||||||
- **[Configuration](docs/SETUP.md)** - Installation et configuration détaillée
|
- **[Configuration](docs/SETUP.md)** - Installation et configuration détaillée
|
||||||
- **[Sécurité](docs/SECURITY-SUMMARY.md)** - Résumé de la sécurisation
|
- **[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](docs/TESTING.md)** - Guide complet des tests
|
||||||
- **[Tests - Résumé](docs/TESTING_SUMMARY.md)** - Résumé de la suite de 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
|
- **[Tests - Démarrage rapide](docs/README-TESTS.md)** - Démarrage rapide des tests
|
||||||
- **[Export ODS](docs/EXPORT-FEATURE.md)** - Fonctionnalité d'export des statistiques
|
- **[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
|
## 🤝 Contribution
|
||||||
|
|
||||||
@@ -452,4 +465,16 @@ Cette application est développée avec des valeurs éthiques :
|
|||||||
|
|
||||||
**Développé avec ❤️ pour faciliter la démocratie participative**
|
**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(),
|
unobserve: jest.fn(),
|
||||||
disconnect: 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
|
// Initialiser le message par défaut
|
||||||
if (campaignData) {
|
if (campaignData) {
|
||||||
setDefaultSubject(`Votez pour la campagne "${campaignData.title}"`);
|
setDefaultSubject(`Votez pour la campagne "${campaignData.title}"`);
|
||||||
setDefaultMessage(`Bonjour,
|
setDefaultMessage(`Bonjour [PRENOM],
|
||||||
|
|
||||||
Vous êtes invité(e) à participer au vote pour la campagne "${campaignData.title}".
|
Vous êtes invité(e) à participer au vote pour la campagne "${campaignData.title}".
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import * as nodemailer from 'nodemailer';
|
import * as nodemailer from 'nodemailer';
|
||||||
import { SmtpSettings } from '@/types';
|
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) {
|
export async function POST(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
@@ -59,19 +62,44 @@ export async function POST(request: NextRequest) {
|
|||||||
// Vérifier la connexion
|
// Vérifier la connexion
|
||||||
await transporter.verify();
|
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
|
// Créer le contenu HTML de l'email
|
||||||
const htmlContent = `
|
const htmlContent = `
|
||||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; line-height: 1.6;">
|
<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;">
|
<div style="background-color: #ffffff; padding: 30px; border: 1px solid #e5e7eb; border-radius: 8px;">
|
||||||
<h1 style="margin: 0; font-size: 24px;">Mes Budgets Participatifs</h1>
|
<div style="color: #374151; font-size: 16px; margin-bottom: 30px;">
|
||||||
</div>
|
${personalizedMessage.replace(/\n/g, '<br>')}
|
||||||
|
|
||||||
<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>
|
</div>
|
||||||
|
|
||||||
<div style="text-align: center; margin: 30px 0;">
|
<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>
|
Cet email a été envoyé automatiquement par Mes Budgets Participatifs.<br>
|
||||||
Si vous avez des questions, contactez l'administrateur de la campagne.
|
Si vous avez des questions, contactez l'administrateur de la campagne.
|
||||||
</p>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export default function SendParticipantEmailModal({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen && campaign && participant) {
|
if (isOpen && campaign && participant) {
|
||||||
setSubject(`Votez pour la campagne "${campaign.title}"`);
|
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}".
|
Vous êtes invité(e) à participer au vote pour la campagne "${campaign.title}".
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user