238 lines
13 KiB
TypeScript
238 lines
13 KiB
TypeScript
function formatDateFr(date: Date | string | null | undefined) {
|
|
if (!date) return "Non renseignée";
|
|
return new Date(date).toLocaleDateString("fr-FR", {
|
|
day: "2-digit",
|
|
month: "long",
|
|
year: "numeric",
|
|
});
|
|
}
|
|
|
|
function escapeHtml(value: string) {
|
|
return value
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """)
|
|
.replace(/'/g, "'");
|
|
}
|
|
|
|
export function generateMaterialReturnReminderEmail(input: {
|
|
associationName: string;
|
|
serviceLabel?: string | null;
|
|
restitutionDate: Date | string;
|
|
uploadLink: string;
|
|
requestTitle: string;
|
|
tone?: "assignment" | "scheduled" | "overdue" | "reactivation";
|
|
}) {
|
|
const restitutionLabel = formatDateFr(input.restitutionDate);
|
|
const serviceLabel = input.serviceLabel?.trim() || "service technique / logistique";
|
|
const tone = input.tone || "scheduled";
|
|
const toneContent = {
|
|
assignment: {
|
|
subject: `Mission de récupération assignée - ${input.associationName} - restitution prévue le ${restitutionLabel}`,
|
|
title: "Mission de récupération assignée",
|
|
intro: `Une mission de récupération du matériel a été attribuée à <strong>${escapeHtml(serviceLabel)}</strong> pour <strong>${escapeHtml(input.associationName)}</strong>.`,
|
|
actionTitle: "Action à préparer",
|
|
actionBody:
|
|
"La fiche d'état des lieux préremplie CCDS est jointe à cet email. Conservez ce lien terrain : il vous permettra de constater la restitution, signer sur smartphone et clôturer le dossier le moment venu.",
|
|
accent: "#0f2d63",
|
|
alertBg: "#f5f9ff",
|
|
alertBorder: "#cfe0ff",
|
|
alertTitleColor: "#0f2d63",
|
|
},
|
|
scheduled: {
|
|
subject: `Récupération matériel à organiser - ${input.associationName} - restitution prévue le ${restitutionLabel}`,
|
|
title: "Récupération du matériel à organiser",
|
|
intro: `La restitution du matériel pour <strong>${escapeHtml(input.associationName)}</strong> approche.`,
|
|
actionTitle: "Action attendue",
|
|
actionBody:
|
|
"La fiche d'état des lieux préremplie CCDS est jointe à cet email. Votre service dispose d'un délai maximal de <strong>3 jours</strong> pour effectuer la récupération, compléter la fiche contradictoire puis la déposer sur le portail.",
|
|
accent: "#0f2d63",
|
|
alertBg: "#fff8e6",
|
|
alertBorder: "#efb100",
|
|
alertTitleColor: "#7c5a00",
|
|
},
|
|
overdue: {
|
|
subject: `Retard de restitution - action requise - ${input.associationName} - ${restitutionLabel}`,
|
|
title: "Alerte retard de restitution",
|
|
intro: `La restitution du matériel pour <strong>${escapeHtml(input.associationName)}</strong> n'a pas encore été validée dans les délais.`,
|
|
actionTitle: "Relance automatique",
|
|
actionBody:
|
|
"Merci d'intervenir en priorité. Le délai de 3 jours après la date de restitution est dépassé. Utilisez le lien ci-dessous pour finaliser le constat terrain et débloquer la clôture du dossier.",
|
|
accent: "#b45309",
|
|
alertBg: "#fff7ed",
|
|
alertBorder: "#fdba74",
|
|
alertTitleColor: "#9a3412",
|
|
},
|
|
reactivation: {
|
|
subject: `Nouveau lien de restitution actif - ${input.associationName} - ${restitutionLabel}`,
|
|
title: "Lien de restitution réactivé",
|
|
intro: `Un nouveau lien terrain actif a été généré pour <strong>${escapeHtml(input.associationName)}</strong>.`,
|
|
actionTitle: "Action immédiate",
|
|
actionBody:
|
|
"Utilisez exclusivement le nouveau lien ci-dessous pour constater la restitution, signer sur smartphone et clôturer le dossier. Les anciens liens de restitution ne sont plus valides.",
|
|
accent: "#0f2d63",
|
|
alertBg: "#eef6ff",
|
|
alertBorder: "#93c5fd",
|
|
alertTitleColor: "#1d4ed8",
|
|
},
|
|
}[tone];
|
|
const subject = toneContent.subject;
|
|
|
|
const text = [
|
|
tone === "assignment"
|
|
? "Assignation logistique - récupération du matériel événementiel"
|
|
: tone === "overdue"
|
|
? "Alerte automatique - restitution du matériel en retard"
|
|
: "Rappel automatique - récupération du matériel événementiel",
|
|
"",
|
|
`Association : ${input.associationName}`,
|
|
`Demande : ${input.requestTitle}`,
|
|
`Service concerné : ${serviceLabel}`,
|
|
`Date de restitution prévue : ${restitutionLabel}`,
|
|
"",
|
|
"La fiche d'état des lieux préremplie CCDS est jointe à cet email.",
|
|
tone === "assignment"
|
|
? "Ce lien terrain devra être utilisé par l'agent mandaté pour effectuer le constat contradictoire, signer et clôturer la restitution."
|
|
: tone === "reactivation"
|
|
? "Un nouveau lien actif vient d'être généré. Les anciens liens de restitution ne sont plus valides."
|
|
: tone === "overdue"
|
|
? "Le délai de 3 jours après la restitution est dépassé. Merci de finaliser la récupération et de valider immédiatement la fiche sur le portail."
|
|
: "Vous disposez d'un délai maximal de 3 jours pour effectuer la récupération, compléter la fiche de manière contradictoire puis la téléverser sur le portail.",
|
|
"",
|
|
`Téléverser la fiche signée : ${input.uploadLink}`,
|
|
"",
|
|
"Ce message a été généré automatiquement par le Portail Associations.",
|
|
].join("\n");
|
|
|
|
const html = `
|
|
<div style="margin:0;padding:32px 0;background:#f4f7fb;font-family:Arial,Helvetica,sans-serif;color:#0f172a;">
|
|
<div style="max-width:720px;margin:0 auto;background:#ffffff;border:1px solid #dbe4f0;border-radius:20px;overflow:hidden;box-shadow:0 12px 30px rgba(15,23,42,0.08);">
|
|
<div style="padding:28px 32px;background:${tone === "overdue" ? "linear-gradient(135deg,#8a4b00 0%,#b45309 100%)" : "linear-gradient(135deg,#0f2d63 0%,#18438f 100%)"};color:#ffffff;">
|
|
<div style="font-size:12px;letter-spacing:0.12em;text-transform:uppercase;opacity:0.82;">Portail Associations</div>
|
|
<h1 style="margin:12px 0 6px 0;font-size:24px;line-height:1.2;">${toneContent.title}</h1>
|
|
<p style="margin:0;font-size:15px;line-height:1.6;opacity:0.9;">
|
|
${toneContent.intro}
|
|
</p>
|
|
</div>
|
|
|
|
<div style="padding:28px 32px;">
|
|
<div style="padding:18px 20px;border:1px solid #cfe0ff;border-radius:16px;background:#f5f9ff;">
|
|
<div style="display:grid;grid-template-columns:180px 1fr;gap:10px 18px;font-size:14px;line-height:1.5;">
|
|
<div style="color:#64748b;">Association</div>
|
|
<div style="font-weight:700;color:#0f172a;">${escapeHtml(input.associationName)}</div>
|
|
<div style="color:#64748b;">Demande</div>
|
|
<div style="font-weight:700;color:#0f172a;">${escapeHtml(input.requestTitle)}</div>
|
|
<div style="color:#64748b;">Service concerné</div>
|
|
<div style="font-weight:700;color:#0f172a;">${escapeHtml(serviceLabel)}</div>
|
|
<div style="color:#64748b;">Restitution prévue</div>
|
|
<div style="font-weight:700;color:#0f172a;">${escapeHtml(restitutionLabel)}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="margin-top:22px;padding:18px 20px;border-left:4px solid ${toneContent.alertBorder};background:${toneContent.alertBg};border-radius:12px;">
|
|
<p style="margin:0 0 8px 0;font-size:15px;font-weight:700;color:${toneContent.alertTitleColor};">${toneContent.actionTitle}</p>
|
|
<p style="margin:0;font-size:14px;line-height:1.7;color:#3f3f46;">
|
|
${toneContent.actionBody}
|
|
</p>
|
|
</div>
|
|
|
|
<div style="margin-top:26px;text-align:center;">
|
|
<a href="${escapeHtml(input.uploadLink)}" style="display:inline-block;padding:14px 22px;background:#0f2d63;color:#ffffff;text-decoration:none;border-radius:12px;font-weight:700;">
|
|
${tone === "reactivation" ? "Ouvrir le nouveau lien terrain" : "Téléverser la fiche signée"}
|
|
</a>
|
|
</div>
|
|
|
|
<div style="margin-top:16px;padding:14px 16px;border:1px dashed #cbd5e1;border-radius:12px;background:#f8fafc;">
|
|
<p style="margin:0 0 6px 0;font-size:12px;font-weight:700;color:#475569;text-transform:uppercase;letter-spacing:0.08em;">
|
|
Lien terrain à utiliser
|
|
</p>
|
|
<p style="margin:0;font-size:13px;line-height:1.6;word-break:break-all;color:#0f172a;">
|
|
<a href="${escapeHtml(input.uploadLink)}" style="color:#0f2d63;text-decoration:underline;">${escapeHtml(input.uploadLink)}</a>
|
|
</p>
|
|
</div>
|
|
|
|
<p style="margin:26px 0 0 0;font-size:13px;line-height:1.6;color:#64748b;">
|
|
Ce message a été généré automatiquement par le Portail Associations afin de sécuriser le retour du matériel événementiel.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
return { subject, text, html };
|
|
}
|
|
|
|
export function generateMaterialReturnCompletionEmail(input: {
|
|
associationName: string;
|
|
requestTitle: string;
|
|
restitutionDate: Date | string;
|
|
serviceLabel?: string | null;
|
|
issueFlag: boolean;
|
|
compliance: "conforme" | "non_conforme";
|
|
discrepancyDetails?: string | null;
|
|
}) {
|
|
const restitutionLabel = formatDateFr(input.restitutionDate);
|
|
const serviceLabel = input.serviceLabel?.trim() || "service technique / logistique";
|
|
const statusLabel = input.issueFlag ? "Alerte dégradation / litige" : "Matériel récupéré / dossier clôturé";
|
|
const subject = `${input.issueFlag ? "Alerte restitution" : "Restitution clôturée"} - ${input.associationName} - ${restitutionLabel}`;
|
|
|
|
const text = [
|
|
"Clôture de restitution du matériel événementiel",
|
|
"",
|
|
`Association : ${input.associationName}`,
|
|
`Demande : ${input.requestTitle}`,
|
|
`Service concerné : ${serviceLabel}`,
|
|
`Restitution : ${restitutionLabel}`,
|
|
`Résultat : ${statusLabel}`,
|
|
`Conformité : ${input.compliance === "conforme" ? "Conforme" : "Non conforme / partielle"}`,
|
|
input.discrepancyDetails ? `Réserves : ${input.discrepancyDetails}` : "",
|
|
"",
|
|
"Le PDF officiel finalisé est joint à cet email pour archivage et suivi.",
|
|
"",
|
|
"Ce message a été généré automatiquement par le Portail Associations.",
|
|
]
|
|
.filter(Boolean)
|
|
.join("\n");
|
|
|
|
const html = `
|
|
<div style="margin:0;padding:32px 0;background:#f4f7fb;font-family:Arial,Helvetica,sans-serif;color:#0f172a;">
|
|
<div style="max-width:720px;margin:0 auto;background:#ffffff;border:1px solid #dbe4f0;border-radius:20px;overflow:hidden;box-shadow:0 12px 30px rgba(15,23,42,0.08);">
|
|
<div style="padding:28px 32px;background:${input.issueFlag ? "#8a4b00" : "#0f2d63"};color:#ffffff;">
|
|
<div style="font-size:12px;letter-spacing:0.12em;text-transform:uppercase;opacity:0.82;">Portail Associations</div>
|
|
<h1 style="margin:12px 0 6px 0;font-size:24px;line-height:1.2;">${input.issueFlag ? "Alerte restitution" : "Restitution clôturée"}</h1>
|
|
<p style="margin:0;font-size:15px;line-height:1.6;opacity:0.92;">
|
|
${input.issueFlag ? "Des réserves ont été constatées lors de la restitution." : "Le matériel a été récupéré et la fiche contradictoire est finalisée."}
|
|
</p>
|
|
</div>
|
|
<div style="padding:28px 32px;">
|
|
<div style="display:grid;grid-template-columns:180px 1fr;gap:10px 18px;font-size:14px;line-height:1.5;">
|
|
<div style="color:#64748b;">Association</div>
|
|
<div style="font-weight:700;color:#0f172a;">${escapeHtml(input.associationName)}</div>
|
|
<div style="color:#64748b;">Demande</div>
|
|
<div style="font-weight:700;color:#0f172a;">${escapeHtml(input.requestTitle)}</div>
|
|
<div style="color:#64748b;">Service concerné</div>
|
|
<div style="font-weight:700;color:#0f172a;">${escapeHtml(serviceLabel)}</div>
|
|
<div style="color:#64748b;">Restitution</div>
|
|
<div style="font-weight:700;color:#0f172a;">${escapeHtml(restitutionLabel)}</div>
|
|
<div style="color:#64748b;">Résultat</div>
|
|
<div style="font-weight:700;color:${input.issueFlag ? "#9a3412" : "#166534"};">${escapeHtml(statusLabel)}</div>
|
|
</div>
|
|
<div style="margin-top:20px;padding:18px 20px;border-radius:14px;background:${input.issueFlag ? "#fff7ed" : "#ecfdf3"};border:1px solid ${input.issueFlag ? "#fdba74" : "#a7f3d0"};">
|
|
<p style="margin:0 0 8px 0;font-size:15px;font-weight:700;color:${input.issueFlag ? "#9a3412" : "#166534"};">
|
|
${input.compliance === "conforme" ? "Restitution conforme" : "Restitution non conforme / partielle"}
|
|
</p>
|
|
<p style="margin:0;font-size:14px;line-height:1.7;color:#3f3f46;">
|
|
${escapeHtml(input.discrepancyDetails || "Aucune réserve particulière n'a été déclarée.")}
|
|
</p>
|
|
</div>
|
|
<p style="margin:22px 0 0 0;font-size:13px;line-height:1.6;color:#64748b;">
|
|
Le PDF officiel finalisé est joint à cet email pour archivage et suivi.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
return { subject, text, html };
|
|
}
|