From 20dc80cd74ffef2380116865dc3420833e2727f4 Mon Sep 17 00:00:00 2001 From: Manus Sandbox Date: Mon, 17 Nov 2025 17:11:27 -0500 Subject: [PATCH] =?UTF-8?q?Checkpoint:=20Ajout=20d'une=20interface=20d'adm?= =?UTF-8?q?inistration=20compl=C3=A8te=20pour=20personnaliser=20les=20temp?= =?UTF-8?q?lates=20d'emails=20avec=20pr=C3=A9visualisation=20en=20temps=20?= =?UTF-8?q?r=C3=A9el,=20stockage=20en=20base=20de=20donn=C3=A9es=20et=20in?= =?UTF-8?q?t=C3=A9gration=20avec=20le=20service=20d'envoi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .manus/db/db-query-1763417332805.json | 9 + .manus/db/db-query-1763417422542.json | 9 + client/src/App.tsx | 2 + client/src/components/DashboardLayout.tsx | 3 +- client/src/pages/AdminEmailTemplates.tsx | 364 ++++++++++++ drizzle/0008_jittery_fenris.sql | 16 + drizzle/meta/0008_snapshot.json | 684 ++++++++++++++++++++++ drizzle/meta/_journal.json | 7 + drizzle/schema.ts | 30 + server/db.ts | 137 ++++- server/emailService.ts | 46 +- server/emailTemplateGenerator.ts | 161 +++++ server/routers.ts | 41 ++ todo.md | 11 + 14 files changed, 1480 insertions(+), 40 deletions(-) create mode 100644 .manus/db/db-query-1763417332805.json create mode 100644 .manus/db/db-query-1763417422542.json create mode 100644 client/src/pages/AdminEmailTemplates.tsx create mode 100644 drizzle/0008_jittery_fenris.sql create mode 100644 drizzle/meta/0008_snapshot.json create mode 100644 server/emailTemplateGenerator.ts 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/client/src/App.tsx b/client/src/App.tsx index e1d991d..8c7597e 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -12,6 +12,7 @@ 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 Inscription from "./pages/Inscription"; function Router() { @@ -26,6 +27,7 @@ function Router() { + {/* Final fallback route */} diff --git a/client/src/components/DashboardLayout.tsx b/client/src/components/DashboardLayout.tsx index 703803e..af6f5d9 100644 --- a/client/src/components/DashboardLayout.tsx +++ b/client/src/components/DashboardLayout.tsx @@ -21,7 +21,7 @@ import { } from "@/components/ui/sidebar"; import { APP_LOGO, APP_TITLE, getLoginUrl } from "@/const"; import { useIsMobile } from "@/hooks/useMobile"; -import { LayoutDashboard, LogOut, PanelLeft, Users, GraduationCap, Calendar, BarChart3, UserCog } from "lucide-react"; +import { LayoutDashboard, LogOut, PanelLeft, Users, GraduationCap, Calendar, BarChart3, UserCog, Mail } from "lucide-react"; import { CSSProperties, useEffect, useRef, useState } from "react"; import { useLocation } from "wouter"; import { DashboardLayoutSkeleton } from './DashboardLayoutSkeleton'; @@ -33,6 +33,7 @@ const menuItems = [ { 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: BarChart3, label: "Rapport Public Cible", path: "/admin/rapport-public-cible" }, ]; 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" + /> +
+ +
+ +