diff --git a/README.md b/README.md index b515aaa..da1d0d7 100644 --- a/README.md +++ b/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 diff --git a/jest.setup.js b/jest.setup.js index b22e660..6cd35d6 100644 --- a/jest.setup.js +++ b/jest.setup.js @@ -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); + } +}; diff --git a/src/__tests__/lib/footer-email.test.ts b/src/__tests__/lib/footer-email.test.ts new file mode 100644 index 0000000..aadb3ae --- /dev/null +++ b/src/__tests__/lib/footer-email.test.ts @@ -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 = `${link.text}`; + footerHtml = footerHtml.replace(link.text, linkHtml); + }); + } + + // Vérifier que le HTML contient les liens cliquables + expect(footerHtml).toContain(' { + 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.'); + }); + }); +}); diff --git a/src/app/admin/campaigns/[id]/send-emails/page.tsx b/src/app/admin/campaigns/[id]/send-emails/page.tsx index 56b75ca..3b76d95 100644 --- a/src/app/admin/campaigns/[id]/send-emails/page.tsx +++ b/src/app/admin/campaigns/[id]/send-emails/page.tsx @@ -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}". diff --git a/src/app/api/send-participant-email/route.ts b/src/app/api/send-participant-email/route.ts index 798c749..f93369b 100644 --- a/src/app/api/send-participant-email/route.ts +++ b/src/app/api/send-participant-email/route.ts @@ -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 HTML + links.forEach(link => { + const linkHtml = `${link.text}`; + 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 = `
-
-

Mes Budgets Participatifs

-
- -
-

Bonjour ${toName},

- -
-

Campagne : ${campaignTitle}

-

${message.replace(/\n/g, '
')}

+
+
+ ${personalizedMessage.replace(/\n/g, '
')}
@@ -102,6 +130,10 @@ export async function POST(request: NextRequest) { Cet email a été envoyé automatiquement par Mes Budgets Participatifs.
Si vous avez des questions, contactez l'administrateur de la campagne.

+
+

+ ${footerHtml} +

`; diff --git a/src/components/SendParticipantEmailModal.tsx b/src/components/SendParticipantEmailModal.tsx index 5de62e6..055f11d 100644 --- a/src/components/SendParticipantEmailModal.tsx +++ b/src/components/SendParticipantEmailModal.tsx @@ -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}".