From 37e0f8da49cb407430fc9e3d77f67d57360c23f6 Mon Sep 17 00:00:00 2001 From: Manus Sandbox Date: Wed, 19 Nov 2025 08:41:10 -0500 Subject: [PATCH] Rollback to 9d67724e --- .gitkeep | 0 .manus/db/db-query-1763414434598.json | 18 + .manus/db/db-query-1763417332805.json | 9 + .manus/db/db-query-1763417422542.json | 9 + DOCUMENTATION.md | 285 +++++++ EMAIL_SETUP.md | 109 +++ client/src/App.tsx | 31 +- client/src/components/DashboardLayout.tsx | 18 +- client/src/pages/Admin.tsx | 154 ++++ client/src/pages/AdminApprenants.tsx | 331 +++++++ client/src/pages/AdminEmailConfig.tsx | 323 +++++++ client/src/pages/AdminEmailTemplates.tsx | 364 ++++++++ client/src/pages/AdminFormations.tsx | 335 ++++++++ client/src/pages/AdminRapportPublicCible.tsx | 280 ++++++ client/src/pages/AdminSequenceInscrits.tsx | 557 ++++++++++++ client/src/pages/AdminSequences.tsx | 853 +++++++++++++++++++ client/src/pages/AdminUsers.tsx | 562 ++++++++++++ client/src/pages/ComponentShowcase.tsx | 2 +- client/src/pages/Home.tsx | 197 ++++- client/src/pages/Inscription.tsx | 432 ++++++++++ drizzle/0000_charming_grey_gargoyle.sql | 13 + drizzle/0001_smooth_warbird.sql | 54 ++ drizzle/0002_fuzzy_sugar_man.sql | 1 + drizzle/0003_steady_mystique.sql | 32 + drizzle/0004_futuristic_malice.sql | 1 + drizzle/0005_cuddly_bushwacker.sql | 2 + drizzle/0006_lame_madrox.sql | 1 + drizzle/0007_awesome_genesis.sql | 10 + drizzle/0008_jittery_fenris.sql | 16 + drizzle/0009_polite_senator_kelly.sql | 13 + drizzle/meta/0000_snapshot.json | 110 +++ drizzle/meta/0001_snapshot.json | 473 ++++++++++ drizzle/meta/0002_snapshot.json | 480 +++++++++++ drizzle/meta/0003_snapshot.json | 485 +++++++++++ drizzle/meta/0004_snapshot.json | 492 +++++++++++ drizzle/meta/0005_snapshot.json | 492 +++++++++++ drizzle/meta/0006_snapshot.json | 500 +++++++++++ drizzle/meta/0007_snapshot.json | 568 ++++++++++++ drizzle/meta/0008_snapshot.json | 684 +++++++++++++++ drizzle/meta/0009_snapshot.json | 779 +++++++++++++++++ drizzle/meta/_journal.json | 75 +- drizzle/schema.ts | 176 +++- migrate-to-sequences.sh | 21 + package.json | 5 +- pnpm-lock.yaml | 242 +++++- server/_core/emailSender.ts | 165 ++++ server/dateUtils.ts | 60 ++ server/db.ts | 572 ++++++++++++- server/emailService.ts | 331 +++++++ server/emailTemplateGenerator.ts | 161 ++++ server/exportService.ts | 234 +++++ server/icsGenerator.ts | 109 +++ server/routers.ts | 704 ++++++++++++++- todo.md | 243 ++++++ 54 files changed, 13118 insertions(+), 55 deletions(-) create mode 100644 .gitkeep create mode 100644 .manus/db/db-query-1763414434598.json create mode 100644 .manus/db/db-query-1763417332805.json create mode 100644 .manus/db/db-query-1763417422542.json create mode 100644 DOCUMENTATION.md create mode 100644 EMAIL_SETUP.md create mode 100644 client/src/pages/Admin.tsx create mode 100644 client/src/pages/AdminApprenants.tsx create mode 100644 client/src/pages/AdminEmailConfig.tsx create mode 100644 client/src/pages/AdminEmailTemplates.tsx create mode 100644 client/src/pages/AdminFormations.tsx create mode 100644 client/src/pages/AdminRapportPublicCible.tsx create mode 100644 client/src/pages/AdminSequenceInscrits.tsx create mode 100644 client/src/pages/AdminSequences.tsx create mode 100644 client/src/pages/AdminUsers.tsx create mode 100644 client/src/pages/Inscription.tsx create mode 100644 drizzle/0000_charming_grey_gargoyle.sql create mode 100644 drizzle/0001_smooth_warbird.sql create mode 100644 drizzle/0002_fuzzy_sugar_man.sql create mode 100644 drizzle/0003_steady_mystique.sql create mode 100644 drizzle/0004_futuristic_malice.sql create mode 100644 drizzle/0005_cuddly_bushwacker.sql create mode 100644 drizzle/0006_lame_madrox.sql create mode 100644 drizzle/0007_awesome_genesis.sql create mode 100644 drizzle/0008_jittery_fenris.sql create mode 100644 drizzle/0009_polite_senator_kelly.sql create mode 100644 drizzle/meta/0000_snapshot.json create mode 100644 drizzle/meta/0001_snapshot.json create mode 100644 drizzle/meta/0002_snapshot.json create mode 100644 drizzle/meta/0003_snapshot.json create mode 100644 drizzle/meta/0004_snapshot.json create mode 100644 drizzle/meta/0005_snapshot.json create mode 100644 drizzle/meta/0006_snapshot.json create mode 100644 drizzle/meta/0007_snapshot.json create mode 100644 drizzle/meta/0008_snapshot.json create mode 100644 drizzle/meta/0009_snapshot.json create mode 100755 migrate-to-sequences.sh create mode 100644 server/_core/emailSender.ts create mode 100644 server/dateUtils.ts create mode 100644 server/emailService.ts create mode 100644 server/emailTemplateGenerator.ts create mode 100644 server/exportService.ts create mode 100644 server/icsGenerator.ts create mode 100644 todo.md diff --git a/.gitkeep b/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/.manus/db/db-query-1763414434598.json b/.manus/db/db-query-1763414434598.json new file mode 100644 index 0000000..138c8d8 --- /dev/null +++ b/.manus/db/db-query-1763414434598.json @@ -0,0 +1,18 @@ +{ + "query": "SELECT id, userId, LEFT(token, 20) as token_preview, expiresAt, used, createdAt FROM passwordResetTokens ORDER BY createdAt DESC LIMIT 5;", + "command": "mysql --batch --raw --column-names --default-character-set=utf8mb4 --host gateway02.us-east-1.prod.aws.tidbcloud.com --port 4000 --user 4CrrYuB5tme73Qo.root --database 7PAT67UmWoxv8vwp8Bbcv6 --execute SELECT id, userId, LEFT(token, 20) as token_preview, expiresAt, used, createdAt FROM passwordResetTokens ORDER BY createdAt DESC LIMIT 5;", + "rows": [ + { + "id": "1", + "userId": "1", + "token_preview": "e6626bd6ba7eabccd094", + "expiresAt": "2025-11-18 21:19:50", + "used": "0", + "createdAt": "2025-11-17 21:19:50" + } + ], + "messages": [], + "stdout": "id\tuserId\ttoken_preview\texpiresAt\tused\tcreatedAt\n1\t1\te6626bd6ba7eabccd094\t2025-11-18 21:19:50\t0\t2025-11-17 21:19:50\n", + "stderr": "", + "execution_time_ms": 901 +} \ No newline at end of file diff --git a/.manus/db/db-query-1763417332805.json b/.manus/db/db-query-1763417332805.json new file mode 100644 index 0000000..cb7dc39 --- /dev/null +++ b/.manus/db/db-query-1763417332805.json @@ -0,0 +1,9 @@ +{ + "query": "SELECT * FROM emailTemplates;", + "command": "mysql --batch --raw --column-names --default-character-set=utf8mb4 --host gateway02.us-east-1.prod.aws.tidbcloud.com --port 4000 --user 4CrrYuB5tme73Qo.root --database 7PAT67UmWoxv8vwp8Bbcv6 --execute SELECT * FROM emailTemplates;", + "rows": [], + "messages": [], + "stdout": "", + "stderr": "", + "execution_time_ms": 57 +} \ No newline at end of file diff --git a/.manus/db/db-query-1763417422542.json b/.manus/db/db-query-1763417422542.json new file mode 100644 index 0000000..19d7239 --- /dev/null +++ b/.manus/db/db-query-1763417422542.json @@ -0,0 +1,9 @@ +{ + "query": "INSERT INTO emailTemplates (type, name, logoUrl, primaryColor, headerBgColor, headerTextColor, headerTitle, footerText, active) VALUES \n('inscription', 'Confirmation d''inscription', NULL, '#2563eb', '#2563eb', '#ffffff', 'Formation Manager Itinova', 'Cet email a été envoyé automatiquement par le système de gestion des formations Itinova. Pour toute question, veuillez contacter le service RH.', 1),\n('teaser', 'Email teaser', NULL, '#2563eb', '#2563eb', '#ffffff', 'Formation Manager Itinova', 'Cet email a été envoyé automatiquement par le système de gestion des formations Itinova. Pour toute question, veuillez contacter le service RH.', 1),\n('rappel', 'Rappel J-7', NULL, '#2563eb', '#2563eb', '#ffffff', 'Formation Manager Itinova', 'Cet email a été envoyé automatiquement par le système de gestion des formations Itinova. Pour toute question, veuillez contacter le service RH.', 1),\n('reset_password', 'Réinitialisation de mot de passe', NULL, '#2563eb', '#2563eb', '#ffffff', 'Formation Manager Itinova', 'Cet email a été envoyé automatiquement par le système de gestion des formations Itinova. Pour toute question, veuillez contacter le service RH.', 1);", + "command": "mysql --batch --raw --column-names --default-character-set=utf8mb4 --host gateway02.us-east-1.prod.aws.tidbcloud.com --port 4000 --user 4CrrYuB5tme73Qo.root --database 7PAT67UmWoxv8vwp8Bbcv6 --execute INSERT INTO emailTemplates (type, name, logoUrl, primaryColor, headerBgColor, headerTextColor, headerTitle, footerText, active) VALUES \n('inscription', 'Confirmation d''inscription', NULL, '#2563eb', '#2563eb', '#ffffff', 'Formation Manager Itinova', 'Cet email a été envoyé automatiquement par le système de gestion des formations Itinova. Pour toute question, veuillez contacter le service RH.', 1),\n('teaser', 'Email teaser', NULL, '#2563eb', '#2563eb', '#ffffff', 'Formation Manager Itinova', 'Cet email a été envoyé automatiquement par le système de gestion des formations Itinova. Pour toute question, veuillez contacter le service RH.', 1),\n('rappel', 'Rappel J-7', NULL, '#2563eb', '#2563eb', '#ffffff', 'Formation Manager Itinova', 'Cet email a été envoyé automatiquement par le système de gestion des formations Itinova. Pour toute question, veuillez contacter le service RH.', 1),\n('reset_password', 'Réinitialisation de mot de passe', NULL, '#2563eb', '#2563eb', '#ffffff', 'Formation Manager Itinova', 'Cet email a été envoyé automatiquement par le système de gestion des formations Itinova. Pour toute question, veuillez contacter le service RH.', 1);", + "rows": [], + "messages": [], + "stdout": "", + "stderr": "", + "execution_time_ms": 64 +} \ No newline at end of file diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md new file mode 100644 index 0000000..4afb8fb --- /dev/null +++ b/DOCUMENTATION.md @@ -0,0 +1,285 @@ +# Documentation - Gestion des Formations Manager Itinova + +## Vue d'ensemble + +L'application **Gestion des Formations Manager Itinova** est une solution complète développée pour gérer l'organisation interne des formations destinées à environ 150 apprenants entre février et octobre 2026, puis 3 sessions par an. Elle permet à la DRH (assistante formation, responsable Développement RH) et à la Directrice Qualité et Transformation de gérer efficacement les inscriptions, les sessions, et les communications automatisées. + +## Fonctionnalités principales + +### 1. Gestion des formations + +L'application permet de créer et gérer plusieurs formations en parallèle. Chaque formation dispose d'un lien unique d'inscription qui peut être partagé avec les apprenants. + +**Caractéristiques :** +- Création de formations avec nom, description et lien unique personnalisé +- Activation/désactivation des formations +- Génération automatique de liens d'inscription distincts pour chaque formation + +### 2. Gestion des sessions + +Les sessions sont les instances concrètes d'une formation avec des dates, un lieu et une capacité définis. + +**Caractéristiques :** +- Création de sessions avec dates de début et fin +- Définition du lieu de formation +- Capacité maximale fixée à 12 participants par session +- Date de blocage automatique à J-15 avant le début de la session +- Statuts de session : ouverte, bloquée, terminée + +### 3. Gestion des apprenants + +L'application maintient une base de données des apprenants avec leurs informations essentielles. + +**Informations collectées :** +- Nom et prénom +- Email professionnel +- Code établissement + +### 4. Système d'inscription + +Les apprenants peuvent s'inscrire via un lien unique propre à chaque formation. Le système applique automatiquement toutes les contraintes définies. + +**Validations automatiques :** +- Vérification de la capacité maximale (12 participants) +- Prévention des doubles inscriptions +- Blocage des inscriptions et désinscriptions à J-15 +- Gestion automatique de la liste d'attente si la session est complète + +### 5. Invitations et communications automatiques + +Dès qu'un apprenant s'inscrit, le système génère et envoie automatiquement une invitation Outlook avec tous les détails de la formation. + +**Fonctionnalités d'envoi :** +- **Email de confirmation** : envoyé immédiatement après l'inscription avec invitation Outlook (.ics) en pièce jointe +- **Invitation Outlook** : fichier .ics avec statut "occupé" incluant le lieu de formation +- **Email teaser** : peut être envoyé manuellement par les administrateurs à tous les inscrits d'une session +- **Email de rappel J-7** : peut être envoyé manuellement 7 jours avant la formation + +### 6. Exports et feuilles de présence + +Les administrateurs peuvent exporter les listes d'inscrits dans différents formats pour faciliter la gestion administrative. + +**Formats disponibles :** +- **Export Excel** : liste complète des inscrits avec toutes leurs informations +- **Export PDF** : liste des inscrits au format PDF +- **Feuille de présence PDF** : document prêt à imprimer avec nom, prénom, code établissement et colonne signature + +## Guide d'utilisation + +### Pour les administrateurs + +#### Connexion + +1. Accédez à l'application via l'URL fournie +2. Cliquez sur "Se connecter" +3. Utilisez vos identifiants Manus OAuth +4. Vous serez redirigé vers le tableau de bord administrateur + +#### Créer une formation + +1. Dans le menu latéral, cliquez sur **Formations** +2. Cliquez sur le bouton **Nouvelle formation** +3. Remplissez les informations : + - Nom de la formation (ex: "Manager Itinova") + - Description (optionnelle) + - Lien unique (ex: "manager-itinova-2026") + - Cochez "Formation active" pour la rendre disponible +4. Cliquez sur **Créer** +5. Le lien d'inscription sera généré automatiquement : `/inscription/[votre-lien-unique]` + +#### Créer une session + +1. Dans le menu latéral, cliquez sur **Sessions** +2. Cliquez sur le bouton **Nouvelle session** +3. Remplissez les informations : + - Sélectionnez la formation associée + - Nom de la session (ex: "Session 1 - Février 2026") + - Date et heure de début + - Date et heure de fin + - Lieu de la formation (sera inclus dans l'invitation Outlook) + - Capacité maximale (par défaut 12) + - Date de blocage (J-15 avant le début) + - Statut (ouverte, bloquée, terminée) +4. Cliquez sur **Créer** + +#### Gérer les inscriptions + +1. Dans le menu **Sessions**, cliquez sur l'icône **Utilisateurs** à côté d'une session +2. Vous verrez : + - Le nombre de places confirmées sur la capacité totale + - Le nombre de personnes en liste d'attente + - La liste complète des inscrits avec leurs informations +3. Actions disponibles : + - Modifier le statut d'une inscription (confirmée, liste d'attente, annulée) + - Exporter la liste en Excel ou PDF + - Générer une feuille de présence + - Envoyer des emails groupés (teaser ou rappel J-7) + +#### Envoyer des emails groupés + +1. Accédez à la page des inscrits d'une session +2. Cliquez sur **Envoyer teaser** pour envoyer un email d'introduction à tous les inscrits confirmés +3. Cliquez sur **Envoyer rappel J-7** pour envoyer un rappel 7 jours avant la formation +4. Un message de confirmation indiquera le nombre d'emails envoyés avec succès + +#### Exporter les données + +1. Accédez à la page des inscrits d'une session +2. Cliquez sur : + - **Exporter Excel** : télécharge un fichier .xlsx avec toutes les informations + - **Exporter PDF** : télécharge un fichier PDF avec la liste des inscrits + - **Feuille de présence** : télécharge un PDF prêt à imprimer pour la signature des participants + +### Pour les apprenants + +#### S'inscrire à une formation + +1. Cliquez sur le lien d'inscription fourni par la DRH (ex: `https://[domaine]/inscription/manager-itinova-2026`) +2. Consultez les sessions disponibles avec : + - Les dates et horaires + - Le lieu de formation + - Le nombre de places disponibles + - Le statut (disponible, complète, fermée) +3. Sélectionnez la session souhaitée en cliquant dessus +4. Remplissez vos informations : + - Nom + - Prénom + - Email professionnel + - Code établissement +5. Cliquez sur **Confirmer mon inscription** +6. Vous recevrez immédiatement : + - Un email de confirmation + - Une invitation Outlook (.ics) à ajouter à votre calendrier + +#### Que se passe-t-il après l'inscription ? + +- **Si votre inscription est confirmée** : vous recevrez une invitation Outlook qui bloquera automatiquement votre agenda avec le statut "occupé" +- **Si la session est complète** : vous serez placé en liste d'attente et contacté si une place se libère +- **15 jours avant la formation** : les inscriptions et désinscriptions seront bloquées +- **7 jours avant la formation** : vous recevrez un email de rappel avec tous les détails pratiques + +## Gestion des rôles et permissions + +L'application utilise un système de rôles pour contrôler l'accès aux fonctionnalités : + +### Rôle Administrateur + +Accordé aux membres de la DRH (assistante formation, responsable Développement RH, Directrice Qualité et Transformation, stagiaire). + +**Permissions :** +- Accès complet au tableau de bord administrateur +- Création, modification et suppression de formations +- Création, modification et suppression de sessions +- Gestion des apprenants +- Consultation et modification des inscriptions +- Envoi d'emails groupés +- Export des données + +### Rôle Utilisateur + +Les apprenants qui s'inscrivent via le lien public. + +**Permissions :** +- Accès au formulaire d'inscription public +- Consultation des sessions disponibles +- Inscription à une session (dans les limites définies) + +## Contraintes et règles métier + +### Capacité des sessions + +Chaque session est limitée à **12 participants maximum**. Au-delà, les apprenants sont automatiquement placés en liste d'attente. + +### Blocage à J-15 + +Les inscriptions et désinscriptions sont automatiquement bloquées **15 jours avant le début de la session**. Cette règle garantit la stabilité des groupes et évite les désistements de dernière minute. + +### Prévention des doubles inscriptions + +Le système empêche un apprenant de s'inscrire plusieurs fois à la même session. Si l'apprenant existe déjà dans la base de données (même email), ses informations sont réutilisées. + +### Statuts des inscriptions + +- **Confirmée** : l'apprenant a une place garantie dans la session +- **Liste d'attente** : la session est complète, l'apprenant sera contacté si une place se libère +- **Annulée** : l'inscription a été annulée par l'apprenant ou l'administrateur + +## Architecture technique + +### Technologies utilisées + +L'application est construite avec une architecture moderne full-stack : + +**Frontend :** +- React 19 avec TypeScript +- Tailwind CSS 4 pour le design +- tRPC pour la communication type-safe avec le backend +- Wouter pour le routage +- shadcn/ui pour les composants d'interface + +**Backend :** +- Node.js avec Express +- tRPC pour les API type-safe +- Drizzle ORM pour l'accès à la base de données +- MySQL/TiDB comme base de données + +**Fonctionnalités :** +- Génération de fichiers ICS (iCalendar) pour Outlook +- Export Excel avec xlsx +- Export PDF avec jsPDF +- Système d'emails automatisés + +### Base de données + +Le schéma de base de données comprend 5 tables principales : + +1. **users** : utilisateurs authentifiés (administrateurs) +2. **formations** : catalogue des formations +3. **sessions** : instances de formations avec dates et lieux +4. **apprenants** : base de données des participants +5. **inscriptions** : relation entre apprenants et sessions avec statuts + +## Confidentialité et sécurité + +L'application respecte les principes de confidentialité des données : + +- **Authentification sécurisée** : via Manus OAuth pour les administrateurs +- **Contrôle d'accès** : seuls les administrateurs peuvent accéder aux données sensibles +- **Protection des données** : les informations personnelles des apprenants sont stockées de manière sécurisée +- **Emails professionnels** : seuls les emails professionnels sont collectés + +## Support et maintenance + +### Contact + +Pour toute question ou problème technique, contactez le service RH ou l'équipe de développement. + +### Évolutions futures + +L'application est conçue pour être évolutive. Des fonctionnalités supplémentaires peuvent être ajoutées selon les besoins : + +- Intégration avec un vrai service SMTP pour l'envoi d'emails +- Tableau de bord analytique avec statistiques de participation +- Système de notation et d'évaluation post-formation +- Gestion des formateurs et de leurs disponibilités +- Notifications push pour les rappels + +## Notes importantes + +### Emails simulés + +Dans la version actuelle, les emails sont **simulés** et affichés dans les logs du serveur. Pour une utilisation en production, il est nécessaire d'intégrer un service SMTP réel (comme SendGrid, Mailgun, ou un serveur SMTP d'entreprise). + +### Personnalisation + +Les templates d'emails peuvent être personnalisés dans le fichier `server/emailService.ts` pour correspondre à la charte graphique de l'entreprise. + +### Sauvegarde des données + +Il est recommandé de mettre en place une stratégie de sauvegarde régulière de la base de données pour prévenir toute perte de données. + +--- + +**Version de la documentation :** 1.0 +**Date de dernière mise à jour :** 2026-01-17 +**Auteur :** Manus AI diff --git a/EMAIL_SETUP.md b/EMAIL_SETUP.md new file mode 100644 index 0000000..bc8a0ef --- /dev/null +++ b/EMAIL_SETUP.md @@ -0,0 +1,109 @@ +# Configuration de l'envoi d'emails + +## État actuel + +Par défaut, l'application **simule** l'envoi d'emails en créant des notifications pour le propriétaire du projet. Les emails ne sont pas réellement envoyés aux utilisateurs finaux. + +## Configuration de l'envoi réel d'emails + +Pour activer l'envoi réel d'emails, vous devez configurer un service d'envoi d'emails externe. L'application supporte **Resend**, un service moderne et simple d'utilisation. + +### Étape 1 : Créer un compte Resend + +1. Allez sur [https://resend.com](https://resend.com) +2. Créez un compte gratuit (100 emails/jour inclus) +3. Vérifiez votre adresse email + +### Étape 2 : Obtenir une clé API + +1. Dans le tableau de bord Resend, allez dans **API Keys** +2. Cliquez sur **Create API Key** +3. Donnez un nom à votre clé (ex: "Manager Itinova Production") +4. Copiez la clé API générée (elle commence par `re_`) + +### Étape 3 : Configurer votre domaine (optionnel mais recommandé) + +Pour envoyer des emails depuis votre propre domaine (ex: `noreply@itinova.org`) : + +1. Dans Resend, allez dans **Domains** +2. Cliquez sur **Add Domain** +3. Entrez votre domaine (ex: `itinova.org`) +4. Suivez les instructions pour ajouter les enregistrements DNS +5. Attendez la vérification (quelques minutes à quelques heures) + +**Note:** Sans domaine vérifié, les emails seront envoyés depuis `noreply@manusvm.computer` + +### Étape 4 : Ajouter les secrets dans Manus + +1. Ouvrez l'interface de gestion de votre projet Manus +2. Allez dans **Settings** → **Secrets** +3. Ajoutez les secrets suivants : + +| Nom du secret | Valeur | Description | +|---------------|--------|-------------| +| `RESEND_API_KEY` | `re_xxxxx...` | Votre clé API Resend | +| `RESEND_FROM_EMAIL` | `noreply@itinova.org` | Adresse email d'envoi (doit être vérifiée dans Resend) | + +4. Sauvegardez les modifications +5. Redémarrez l'application (elle détectera automatiquement la nouvelle configuration) + +### Étape 5 : Tester l'envoi + +1. Allez dans **Utilisateurs** +2. Cliquez sur le bouton de réinitialisation de mot de passe pour un utilisateur +3. Vérifiez que l'email a bien été reçu + +## Vérification de la configuration + +Vous pouvez vérifier si l'envoi d'emails réel est activé en consultant les logs du serveur : + +- **Emails simulés** : `[Email] RESEND_API_KEY non configurée, email simulé` +- **Emails réels** : `[Email] Email envoyé via Resend: ` + +## Alternatives à Resend + +Si vous préférez utiliser un autre service d'envoi d'emails, vous pouvez modifier le fichier `server/_core/emailSender.ts` pour intégrer : + +- **SendGrid** : Service populaire avec 100 emails/jour gratuits +- **Mailgun** : Service robuste avec 5000 emails/mois gratuits +- **AWS SES** : Service Amazon très économique pour gros volumes +- **Votre propre serveur SMTP** : Configuration personnalisée + +## Limites et quotas + +### Plan gratuit Resend +- 100 emails par jour +- 3000 emails par mois +- Support email uniquement + +### Plans payants Resend +- À partir de 20$/mois pour 50 000 emails +- Support prioritaire +- Webhooks et analytics avancés + +## Dépannage + +### Les emails n'arrivent pas + +1. Vérifiez que `RESEND_API_KEY` est correctement configurée +2. Vérifiez que `RESEND_FROM_EMAIL` utilise un domaine vérifié +3. Consultez les logs du serveur pour voir les erreurs +4. Vérifiez le dossier spam/courrier indésirable +5. Consultez le tableau de bord Resend pour voir l'état des envois + +### Erreur "Domain not verified" + +Vous devez vérifier votre domaine dans Resend en ajoutant les enregistrements DNS requis. + +### Quota dépassé + +Si vous dépassez le quota gratuit, vous devrez : +- Attendre le lendemain (quota journalier) +- Passer à un plan payant +- Utiliser un autre service d'envoi + +## Support + +Pour toute question sur la configuration de l'envoi d'emails, consultez : +- [Documentation Resend](https://resend.com/docs) +- [API Reference Resend](https://resend.com/docs/api-reference) diff --git a/client/src/App.tsx b/client/src/App.tsx index 5c7a610..eeb8c86 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -5,12 +5,31 @@ import { Route, Switch } from "wouter"; import ErrorBoundary from "./components/ErrorBoundary"; import { ThemeProvider } from "./contexts/ThemeContext"; import Home from "./pages/Home"; +import AdminRapportPublicCible from "./pages/AdminRapportPublicCible"; +import Admin from "./pages/Admin"; +import AdminFormations from "./pages/AdminFormations"; +import AdminSequences from "./pages/AdminSequences"; +import AdminApprenants from "./pages/AdminApprenants"; +import AdminSequenceInscrits from "./pages/AdminSequenceInscrits"; +import AdminUsers from "./pages/AdminUsers"; +import AdminEmailTemplates from "./pages/AdminEmailTemplates"; +import AdminEmailConfig from "./pages/AdminEmailConfig"; +import Inscription from "./pages/Inscription"; function Router() { - // make sure to consider if you need authentication for certain routes return ( + + + + + + + + + + {/* Final fallback route */} @@ -18,18 +37,10 @@ function Router() { ); } -// NOTE: About Theme -// - First choose a default theme according to your design style (dark or light bg), than change color palette in index.css -// to keep consistent foreground/background color across components -// - If you want to make theme switchable, pass `switchable` ThemeProvider and use `useTheme` hook - function App() { return ( - + diff --git a/client/src/components/DashboardLayout.tsx b/client/src/components/DashboardLayout.tsx index 11ed6d0..c88a2a6 100644 --- a/client/src/components/DashboardLayout.tsx +++ b/client/src/components/DashboardLayout.tsx @@ -21,15 +21,21 @@ import { } from "@/components/ui/sidebar"; import { APP_LOGO, APP_TITLE, getLoginUrl } from "@/const"; import { useIsMobile } from "@/hooks/useMobile"; -import { LayoutDashboard, LogOut, PanelLeft, Users } from "lucide-react"; +import { LayoutDashboard, LogOut, PanelLeft, Users, GraduationCap, Calendar, BarChart3, UserCog, Mail, Settings } from "lucide-react"; import { CSSProperties, useEffect, useRef, useState } from "react"; import { useLocation } from "wouter"; import { DashboardLayoutSkeleton } from './DashboardLayoutSkeleton'; import { Button } from "./ui/button"; const menuItems = [ - { icon: LayoutDashboard, label: "Page 1", path: "/" }, - { icon: Users, label: "Page 2", path: "/some-path" }, + { icon: LayoutDashboard, label: "Tableau de bord", path: "/admin" }, + { icon: GraduationCap, label: "Formations", path: "/admin/formations" }, + { icon: Calendar, label: "Séquences", path: "/admin/sequences" }, + { icon: Users, label: "Apprenants", path: "/admin/apprenants" }, + { icon: UserCog, label: "Utilisateurs", path: "/admin/users" }, + { icon: Mail, label: "Templates d'emails", path: "/admin/email-templates" }, + { icon: Settings, label: "Configuration SMTP", path: "/admin/email-config" }, + { icon: BarChart3, label: "Rapport Public Cible", path: "/admin/rapport-public-cible" }, ]; const SIDEBAR_WIDTH_KEY = "sidebar-width"; @@ -73,7 +79,7 @@ export default function DashboardLayout({

{APP_TITLE}

- Please sign in to continue + Veuillez vous connecter pour continuer

@@ -84,7 +90,7 @@ export default function DashboardLayout({ size="lg" className="w-full shadow-lg hover:shadow-xl transition-all" > - Sign in + Se connecter @@ -255,7 +261,7 @@ function DashboardLayoutContent({ className="cursor-pointer text-destructive focus:text-destructive" > - Sign out + Déconnexion diff --git a/client/src/pages/Admin.tsx b/client/src/pages/Admin.tsx new file mode 100644 index 0000000..614a545 --- /dev/null +++ b/client/src/pages/Admin.tsx @@ -0,0 +1,154 @@ +import DashboardLayout from "@/components/DashboardLayout"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { trpc } from "@/lib/trpc"; +import { GraduationCap, Calendar, Users, CheckCircle, PieChart as PieChartIcon } from "lucide-react"; +import { Skeleton } from "@/components/ui/skeleton"; +import { PieChart, Pie, Cell, ResponsiveContainer, Legend, Tooltip } from "recharts"; +import { useMemo } from "react"; + +export default function Admin() { + const { data: formations, isLoading: loadingFormations } = trpc.formations.list.useQuery(); + const { data: sequences, isLoading: loadingSessions } = trpc.sequences.list.useQuery(); + const { data: apprenants, isLoading: loadingApprenants } = trpc.apprenants.list.useQuery(); + + // Statistiques par fonction + const statsFonction = useMemo(() => { + if (!apprenants) return []; + + const directeurs = apprenants.filter(a => a.fonction === "directeur").length; + const chefsService = apprenants.filter(a => a.fonction === "chef_service").length; + const autres = apprenants.filter(a => a.fonction === "autre").length; + + return [ + { name: "Directeurs", value: directeurs, color: "#3b82f6" }, + { name: "Chefs de service", value: chefsService, color: "#10b981" }, + { name: "Autres", value: autres, color: "#f59e0b" }, + ].filter(item => item.value > 0); + }, [apprenants]); + + const stats = [ + { + title: "Formations actives", + value: formations?.filter(f => f.actif).length || 0, + icon: , + loading: loadingFormations, + }, + { + title: "Séquences ouvertes", + value: sequences?.filter(s => s.statut === "ouverte").length || 0, + icon: , + loading: loadingSessions, + }, + { + title: "Apprenants inscrits", + value: apprenants?.length || 0, + icon: , + loading: loadingApprenants, + }, + { + title: "Séquences terminées", + value: sequences?.filter(s => s.statut === "terminee").length || 0, + icon: , + loading: loadingSessions, + }, + ]; + + return ( + +
+
+

Tableau de bord

+

+ Gestion des formations Manager Itinova +

+
+ +
+ {stats.map((stat, index) => ( + + + + {stat.title} + + {stat.icon} + + + {stat.loading ? ( + + ) : ( +
{stat.value}
+ )} +
+
+ ))} +
+ + {/* Graphique de répartition par fonction */} + {statsFonction.length > 0 && ( + + + + + Répartition des apprenants par fonction + + + Distribution des {apprenants?.length || 0} apprenants selon leur fonction + + + + {loadingApprenants ? ( +
+ +
+ ) : ( +
+ + + `${name}: ${(percent * 100).toFixed(0)}%`} + outerRadius={100} + fill="#8884d8" + dataKey="value" + > + {statsFonction.map((entry, index) => ( + + ))} + + + + + +
+ )} +
+
+ )} + + + + Bienvenue dans l'espace d'administration + + Gérez vos formations, sequences et apprenants depuis le menu de navigation. + + + +
+

Fonctionnalités principales :

+
    +
  • Créer et gérer les formations avec liens d'inscription uniques
  • +
  • Organiser les sequences avec limitation à 12 participants
  • +
  • Gérer les inscriptions avec blocage automatique à J-15
  • +
  • Exporter les listes d'inscrits en PDF ou Excel
  • +
  • Générer des invitations Outlook et envoyer des emails automatiques
  • +
+
+
+
+
+
+ ); +} diff --git a/client/src/pages/AdminApprenants.tsx b/client/src/pages/AdminApprenants.tsx new file mode 100644 index 0000000..7ac7ca8 --- /dev/null +++ b/client/src/pages/AdminApprenants.tsx @@ -0,0 +1,331 @@ +import DashboardLayout from "@/components/DashboardLayout"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { trpc } from "@/lib/trpc"; +import { Plus, Pencil, Trash2, Search, SlidersHorizontal } from "lucide-react"; +import { useState, useMemo } from "react"; +import { toast } from "sonner"; +import { Skeleton } from "@/components/ui/skeleton"; + +export default function AdminApprenants() { + const [open, setOpen] = useState(false); + const [editingId, setEditingId] = useState(null); + const [searchTerm, setSearchTerm] = useState(""); + const [filterFonction, setFilterFonction] = useState("all"); + const [formData, setFormData] = useState({ + nom: "", + prenom: "", + email: "", + codeEtablissement: "", + fonction: "autre" as "directeur" | "chef_service" | "autre", + }); + + const utils = trpc.useUtils(); + const { data: apprenants, isLoading } = trpc.apprenants.list.useQuery(); + + const createMutation = trpc.apprenants.create.useMutation({ + onSuccess: () => { + utils.apprenants.list.invalidate(); + toast.success("Apprenant créé avec succès"); + setOpen(false); + resetForm(); + }, + onError: (error) => { + toast.error("Erreur lors de la création : " + error.message); + }, + }); + + const updateMutation = trpc.apprenants.update.useMutation({ + onSuccess: () => { + utils.apprenants.list.invalidate(); + toast.success("Apprenant mis à jour avec succès"); + setOpen(false); + resetForm(); + }, + onError: (error) => { + toast.error("Erreur lors de la mise à jour : " + error.message); + }, + }); + + const deleteMutation = trpc.apprenants.delete.useMutation({ + onSuccess: () => { + utils.apprenants.list.invalidate(); + toast.success("Apprenant supprimé avec succès"); + }, + onError: (error) => { + toast.error("Erreur lors de la suppression : " + error.message); + }, + }); + + const resetForm = () => { + setFormData({ nom: "", prenom: "", email: "", codeEtablissement: "", fonction: "autre" }); + setEditingId(null); + }; + + const handleEdit = (apprenant: any) => { + setFormData({ + nom: apprenant.nom, + prenom: apprenant.prenom, + email: apprenant.email, + codeEtablissement: apprenant.codeEtablissement, + fonction: apprenant.fonction || "autre", + }); + setEditingId(apprenant.id); + setOpen(true); + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (editingId) { + updateMutation.mutate({ id: editingId, ...formData }); + } else { + createMutation.mutate(formData); + } + }; + + const handleDelete = (id: number) => { + if (confirm("Êtes-vous sûr de vouloir supprimer cet apprenant ?")) { + deleteMutation.mutate({ id }); + } + }; + + // Filtrer les apprenants + const filteredApprenants = useMemo(() => { + if (!apprenants) return []; + + return apprenants.filter(apprenant => { + const matchesSearch = searchTerm === "" || + apprenant.nom.toLowerCase().includes(searchTerm.toLowerCase()) || + apprenant.prenom.toLowerCase().includes(searchTerm.toLowerCase()) || + apprenant.email.toLowerCase().includes(searchTerm.toLowerCase()) || + apprenant.codeEtablissement.toLowerCase().includes(searchTerm.toLowerCase()); + + const matchesFonction = filterFonction === "all" || apprenant.fonction === filterFonction; + + return matchesSearch && matchesFonction; + }); + }, [apprenants, searchTerm, filterFonction]); + + return ( + +
+
+
+

Apprenants

+

+ Gérez les apprenants et leurs informations +

+
+ { + setOpen(isOpen); + if (!isOpen) resetForm(); + }}> + + + + +
+ + + {editingId ? "Modifier l'apprenant" : "Nouvel apprenant"} + + + Ajoutez les informations de l'apprenant + + +
+
+
+ + setFormData({ ...formData, nom: e.target.value })} + placeholder="Dupont" + required + /> +
+
+ + setFormData({ ...formData, prenom: e.target.value })} + placeholder="Jean" + required + /> +
+
+
+ + setFormData({ ...formData, email: e.target.value })} + placeholder="jean.dupont@example.com" + required + /> +
+
+ + setFormData({ ...formData, codeEtablissement: e.target.value })} + placeholder="ETB001" + required + /> +
+
+ + +
+
+ + + + +
+
+
+
+ + {/* Filtres et recherche */} + + + + + Recherche + + + +
+
+ +
+ + setSearchTerm(e.target.value)} + className="pl-8" + /> +
+
+ +
+ + +
+
+ + {/* Compteur de résultats */} +
+ {filteredApprenants.length} apprenant(s) trouvé(s) +
+
+
+ + + + Liste des apprenants + + Gérez les apprenants inscrits aux formations + + + + {isLoading ? ( +
+ + + +
+ ) : filteredApprenants && filteredApprenants.length > 0 ? ( + + + + Nom + Prénom + Email + Code établissement + Fonction + Actions + + + + {filteredApprenants.map((apprenant) => ( + + {apprenant.nom} + {apprenant.prenom} + {apprenant.email} + {apprenant.codeEtablissement} + + {apprenant.fonction === "directeur" ? "Directeur" : + apprenant.fonction === "chef_service" ? "Chef de service" : "Autre"} + + +
+ + +
+
+
+ ))} +
+
+ ) : ( +
+ Aucun apprenant pour le moment. Créez-en un pour commencer. +
+ )} +
+
+
+
+ ); +} diff --git a/client/src/pages/AdminEmailConfig.tsx b/client/src/pages/AdminEmailConfig.tsx new file mode 100644 index 0000000..915f337 --- /dev/null +++ b/client/src/pages/AdminEmailConfig.tsx @@ -0,0 +1,323 @@ +import { useEffect, useState } from "react"; +import DashboardLayout from "@/components/DashboardLayout"; +import { trpc } from "@/lib/trpc"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Label } from "@/components/ui/label"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { toast } from "sonner"; +import { Switch } from "@/components/ui/switch"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { AlertCircle, CheckCircle2, Mail, Send } from "lucide-react"; + +export default function AdminEmailConfig() { + const { data: config, isLoading, refetch } = trpc.emailConfig.get.useQuery(); + const upsertMutation = trpc.emailConfig.upsert.useMutation(); + const testEmailMutation = trpc.emailConfig.testEmail.useMutation(); + + const [formData, setFormData] = useState({ + provider: "resend", + apiKey: "", + fromEmail: "", + fromName: "Formation Manager Itinova", + mode: "simulation" as "simulation" | "production", + domainVerified: false, + }); + + const [testEmail, setTestEmail] = useState(""); + + // Charger la configuration existante + useEffect(() => { + if (config) { + setFormData({ + provider: config.provider, + apiKey: config.apiKey || "", + fromEmail: config.fromEmail, + fromName: config.fromName, + mode: config.mode, + domainVerified: config.domainVerified, + }); + } + }, [config]); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + + upsertMutation.mutate(formData, { + onSuccess: () => { + toast.success("Configuration enregistrée avec succès"); + refetch(); + }, + onError: (error) => { + toast.error(`Erreur: ${error.message}`); + }, + }); + }; + + const handleTestEmail = () => { + if (!testEmail) { + toast.error("Veuillez saisir une adresse email"); + return; + } + + testEmailMutation.mutate( + { toEmail: testEmail }, + { + onSuccess: (data) => { + if (data.success) { + toast.success(data.message || "Email de test envoyé avec succès"); + } else { + toast.error(data.message || "Échec de l'envoi de l'email de test"); + } + }, + onError: (error) => { + toast.error(`Erreur: ${error.message}`); + }, + } + ); + }; + + if (isLoading) { + return ( + +
+

Chargement...

+
+
+ ); + } + + return ( + +
+
+

Configuration des emails

+

+ Configurez l'envoi d'emails via Resend et basculez entre mode simulation et production +

+
+ +
+ {/* Statut actuel */} + + + + + Statut actuel + + + +
+
+
+

Mode d'envoi

+

+ {formData.mode === "simulation" + ? "Les emails sont simulés et envoyés comme notifications" + : "Les emails sont envoyés via Resend"} +

+
+
+ {formData.mode === "production" ? "Production" : "Simulation"} +
+
+ + {formData.apiKey && ( + + + + Clé API Resend configurée + + + )} + + {!formData.apiKey && ( + + + + Aucune clé API configurée. Les emails seront simulés. + + + )} +
+
+
+ + {/* Formulaire de configuration */} + + + Configuration Resend + + Configurez votre compte Resend pour envoyer de vrais emails + + + +
+
+ + setFormData({ ...formData, apiKey: e.target.value })} + /> +

+ Obtenez votre clé API sur{" "} + + resend.com/api-keys + +

+
+ +
+ + setFormData({ ...formData, fromEmail: e.target.value })} + required + /> +

+ L'adresse email doit être vérifiée dans votre compte Resend +

+
+ +
+ + setFormData({ ...formData, fromName: e.target.value })} + required + /> +
+ +
+
+ +

+ Activer l'envoi réel d'emails (désactiver pour simuler) +

+
+ + setFormData({ ...formData, mode: checked ? "production" : "simulation" }) + } + /> +
+ +
+ +
+
+
+
+ + {/* Test d'envoi */} + + + + + Test d'envoi + + + Envoyez un email de test pour vérifier votre configuration + + + +
+
+ + setTestEmail(e.target.value)} + /> +
+ + + + {formData.mode === "simulation" && ( + + + + Mode simulation actif. L'email de test sera simulé et envoyé comme notification au propriétaire. + + + )} +
+
+
+ + {/* Documentation */} + + + Guide de configuration + + +
+
+

1. Créer un compte Resend

+

+ Inscrivez-vous gratuitement sur{" "} + + resend.com + {" "} + (100 emails/jour gratuits) +

+
+ +
+

2. Vérifier votre domaine

+

+ Ajoutez et vérifiez votre domaine dans les paramètres Resend pour pouvoir envoyer des emails +

+
+ +
+

3. Obtenir la clé API

+

+ Générez une clé API dans la section "API Keys" de votre compte Resend +

+
+ +
+

4. Configurer et tester

+

+ Saisissez vos informations ci-dessus, enregistrez, puis testez l'envoi avant de passer en mode production +

+
+
+
+
+
+
+
+ ); +} diff --git a/client/src/pages/AdminEmailTemplates.tsx b/client/src/pages/AdminEmailTemplates.tsx new file mode 100644 index 0000000..148ebc9 --- /dev/null +++ b/client/src/pages/AdminEmailTemplates.tsx @@ -0,0 +1,364 @@ +import { useState, useEffect } from "react"; +import { trpc } from "@/lib/trpc"; +import DashboardLayout from "@/components/DashboardLayout"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { toast } from "sonner"; +import { Mail, Eye, Save, RotateCcw } from "lucide-react"; + +export default function AdminEmailTemplates() { + const [selectedType, setSelectedType] = useState("inscription"); + const [formData, setFormData] = useState({ + type: "inscription", + name: "", + logoUrl: "", + primaryColor: "#2563eb", + headerBgColor: "#2563eb", + headerTextColor: "#ffffff", + headerTitle: "Formation Manager Itinova", + footerText: "", + active: true, + }); + + const { data: templates, refetch } = trpc.emailTemplates.list.useQuery(); + const { data: currentTemplate } = trpc.emailTemplates.getByType.useQuery( + { type: selectedType }, + { enabled: !!selectedType } + ); + + const upsertMutation = trpc.emailTemplates.upsert.useMutation({ + onSuccess: () => { + toast.success("Template enregistré avec succès"); + refetch(); + }, + onError: (error) => { + toast.error(`Erreur: ${error.message}`); + }, + }); + + const initMutation = trpc.emailTemplates.initializeDefaults.useMutation({ + onSuccess: () => { + toast.success("Templates par défaut initialisés"); + refetch(); + }, + onError: (error) => { + toast.error(`Erreur: ${error.message}`); + }, + }); + + // Charger le template sélectionné + useEffect(() => { + if (currentTemplate) { + setFormData({ + type: currentTemplate.type, + name: currentTemplate.name, + logoUrl: currentTemplate.logoUrl || "", + primaryColor: currentTemplate.primaryColor, + headerBgColor: currentTemplate.headerBgColor, + headerTextColor: currentTemplate.headerTextColor, + headerTitle: currentTemplate.headerTitle, + footerText: currentTemplate.footerText || "", + active: currentTemplate.active, + }); + } + }, [currentTemplate]); + + const handleSave = () => { + upsertMutation.mutate({ + ...formData, + logoUrl: formData.logoUrl || null, + footerText: formData.footerText || null, + }); + }; + + const handleReset = () => { + if (currentTemplate) { + setFormData({ + type: currentTemplate.type, + name: currentTemplate.name, + logoUrl: currentTemplate.logoUrl || "", + primaryColor: currentTemplate.primaryColor, + headerBgColor: currentTemplate.headerBgColor, + headerTextColor: currentTemplate.headerTextColor, + headerTitle: currentTemplate.headerTitle, + footerText: currentTemplate.footerText || "", + active: currentTemplate.active, + }); + } + }; + + const handleInitDefaults = () => { + if (confirm("Initialiser les templates par défaut ? Cela ne modifiera pas les templates existants.")) { + initMutation.mutate(); + } + }; + + // Générer la prévisualisation HTML + const generatePreview = () => { + return ` + + + + + + + +
+
+ ${formData.logoUrl ? `Logo` : ''} +

${formData.headerTitle}

+
+
+

Exemple de contenu

+

Bonjour,

+

Ceci est un exemple de prévisualisation de votre template d'email personnalisé.

+
+

Information importante

+

Ce bloc utilise la couleur principale que vous avez choisie.

+
+ +

Cordialement,
L'équipe Formation Manager Itinova

+
+ +
+ + + `.trim(); + }; + + const templateTypes = [ + { value: "inscription", label: "Confirmation d'inscription" }, + { value: "teaser", label: "Email teaser" }, + { value: "rappel", label: "Rappel J-7" }, + { value: "reset_password", label: "Réinitialisation de mot de passe" }, + ]; + + return ( + +
+

Templates d'emails

+

+ Personnalisez l'apparence de vos emails automatiques +

+
+ + {(!templates || templates.length === 0) && ( + + + + + Initialisation requise + + + Aucun template trouvé. Initialisez les templates par défaut pour commencer. + + + + + + + )} + +
+ {/* Éditeur */} + + + Éditeur de template + + Modifiez les paramètres du template sélectionné + + + +
+ + +
+ +
+ + setFormData({ ...formData, name: e.target.value })} + placeholder="Ex: Confirmation d'inscription" + /> +
+ +
+ + setFormData({ ...formData, logoUrl: e.target.value })} + placeholder="https://exemple.com/logo.png" + /> +
+ +
+
+ +
+ setFormData({ ...formData, headerBgColor: e.target.value })} + className="w-16 h-10 p-1" + /> + setFormData({ ...formData, headerBgColor: e.target.value })} + placeholder="#2563eb" + /> +
+
+ +
+ +
+ setFormData({ ...formData, headerTextColor: e.target.value })} + className="w-16 h-10 p-1" + /> + setFormData({ ...formData, headerTextColor: e.target.value })} + placeholder="#ffffff" + /> +
+
+
+ +
+ +
+ setFormData({ ...formData, primaryColor: e.target.value })} + className="w-16 h-10 p-1" + /> + setFormData({ ...formData, primaryColor: e.target.value })} + placeholder="#2563eb" + /> +
+

+ Utilisée pour les boutons et les accents +

+
+ +
+ + setFormData({ ...formData, headerTitle: e.target.value })} + placeholder="Formation Manager Itinova" + /> +
+ +
+ +