Checkpoint: Ajout d'une interface d'administration complète pour personnaliser les templates d'emails avec prévisualisation en temps réel, stockage en base de données et intégration avec le service d'envoi
This commit is contained in:
364
client/src/pages/AdminEmailTemplates.tsx
Normal file
364
client/src/pages/AdminEmailTemplates.tsx
Normal file
@@ -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<string>("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 `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; margin: 0; padding: 20px; background-color: #f5f5f5; }
|
||||
.container { max-width: 600px; margin: 0 auto; background-color: white; }
|
||||
.header { background-color: ${formData.headerBgColor}; color: ${formData.headerTextColor}; padding: 30px 20px; text-align: center; }
|
||||
.header h1 { margin: 0; font-size: 24px; }
|
||||
${formData.logoUrl ? `.header img { max-width: 150px; margin-bottom: 15px; }` : ''}
|
||||
.content { background-color: #f9fafb; padding: 30px 20px; }
|
||||
.content h2 { color: ${formData.primaryColor}; margin-top: 0; }
|
||||
.footer { background-color: #f3f4f6; padding: 20px; text-align: center; font-size: 12px; color: #6b7280; }
|
||||
.button { display: inline-block; padding: 12px 24px; background-color: ${formData.primaryColor}; color: white; text-decoration: none; border-radius: 6px; margin: 10px 0; }
|
||||
.info-box { background-color: #dbeafe; border-left: 4px solid ${formData.primaryColor}; padding: 15px; margin: 15px 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
${formData.logoUrl ? `<img src="${formData.logoUrl}" alt="Logo" />` : ''}
|
||||
<h1>${formData.headerTitle}</h1>
|
||||
</div>
|
||||
<div class="content">
|
||||
<h2>Exemple de contenu</h2>
|
||||
<p>Bonjour,</p>
|
||||
<p>Ceci est un exemple de prévisualisation de votre template d'email personnalisé.</p>
|
||||
<div class="info-box">
|
||||
<p><strong>Information importante</strong></p>
|
||||
<p>Ce bloc utilise la couleur principale que vous avez choisie.</p>
|
||||
</div>
|
||||
<div style="text-align: center; margin: 20px 0;">
|
||||
<a href="#" class="button">Bouton d'action</a>
|
||||
</div>
|
||||
<p>Cordialement,<br/>L'équipe Formation Manager Itinova</p>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>${formData.footerText || 'Texte du pied de page'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`.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 (
|
||||
<DashboardLayout>
|
||||
<div className="mb-6">
|
||||
<h1 className="text-3xl font-bold mb-2">Templates d'emails</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Personnalisez l'apparence de vos emails automatiques
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{(!templates || templates.length === 0) && (
|
||||
<Card className="mb-6 border-yellow-200 bg-yellow-50">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Mail className="h-5 w-5" />
|
||||
Initialisation requise
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Aucun template trouvé. Initialisez les templates par défaut pour commencer.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Button onClick={handleInitDefaults} disabled={initMutation.isPending}>
|
||||
{initMutation.isPending ? "Initialisation..." : "Initialiser les templates par défaut"}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Éditeur */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Éditeur de template</CardTitle>
|
||||
<CardDescription>
|
||||
Modifiez les paramètres du template sélectionné
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="templateType">Type de template</Label>
|
||||
<Select
|
||||
value={selectedType}
|
||||
onValueChange={(value) => {
|
||||
setSelectedType(value);
|
||||
setFormData((prev) => ({ ...prev, type: value }));
|
||||
}}
|
||||
>
|
||||
<SelectTrigger id="templateType">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{templateTypes.map((type) => (
|
||||
<SelectItem key={type.value} value={type.value}>
|
||||
{type.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="name">Nom du template</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
placeholder="Ex: Confirmation d'inscription"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="logoUrl">URL du logo (optionnel)</Label>
|
||||
<Input
|
||||
id="logoUrl"
|
||||
value={formData.logoUrl}
|
||||
onChange={(e) => setFormData({ ...formData, logoUrl: e.target.value })}
|
||||
placeholder="https://exemple.com/logo.png"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="headerBgColor">Couleur fond en-tête</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
id="headerBgColor"
|
||||
type="color"
|
||||
value={formData.headerBgColor}
|
||||
onChange={(e) => setFormData({ ...formData, headerBgColor: e.target.value })}
|
||||
className="w-16 h-10 p-1"
|
||||
/>
|
||||
<Input
|
||||
value={formData.headerBgColor}
|
||||
onChange={(e) => setFormData({ ...formData, headerBgColor: e.target.value })}
|
||||
placeholder="#2563eb"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="headerTextColor">Couleur texte en-tête</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
id="headerTextColor"
|
||||
type="color"
|
||||
value={formData.headerTextColor}
|
||||
onChange={(e) => setFormData({ ...formData, headerTextColor: e.target.value })}
|
||||
className="w-16 h-10 p-1"
|
||||
/>
|
||||
<Input
|
||||
value={formData.headerTextColor}
|
||||
onChange={(e) => setFormData({ ...formData, headerTextColor: e.target.value })}
|
||||
placeholder="#ffffff"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="primaryColor">Couleur principale</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
id="primaryColor"
|
||||
type="color"
|
||||
value={formData.primaryColor}
|
||||
onChange={(e) => setFormData({ ...formData, primaryColor: e.target.value })}
|
||||
className="w-16 h-10 p-1"
|
||||
/>
|
||||
<Input
|
||||
value={formData.primaryColor}
|
||||
onChange={(e) => setFormData({ ...formData, primaryColor: e.target.value })}
|
||||
placeholder="#2563eb"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Utilisée pour les boutons et les accents
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="headerTitle">Titre de l'en-tête</Label>
|
||||
<Input
|
||||
id="headerTitle"
|
||||
value={formData.headerTitle}
|
||||
onChange={(e) => setFormData({ ...formData, headerTitle: e.target.value })}
|
||||
placeholder="Formation Manager Itinova"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="footerText">Texte du pied de page</Label>
|
||||
<Textarea
|
||||
id="footerText"
|
||||
value={formData.footerText}
|
||||
onChange={(e) => setFormData({ ...formData, footerText: e.target.value })}
|
||||
placeholder="Cet email a été envoyé automatiquement..."
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 pt-4">
|
||||
<Button onClick={handleSave} disabled={upsertMutation.isPending} className="flex-1">
|
||||
<Save className="h-4 w-4 mr-2" />
|
||||
{upsertMutation.isPending ? "Enregistrement..." : "Enregistrer"}
|
||||
</Button>
|
||||
<Button onClick={handleReset} variant="outline">
|
||||
<RotateCcw className="h-4 w-4 mr-2" />
|
||||
Réinitialiser
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Prévisualisation */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Eye className="h-5 w-5" />
|
||||
Prévisualisation en temps réel
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Aperçu de votre template avec les paramètres actuels
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="border rounded-lg overflow-hidden bg-gray-50">
|
||||
<iframe
|
||||
srcDoc={generatePreview()}
|
||||
className="w-full h-[600px] border-0"
|
||||
title="Prévisualisation du template"
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</DashboardLayout>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user