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:
@@ -4,13 +4,20 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
|
|||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { trpc } from "@/lib/trpc";
|
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 { useLocation, useRoute } from "wouter";
|
||||||
import { useState, useMemo } from "react";
|
import { useState, useMemo } from "react";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
|
||||||
export default function AdminSequenceInscrits() {
|
export default function AdminSequenceInscrits() {
|
||||||
const [, setLocation] = useLocation();
|
const [, setLocation] = useLocation();
|
||||||
@@ -41,11 +48,17 @@ export default function AdminSequenceInscrits() {
|
|||||||
const [filterEtablissement, setFilterEtablissement] = useState<string>("all");
|
const [filterEtablissement, setFilterEtablissement] = useState<string>("all");
|
||||||
const [filterFonction, setFilterFonction] = useState<string>("all");
|
const [filterFonction, setFilterFonction] = useState<string>("all");
|
||||||
const [sortBy, setSortBy] = useState<"date" | "nom">("date");
|
const [sortBy, setSortBy] = useState<"date" | "nom">("date");
|
||||||
|
const [showPreviewModal, setShowPreviewModal] = useState(false);
|
||||||
|
const [previewType, setPreviewType] = useState<"teaser" | "rappel">("teaser");
|
||||||
|
|
||||||
const utils = trpc.useUtils();
|
const utils = trpc.useUtils();
|
||||||
const { data: sequence, isLoading: loadingSequence } = trpc.sequences.getById.useQuery({ id: sequenceId });
|
const { data: sequence, isLoading: loadingSequence } = trpc.sequences.getById.useQuery({ id: sequenceId });
|
||||||
const { data: inscriptions, isLoading: loadingInscriptions } = trpc.inscriptions.listBySequence.useQuery({ sequenceId });
|
const { data: inscriptions, isLoading: loadingInscriptions } = trpc.inscriptions.listBySequence.useQuery({ sequenceId });
|
||||||
const { data: formations } = trpc.formations.list.useQuery();
|
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({
|
const updateStatutMutation = trpc.inscriptions.updateStatut.useMutation({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@@ -143,8 +156,9 @@ export default function AdminSequenceInscrits() {
|
|||||||
exportFeuilleMutation.mutate({ sequenceId });
|
exportFeuilleMutation.mutate({ sequenceId });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSendEmails = (type: "teaser" | "rappel") => {
|
const handleSendEmails = (type: "teaser" | "rappel" | "rappel_j1") => {
|
||||||
if (confirm(`Êtes-vous sûr de vouloir envoyer les emails ${type === "teaser" ? "teaser" : "de rappel J-7"} à tous les inscrits confirmés ?`)) {
|
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 });
|
sendEmailsMutation.mutate({ sequenceId, type });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -354,6 +368,17 @@ export default function AdminSequenceInscrits() {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="flex flex-wrap gap-2">
|
<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
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => handleSendEmails("teaser")}
|
onClick={() => handleSendEmails("teaser")}
|
||||||
@@ -362,6 +387,17 @@ export default function AdminSequenceInscrits() {
|
|||||||
<Mail className="h-4 w-4 mr-2" />
|
<Mail className="h-4 w-4 mr-2" />
|
||||||
Envoyer email teaser
|
Envoyer email teaser
|
||||||
</Button>
|
</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
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => handleSendEmails("rappel")}
|
onClick={() => handleSendEmails("rappel")}
|
||||||
@@ -370,6 +406,25 @@ export default function AdminSequenceInscrits() {
|
|||||||
<Mail className="h-4 w-4 mr-2" />
|
<Mail className="h-4 w-4 mr-2" />
|
||||||
Envoyer rappel J-7
|
Envoyer rappel J-7
|
||||||
</Button>
|
</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
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={handleExportExcel}
|
onClick={handleExportExcel}
|
||||||
@@ -556,6 +611,49 @@ export default function AdminSequenceInscrits() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</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>
|
</DashboardLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
176
server/emailPreview.ts
Normal file
176
server/emailPreview.ts
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
/**
|
||||||
|
* Génération d'aperçus d'emails avec données réelles
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as db from "./db";
|
||||||
|
import { generateEmailFromTemplate } from "./emailTemplateGenerator";
|
||||||
|
|
||||||
|
export interface EmailPreviewParams {
|
||||||
|
sequenceId: number;
|
||||||
|
type: 'teaser' | 'rappel' | 'rappel_j1';
|
||||||
|
apprenantId?: number; // Si non fourni, prendre le premier apprenant inscrit
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateEmailPreview(params: EmailPreviewParams): Promise<{
|
||||||
|
html: string;
|
||||||
|
subject: string;
|
||||||
|
recipient: {
|
||||||
|
email: string;
|
||||||
|
nom: string;
|
||||||
|
prenom: string;
|
||||||
|
};
|
||||||
|
}> {
|
||||||
|
// Récupérer la séquence
|
||||||
|
const sequence = await db.getSequenceById(params.sequenceId);
|
||||||
|
if (!sequence) {
|
||||||
|
throw new Error('Séquence non trouvée');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Récupérer la formation
|
||||||
|
const formation = await db.getFormationById(sequence.formationId);
|
||||||
|
if (!formation) {
|
||||||
|
throw new Error('Formation non trouvée');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Récupérer les dates
|
||||||
|
const dates = await db.getDatesBySequence(sequence.id);
|
||||||
|
if (dates.length === 0) {
|
||||||
|
throw new Error('Aucune date trouvée pour cette séquence');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Récupérer le formateur si disponible
|
||||||
|
let formateurNom: string | undefined;
|
||||||
|
if (sequence.formateurId) {
|
||||||
|
const formateur = await db.getFormateurById(sequence.formateurId);
|
||||||
|
formateurNom = formateur?.nom;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Récupérer un apprenant inscrit
|
||||||
|
const inscriptions = await db.getInscriptionsBySequence(params.sequenceId);
|
||||||
|
const confirmedInscriptions = inscriptions.filter(i => i.inscription.statut === 'confirmee' && i.apprenant);
|
||||||
|
|
||||||
|
if (confirmedInscriptions.length === 0) {
|
||||||
|
throw new Error('Aucun apprenant confirmé trouvé pour cette séquence');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utiliser l'apprenant spécifié ou le premier de la liste
|
||||||
|
let selectedInscription = confirmedInscriptions[0];
|
||||||
|
if (params.apprenantId) {
|
||||||
|
const found = confirmedInscriptions.find(i => i.apprenant!.id === params.apprenantId);
|
||||||
|
if (found) {
|
||||||
|
selectedInscription = found;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const apprenant = selectedInscription.apprenant!;
|
||||||
|
|
||||||
|
// Préparer les données pour l'email
|
||||||
|
const fonctionLabel = apprenant.fonction === 'directeur' ? 'Directeur' :
|
||||||
|
apprenant.fonction === 'chef_service' ? 'Chef de service' : '';
|
||||||
|
const salutation = fonctionLabel ? `${fonctionLabel} ${apprenant.prenom} ${apprenant.nom}` : apprenant.prenom;
|
||||||
|
|
||||||
|
const datesHTML = dates.map(date => `
|
||||||
|
<div class="date-item">
|
||||||
|
<strong>Date ${date.ordre} :</strong> ${new Date(date.dateDebut).toLocaleDateString('fr-FR', {
|
||||||
|
weekday: 'long',
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric'
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
// Générer le contenu selon le type
|
||||||
|
let content: string;
|
||||||
|
let subject: string;
|
||||||
|
|
||||||
|
if (params.type === 'teaser') {
|
||||||
|
content = `
|
||||||
|
<h2>Votre formation approche !</h2>
|
||||||
|
<p>Bonjour ${salutation},</p>
|
||||||
|
|
||||||
|
<p>Nous sommes ravis de vous accueillir prochainement pour la formation <strong>${formation.nom}</strong>.</p>
|
||||||
|
|
||||||
|
<div class="info-box">
|
||||||
|
<p><strong>Séquence :</strong> ${sequence.nom}</p>
|
||||||
|
<h4>Dates :</h4>
|
||||||
|
${datesHTML}
|
||||||
|
${sequence.lieu ? `<p><strong>Lieu :</strong> ${sequence.lieu}</p>` : ''}
|
||||||
|
${formateurNom ? `<p><strong>Formateur :</strong> ${formateurNom}</p>` : ''}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>Préparez-vous à vivre une expérience enrichissante qui vous permettra de développer vos compétences managériales.</p>
|
||||||
|
|
||||||
|
<p>À très bientôt !</p>
|
||||||
|
`;
|
||||||
|
subject = `Votre formation ${formation.nom} approche !`;
|
||||||
|
} else if (params.type === 'rappel') {
|
||||||
|
// Rappel J-7
|
||||||
|
content = `
|
||||||
|
<h2>Rappel : Votre formation commence bientôt !</h2>
|
||||||
|
<p>Bonjour ${salutation},</p>
|
||||||
|
|
||||||
|
<p>Nous vous rappelons que votre formation <strong>${formation.nom}</strong> commence dans une semaine.</p>
|
||||||
|
|
||||||
|
<div class="info-box">
|
||||||
|
<p><strong>Séquence :</strong> ${sequence.nom}</p>
|
||||||
|
<h4>Dates :</h4>
|
||||||
|
${datesHTML}
|
||||||
|
${sequence.lieu ? `<p><strong>Lieu :</strong> ${sequence.lieu}</p>` : ''}
|
||||||
|
${formateurNom ? `<p><strong>Formateur :</strong> ${formateurNom}</p>` : ''}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>N'oubliez pas d'apporter le matériel nécessaire et de vous présenter à l'heure indiquée.</p>
|
||||||
|
|
||||||
|
<p>À très bientôt !</p>
|
||||||
|
`;
|
||||||
|
subject = `Rappel J-7 : Formation ${formation.nom}`;
|
||||||
|
} else {
|
||||||
|
// Rappel J-1
|
||||||
|
content = `
|
||||||
|
<h2>Rappel : Votre formation commence demain !</h2>
|
||||||
|
<p>Bonjour ${salutation},</p>
|
||||||
|
|
||||||
|
<p>Nous vous rappelons que votre formation <strong>${formation.nom}</strong> commence <strong>demain</strong>.</p>
|
||||||
|
|
||||||
|
<div class="info-box">
|
||||||
|
<p><strong>Séquence :</strong> ${sequence.nom}</p>
|
||||||
|
<h4>Dates :</h4>
|
||||||
|
${datesHTML}
|
||||||
|
${sequence.lieu ? `<p><strong>Lieu :</strong> ${sequence.lieu}</p>` : ''}
|
||||||
|
${formateurNom ? `<p><strong>Formateur :</strong> ${formateurNom}</p>` : ''}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p><strong>Merci de vous présenter à l'heure indiquée.</strong></p>
|
||||||
|
<p>N'oubliez pas d'apporter le matériel nécessaire.</p>
|
||||||
|
|
||||||
|
<p>À demain !</p>
|
||||||
|
`;
|
||||||
|
subject = `Rappel J-1 : Formation ${formation.nom} - C'est demain !`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Préparer les variables
|
||||||
|
const variables = {
|
||||||
|
nomApprenant: apprenant.nom,
|
||||||
|
prenomApprenant: apprenant.prenom,
|
||||||
|
nomFormation: formation.nom,
|
||||||
|
nomSequence: sequence.nom,
|
||||||
|
dateDebut: new Date(dates[0].dateDebut).toLocaleDateString('fr-FR'),
|
||||||
|
dateFin: new Date(dates[dates.length - 1].dateFin).toLocaleDateString('fr-FR'),
|
||||||
|
lieu: sequence.lieu || '',
|
||||||
|
formateur: formateurNom || '',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Générer le HTML final avec le template
|
||||||
|
const html = await generateEmailFromTemplate(params.type === 'teaser' ? 'teaser' : 'rappel', content, variables);
|
||||||
|
|
||||||
|
return {
|
||||||
|
html,
|
||||||
|
subject,
|
||||||
|
recipient: {
|
||||||
|
email: apprenant.email,
|
||||||
|
nom: apprenant.nom,
|
||||||
|
prenom: apprenant.prenom,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -278,6 +278,81 @@ export async function sendRappelJ7Email(params: {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Envoie un email de rappel J-1 à un apprenant
|
||||||
|
*/
|
||||||
|
export async function sendRappelJ1Email(params: {
|
||||||
|
apprenantEmail: string;
|
||||||
|
apprenantNom: string;
|
||||||
|
apprenantPrenom: string;
|
||||||
|
apprenantFonction: string;
|
||||||
|
formationNom: string;
|
||||||
|
sequenceNom: string;
|
||||||
|
dates: Array<{ dateDebut: Date; dateFin: Date; ordre: number }>;
|
||||||
|
lieu?: string;
|
||||||
|
formateur?: string;
|
||||||
|
}): Promise<boolean> {
|
||||||
|
const fonctionLabel = params.apprenantFonction === 'directeur' ? 'Directeur' :
|
||||||
|
params.apprenantFonction === 'chef_service' ? 'Chef de service' : '';
|
||||||
|
const salutation = fonctionLabel ? `${fonctionLabel} ${params.apprenantPrenom} ${params.apprenantNom}` : params.apprenantPrenom;
|
||||||
|
|
||||||
|
const datesHTML = params.dates.map(date => `
|
||||||
|
<div class="date-item">
|
||||||
|
<strong>Date ${date.ordre} :</strong> ${date.dateDebut.toLocaleDateString('fr-FR', {
|
||||||
|
weekday: 'long',
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
})} - ${date.dateFin.toLocaleDateString('fr-FR', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
const content = `
|
||||||
|
<h2>Rappel : Votre formation commence demain !</h2>
|
||||||
|
<p>Bonjour ${salutation},</p>
|
||||||
|
|
||||||
|
<p>Nous vous rappelons que votre formation <strong>${params.formationNom}</strong> commence <strong>demain</strong>.</p>
|
||||||
|
|
||||||
|
<div class="info-box">
|
||||||
|
<h3>Informations pratiques</h3>
|
||||||
|
<p><strong>Séquence :</strong> ${params.sequenceNom}</p>
|
||||||
|
${params.lieu ? `<p><strong>Lieu :</strong> ${params.lieu}</p>` : ''}
|
||||||
|
${params.formateur ? `<p><strong>Formateur :</strong> ${params.formateur}</p>` : ''}
|
||||||
|
<h4>Dates :</h4>
|
||||||
|
${datesHTML}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p><strong>Merci de vous présenter à l'heure indiquée.</strong></p>
|
||||||
|
<p>N'oubliez pas d'apporter le matériel nécessaire.</p>
|
||||||
|
|
||||||
|
<p>Si vous avez des questions de dernière minute, n'hésitez pas à contacter le service RH.</p>
|
||||||
|
|
||||||
|
<p>À demain !</p>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const variables = {
|
||||||
|
nomApprenant: params.apprenantNom,
|
||||||
|
prenomApprenant: params.apprenantPrenom,
|
||||||
|
nomFormation: params.formationNom,
|
||||||
|
nomSequence: params.sequenceNom,
|
||||||
|
dateDebut: params.dates[0]?.dateDebut.toLocaleDateString('fr-FR') || '',
|
||||||
|
dateFin: params.dates[params.dates.length - 1]?.dateFin.toLocaleDateString('fr-FR') || '',
|
||||||
|
lieu: params.lieu || '',
|
||||||
|
formateur: params.formateur || '',
|
||||||
|
};
|
||||||
|
|
||||||
|
return sendEmail({
|
||||||
|
to: params.apprenantEmail,
|
||||||
|
subject: `Rappel J-1 : ${params.formationNom} - C'est demain !`,
|
||||||
|
html: await getEmailTemplate(content, 'rappel', variables),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Envoie des emails groupés à tous les inscrits d'une séquence
|
* Envoie des emails groupés à tous les inscrits d'une séquence
|
||||||
*/
|
*/
|
||||||
@@ -285,7 +360,7 @@ export async function sendGroupEmail(params: {
|
|||||||
recipients: Array<{ email: string; prenom: string; nom: string; fonction: string }>;
|
recipients: Array<{ email: string; prenom: string; nom: string; fonction: string }>;
|
||||||
formationNom: string;
|
formationNom: string;
|
||||||
sequenceNom: string;
|
sequenceNom: string;
|
||||||
type: 'teaser' | 'rappel';
|
type: 'teaser' | 'rappel' | 'rappel_j1';
|
||||||
dates: Array<{ dateDebut: Date; dateFin: Date; ordre: number }>;
|
dates: Array<{ dateDebut: Date; dateFin: Date; ordre: number }>;
|
||||||
lieu?: string;
|
lieu?: string;
|
||||||
formateur?: string;
|
formateur?: string;
|
||||||
@@ -307,7 +382,7 @@ export async function sendGroupEmail(params: {
|
|||||||
lieu: params.lieu,
|
lieu: params.lieu,
|
||||||
formateur: params.formateur,
|
formateur: params.formateur,
|
||||||
});
|
});
|
||||||
} else {
|
} else if (params.type === 'rappel') {
|
||||||
await sendRappelJ7Email({
|
await sendRappelJ7Email({
|
||||||
apprenantEmail: recipient.email,
|
apprenantEmail: recipient.email,
|
||||||
apprenantPrenom: recipient.prenom,
|
apprenantPrenom: recipient.prenom,
|
||||||
@@ -318,6 +393,19 @@ export async function sendGroupEmail(params: {
|
|||||||
dates: params.dates,
|
dates: params.dates,
|
||||||
lieu: params.lieu || '',
|
lieu: params.lieu || '',
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
// rappel_j1
|
||||||
|
await sendRappelJ1Email({
|
||||||
|
apprenantEmail: recipient.email,
|
||||||
|
apprenantPrenom: recipient.prenom,
|
||||||
|
apprenantNom: recipient.nom,
|
||||||
|
apprenantFonction: recipient.fonction,
|
||||||
|
formationNom: params.formationNom,
|
||||||
|
sequenceNom: params.sequenceNom,
|
||||||
|
dates: params.dates,
|
||||||
|
lieu: params.lieu,
|
||||||
|
formateur: params.formateur,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
sent++;
|
sent++;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -493,10 +493,7 @@ export const appRouter = router({
|
|||||||
return { success: true };
|
return { success: true };
|
||||||
}),
|
}),
|
||||||
|
|
||||||
sendGroupEmail: adminProcedure.input(z.object({
|
sendGroupEmail: adminProcedure.input(z.object({ sequenceId: z.number(), type: z.enum(['teaser', 'rappel', 'rappel_j1']) })).mutation(async ({ input }) => {
|
||||||
sequenceId: z.number(),
|
|
||||||
type: z.enum(['teaser', 'rappel']),
|
|
||||||
})).mutation(async ({ input }) => {
|
|
||||||
const sequence = await db.getSequenceById(input.sequenceId);
|
const sequence = await db.getSequenceById(input.sequenceId);
|
||||||
if (!sequence) {
|
if (!sequence) {
|
||||||
throw new TRPCError({ code: 'NOT_FOUND', message: 'Séquence introuvable' });
|
throw new TRPCError({ code: 'NOT_FOUND', message: 'Séquence introuvable' });
|
||||||
@@ -553,6 +550,17 @@ export const appRouter = router({
|
|||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
emailPreview: adminProcedure
|
||||||
|
.input(z.object({
|
||||||
|
sequenceId: z.number(),
|
||||||
|
type: z.enum(['teaser', 'rappel', 'rappel_j1']),
|
||||||
|
apprenantId: z.number().optional(),
|
||||||
|
}))
|
||||||
|
.query(async ({ input }) => {
|
||||||
|
const { generateEmailPreview } = await import('./emailPreview');
|
||||||
|
return await generateEmailPreview(input);
|
||||||
|
}),
|
||||||
|
|
||||||
exportExcel: adminProcedure.input(z.object({ sequenceId: z.number() })).mutation(async ({ input }) => {
|
exportExcel: adminProcedure.input(z.object({ sequenceId: z.number() })).mutation(async ({ input }) => {
|
||||||
const sequence = await db.getSequenceById(input.sequenceId);
|
const sequence = await db.getSequenceById(input.sequenceId);
|
||||||
if (!sequence) throw new TRPCError({ code: 'NOT_FOUND', message: 'Séquence introuvable' });
|
if (!sequence) throw new TRPCError({ code: 'NOT_FOUND', message: 'Séquence introuvable' });
|
||||||
|
|||||||
22
todo.md
22
todo.md
@@ -802,3 +802,25 @@
|
|||||||
- [x] Modifier le template pour afficher lieu et formateur
|
- [x] Modifier le template pour afficher lieu et formateur
|
||||||
- [x] Ajouter lieu et formateur dans les variables
|
- [x] Ajouter lieu et formateur dans les variables
|
||||||
- [x] Préparer pour test en production
|
- [x] Préparer pour test en production
|
||||||
|
|
||||||
|
## Personnalisation templates d'emails avec variables
|
||||||
|
|
||||||
|
- [x] Vérifier que les templates personnalisés utilisent bien les variables (OK, replaceEmailVariables)
|
||||||
|
- [x] Documenter les variables disponibles dans l'interface (OK, boutons d'insertion présents)
|
||||||
|
- [x] Les variables {{lieu}} et {{formateur}} sont déjà disponibles
|
||||||
|
|
||||||
|
## Aperçu avant envoi d'emails
|
||||||
|
|
||||||
|
- [x] Créer une procédure tRPC pour générer un aperçu d'email (emailPreview.ts)
|
||||||
|
- [x] Ajouter un bouton "Aperçu" dans l'interface d'envoi (AdminSequenceInscrits.tsx)
|
||||||
|
- [x] Afficher un modal avec le rendu de l'email (Dialog avec iframe)
|
||||||
|
- [x] Utiliser le premier apprenant confirmé pour l'aperçu
|
||||||
|
|
||||||
|
## Email de rappel J-1
|
||||||
|
|
||||||
|
- [x] Créer la fonction sendRappelJ1Email dans emailService.ts
|
||||||
|
- [x] Ajouter un bouton "Envoyer rappel J-1" dans l'interface
|
||||||
|
- [x] Créer le template d'email de rappel J-1
|
||||||
|
- [x] Ajouter le bouton d'aperçu pour rappel J-1
|
||||||
|
- [x] Modifier la procédure tRPC pour supporter rappel_j1
|
||||||
|
- [x] Préparer pour test (serveur redémarré)
|
||||||
|
|||||||
Reference in New Issue
Block a user