Checkpoint: Implémentation de trois fonctionnalités majeures :

1. Personnalisation des templates d'emails avec variables {{lieu}} et {{formateur}} (déjà disponible)
2. Aperçu avant envoi avec données réelles d'un apprenant (modal avec iframe)
3. Email de rappel J-1 avec bouton d'envoi et d'aperçu

Les templates personnalisés supportent maintenant toutes les variables disponibles.
L'aperçu permet de vérifier le rendu final avant l'envoi groupé.
Le rappel J-1 complète le système de communication avec les apprenants.
This commit is contained in:
Manus Sandbox
2025-11-30 13:33:32 -05:00
parent 9e1d574d2e
commit d73db2820c
5 changed files with 401 additions and 9 deletions

View File

@@ -4,13 +4,20 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
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 { ArrowLeft, Download, Mail, Search } from "lucide-react";
import { ArrowLeft, Download, Mail, Search, Eye } from "lucide-react";
import { useLocation, useRoute } from "wouter";
import { useState, useMemo } from "react";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Skeleton } from "@/components/ui/skeleton";
import { toast } from "sonner";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
export default function AdminSequenceInscrits() {
const [, setLocation] = useLocation();
@@ -41,11 +48,17 @@ export default function AdminSequenceInscrits() {
const [filterEtablissement, setFilterEtablissement] = useState<string>("all");
const [filterFonction, setFilterFonction] = useState<string>("all");
const [sortBy, setSortBy] = useState<"date" | "nom">("date");
const [showPreviewModal, setShowPreviewModal] = useState(false);
const [previewType, setPreviewType] = useState<"teaser" | "rappel">("teaser");
const utils = trpc.useUtils();
const { data: sequence, isLoading: loadingSequence } = trpc.sequences.getById.useQuery({ id: sequenceId });
const { data: inscriptions, isLoading: loadingInscriptions } = trpc.inscriptions.listBySequence.useQuery({ sequenceId });
const { data: formations } = trpc.formations.list.useQuery();
const { data: emailPreview, isLoading: loadingPreview } = trpc.inscriptions.emailPreview.useQuery(
{ sequenceId, type: previewType },
{ enabled: showPreviewModal }
);
const updateStatutMutation = trpc.inscriptions.updateStatut.useMutation({
onSuccess: () => {
@@ -143,8 +156,9 @@ export default function AdminSequenceInscrits() {
exportFeuilleMutation.mutate({ sequenceId });
};
const handleSendEmails = (type: "teaser" | "rappel") => {
if (confirm(`Êtes-vous sûr de vouloir envoyer les emails ${type === "teaser" ? "teaser" : "de rappel J-7"} à tous les inscrits confirmés ?`)) {
const handleSendEmails = (type: "teaser" | "rappel" | "rappel_j1") => {
const typeLabel = type === "teaser" ? "teaser" : type === "rappel" ? "de rappel J-7" : "de rappel J-1";
if (confirm(`Êtes-vous sûr de vouloir envoyer les emails ${typeLabel} à tous les inscrits confirmés ?`)) {
sendEmailsMutation.mutate({ sequenceId, type });
}
};
@@ -354,6 +368,17 @@ export default function AdminSequenceInscrits() {
</CardHeader>
<CardContent>
<div className="flex flex-wrap gap-2">
<Button
variant="outline"
onClick={() => {
setPreviewType("teaser");
setShowPreviewModal(true);
}}
disabled={nbConfirmes === 0}
>
<Eye className="h-4 w-4 mr-2" />
Aperçu teaser
</Button>
<Button
variant="outline"
onClick={() => handleSendEmails("teaser")}
@@ -362,6 +387,17 @@ export default function AdminSequenceInscrits() {
<Mail className="h-4 w-4 mr-2" />
Envoyer email teaser
</Button>
<Button
variant="outline"
onClick={() => {
setPreviewType("rappel");
setShowPreviewModal(true);
}}
disabled={nbConfirmes === 0}
>
<Eye className="h-4 w-4 mr-2" />
Aperçu rappel J-7
</Button>
<Button
variant="outline"
onClick={() => handleSendEmails("rappel")}
@@ -370,6 +406,25 @@ export default function AdminSequenceInscrits() {
<Mail className="h-4 w-4 mr-2" />
Envoyer rappel J-7
</Button>
<Button
variant="outline"
onClick={() => {
setPreviewType("rappel_j1");
setShowPreviewModal(true);
}}
disabled={nbConfirmes === 0}
>
<Eye className="h-4 w-4 mr-2" />
Aperçu rappel J-1
</Button>
<Button
variant="outline"
onClick={() => handleSendEmails("rappel_j1")}
disabled={sendEmailsMutation.isPending || nbConfirmes === 0}
>
<Mail className="h-4 w-4 mr-2" />
Envoyer rappel J-1
</Button>
<Button
variant="outline"
onClick={handleExportExcel}
@@ -556,6 +611,49 @@ export default function AdminSequenceInscrits() {
</CardContent>
</Card>
</div>
{/* Modal d'aperçu d'email */}
<Dialog open={showPreviewModal} onOpenChange={setShowPreviewModal}>
<DialogContent className="max-w-4xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>
Aperçu de l'email {previewType === "teaser" ? "teaser" : previewType === "rappel" ? "de rappel J-7" : "de rappel J-1"}
</DialogTitle>
<DialogDescription>
{loadingPreview ? (
"Chargement de l'aperçu..."
) : emailPreview ? (
`Destinataire : ${emailPreview.recipient.prenom} ${emailPreview.recipient.nom} (${emailPreview.recipient.email})`
) : (
"Erreur lors du chargement de l'aperçu"
)}
</DialogDescription>
</DialogHeader>
{loadingPreview ? (
<div className="flex justify-center p-8">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary"></div>
</div>
) : emailPreview ? (
<div className="space-y-4">
<div className="bg-muted p-3 rounded-md">
<p className="text-sm font-medium">Sujet :</p>
<p className="text-sm">{emailPreview.subject}</p>
</div>
<div className="border rounded-md p-4 bg-white">
<iframe
srcDoc={emailPreview.html}
className="w-full h-[500px] border-0"
title="Aperçu de l'email"
/>
</div>
</div>
) : (
<div className="text-center p-8 text-muted-foreground">
Impossible de charger l'aperçu. Vérifiez qu'il y a des apprenants confirmés.
</div>
)}
</DialogContent>
</Dialog>
</DashboardLayout>
);
}