Initial local backup snapshot
This commit is contained in:
commit
acd9e14ba5
367 changed files with 118038 additions and 0 deletions
343
server/emailRecap.ts
Normal file
343
server/emailRecap.ts
Normal file
|
|
@ -0,0 +1,343 @@
|
|||
/**
|
||||
* Génère un récapitulatif complet d'une demande traitée pour diffusion interne.
|
||||
*/
|
||||
|
||||
const requestTypeLabels: Record<string, string> = {
|
||||
subvention_fonctionnement: "Subvention de fonctionnement",
|
||||
subvention_projet: "Subvention de projet",
|
||||
agrement_jeunesse_education: "Agrément Jeunesse et Éducation Populaire",
|
||||
agrement_sport: "Agrément Sport",
|
||||
autorisation_occupation: "Autorisation d'occupation",
|
||||
demande_salle: "Demande de salle / local",
|
||||
demande_materiel_evenementiel: "Demande de matériel événementiel",
|
||||
autre: "Autre demande",
|
||||
};
|
||||
|
||||
const statusLabels: Record<string, string> = {
|
||||
validee: "Validée",
|
||||
refusee: "Refusée",
|
||||
en_cours_traitement: "En cours de traitement",
|
||||
information_complementaire: "Information complémentaire demandée",
|
||||
};
|
||||
|
||||
const statusBadgeStyles: Record<string, { label: string; bg: string; border: string; text: string }> = {
|
||||
validee: { label: "VALIDÉE", bg: "#ecfdf3", border: "#a7f3d0", text: "#166534" },
|
||||
refusee: { label: "REFUSÉE", bg: "#fef2f2", border: "#fecaca", text: "#b91c1c" },
|
||||
en_cours_traitement: { label: "EN COURS", bg: "#eff6ff", border: "#bfdbfe", text: "#1d4ed8" },
|
||||
information_complementaire: { label: "INFO REQUISE", bg: "#fff7ed", border: "#fed7aa", text: "#c2410c" },
|
||||
};
|
||||
|
||||
interface RecapData {
|
||||
request: {
|
||||
id: number;
|
||||
titre: string;
|
||||
type: string;
|
||||
status: string;
|
||||
description: string | null;
|
||||
montantDemande: number | null;
|
||||
montantAccorde: number | null;
|
||||
commentaireAdmin: string | null;
|
||||
formData: string | null;
|
||||
dateSubmission: Date | null;
|
||||
dateTraitement: Date | null;
|
||||
createdAt: Date | null;
|
||||
};
|
||||
association: {
|
||||
nomAssociation: string;
|
||||
siret: string | null;
|
||||
adresse: string | null;
|
||||
codePostal: string | null;
|
||||
ville: string | null;
|
||||
telephone: string | null;
|
||||
emailContact: string | null;
|
||||
nomRepresentant: string | null;
|
||||
} | null;
|
||||
traitePar: string;
|
||||
documents?: { name: string; type: string }[];
|
||||
serviceLabel?: string | null;
|
||||
}
|
||||
|
||||
function formatDateTimeFr(date: Date | string | null | undefined) {
|
||||
if (!date) return "Non renseignée";
|
||||
return new Date(date).toLocaleString("fr-FR", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
}
|
||||
|
||||
function formatDateFr(date: string | null | undefined) {
|
||||
if (!date) return "Non renseignée";
|
||||
return new Date(date).toLocaleDateString("fr-FR", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
});
|
||||
}
|
||||
|
||||
function formatAmount(amount: number | null | undefined) {
|
||||
if (amount == null) return "Non renseigné";
|
||||
return `${(amount / 100).toLocaleString("fr-FR")} €`;
|
||||
}
|
||||
|
||||
function escapeHtml(value: string) {
|
||||
return value
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
function parseRequestHighlights(requestType: string, formData: any): Array<{ label: string; value: string }> {
|
||||
if (!formData || typeof formData !== "object") return [];
|
||||
|
||||
if (requestType === "demande_salle") {
|
||||
const salles = Array.isArray(formData.sallesNoms)
|
||||
? formData.sallesNoms.join(", ")
|
||||
: Array.isArray(formData.sallesSelectionnees)
|
||||
? formData.sallesSelectionnees.join(", ")
|
||||
: "";
|
||||
|
||||
const highlights = [
|
||||
salles ? { label: "Salle(s)", value: salles } : null,
|
||||
formData.dateReservation ? { label: "Date de début", value: formatDateFr(formData.dateReservation) } : null,
|
||||
formData.dateFinReservation ? { label: "Date de fin", value: formatDateFr(formData.dateFinReservation) } : null,
|
||||
formData.heureDebut || formData.heureFin
|
||||
? { label: "Créneau", value: `${formData.heureDebut || "?"} - ${formData.heureFin || "?"}` }
|
||||
: null,
|
||||
formData.nombreParticipants ? { label: "Participants", value: String(formData.nombreParticipants) } : null,
|
||||
formData.motifReservation ? { label: "Motif", value: String(formData.motifReservation) } : null,
|
||||
];
|
||||
return highlights.filter(Boolean) as Array<{ label: string; value: string }>;
|
||||
}
|
||||
|
||||
if (requestType === "demande_materiel_evenementiel") {
|
||||
const highlights = [
|
||||
formData.commune ? { label: "Commune", value: String(formData.commune) } : null,
|
||||
formData.dateDebutManifestation || formData.dateManifestation
|
||||
? { label: "Début manifestation", value: formatDateFr(formData.dateDebutManifestation || formData.dateManifestation) }
|
||||
: null,
|
||||
formData.dateFinManifestation
|
||||
? { label: "Fin manifestation", value: formatDateFr(formData.dateFinManifestation) }
|
||||
: null,
|
||||
formData.dateRestitution ? { label: "Restitution", value: formatDateFr(formData.dateRestitution) } : null,
|
||||
formData.motifDemande ? { label: "Motif", value: String(formData.motifDemande) } : null,
|
||||
];
|
||||
return highlights.filter(Boolean) as Array<{ label: string; value: string }>;
|
||||
}
|
||||
|
||||
return [
|
||||
formData.dateDebut ? { label: "Date de début", value: formatDateFr(formData.dateDebut) } : null,
|
||||
formData.dateFin ? { label: "Date de fin", value: formatDateFr(formData.dateFin) } : null,
|
||||
formData.objetProjet ? { label: "Objet", value: String(formData.objetProjet) } : null,
|
||||
].filter(Boolean) as Array<{ label: string; value: string }>;
|
||||
}
|
||||
|
||||
export function generateRequestRecapEmail(data: RecapData): { title: string; content: string; html: string } {
|
||||
const { request, association, traitePar, documents, serviceLabel } = data;
|
||||
const typeLabel = requestTypeLabels[request.type] || request.type;
|
||||
const statusLabel = statusLabels[request.status] || request.status;
|
||||
const badge = statusBadgeStyles[request.status] || {
|
||||
label: statusLabel.toUpperCase(),
|
||||
bg: "#f3f4f6",
|
||||
border: "#d1d5db",
|
||||
text: "#374151",
|
||||
};
|
||||
|
||||
let formData: any = {};
|
||||
try {
|
||||
formData = request.formData ? JSON.parse(request.formData) : {};
|
||||
} catch {
|
||||
formData = {};
|
||||
}
|
||||
|
||||
const highlights = parseRequestHighlights(request.type, formData);
|
||||
const subject = `Demande traitée #${request.id} - ${statusLabel} - ${request.titre}`;
|
||||
|
||||
const textSections: string[] = [
|
||||
"Récapitulatif de demande traitée",
|
||||
"",
|
||||
`Demande n°${request.id} - ${statusLabel}`,
|
||||
`Type : ${typeLabel}`,
|
||||
`Association : ${association?.nomAssociation || "Non renseignée"}`,
|
||||
`Traitée par : ${traitePar}`,
|
||||
`Date de traitement : ${formatDateTimeFr(request.dateTraitement)}`,
|
||||
serviceLabel ? `Service concerné : ${serviceLabel}` : "",
|
||||
request.commentaireAdmin ? `Commentaire : ${request.commentaireAdmin}` : "",
|
||||
request.montantAccorde != null ? `Montant accordé : ${formatAmount(request.montantAccorde)}` : "",
|
||||
"",
|
||||
"Informations de l'association",
|
||||
`- Nom : ${association?.nomAssociation || "Non renseigné"}`,
|
||||
association?.siret ? `- SIRET : ${association.siret}` : "",
|
||||
association?.adresse ? `- Adresse : ${association.adresse}${association.codePostal ? `, ${association.codePostal}` : ""}${association.ville ? ` ${association.ville}` : ""}` : "",
|
||||
association?.nomRepresentant ? `- Représentant : ${association.nomRepresentant}` : "",
|
||||
association?.telephone ? `- Téléphone : ${association.telephone}` : "",
|
||||
association?.emailContact ? `- Email : ${association.emailContact}` : "",
|
||||
"",
|
||||
"Points clés du dossier",
|
||||
...highlights.map((item) => `- ${item.label} : ${item.value}`),
|
||||
request.description ? `- Description : ${request.description}` : "",
|
||||
request.montantDemande != null ? `- Montant demandé : ${formatAmount(request.montantDemande)}` : "",
|
||||
];
|
||||
|
||||
if (documents?.length) {
|
||||
textSections.push("", "Documents joints au dossier");
|
||||
textSections.push(...documents.map((doc, index) => `${index + 1}. ${doc.name} (${doc.type})`));
|
||||
}
|
||||
|
||||
textSections.push(
|
||||
"",
|
||||
"Le PDF récapitulatif est joint à cet email lorsqu'il est disponible pour ce type de demande.",
|
||||
"",
|
||||
"Ce message a été généré automatiquement par le Portail Associations."
|
||||
);
|
||||
|
||||
const htmlHighlights = highlights
|
||||
.map(
|
||||
(item) => `
|
||||
<tr>
|
||||
<td style="padding:8px 0;color:#6b7280;font-size:14px;width:180px;vertical-align:top;">${escapeHtml(item.label)}</td>
|
||||
<td style="padding:8px 0;color:#111827;font-size:14px;font-weight:600;">${escapeHtml(item.value)}</td>
|
||||
</tr>`
|
||||
)
|
||||
.join("");
|
||||
|
||||
const htmlDocuments = documents?.length
|
||||
? `
|
||||
<div style="margin-top:24px;padding:20px;border:1px solid #e5e7eb;border-radius:14px;background:#ffffff;">
|
||||
<h3 style="margin:0 0 12px 0;font-size:16px;color:#111827;">Documents du dossier</h3>
|
||||
<ul style="margin:0;padding-left:18px;color:#374151;font-size:14px;line-height:1.6;">
|
||||
${documents.map((doc) => `<li>${escapeHtml(doc.name)} <span style="color:#6b7280;">(${escapeHtml(doc.type)})</span></li>`).join("")}
|
||||
</ul>
|
||||
</div>`
|
||||
: "";
|
||||
|
||||
const html = `
|
||||
<div style="margin:0;padding:32px 0;background:#f5f7fb;font-family:Arial,Helvetica,sans-serif;color:#111827;">
|
||||
<div style="max-width:720px;margin:0 auto;background:#ffffff;border:1px solid #e5e7eb;border-radius:20px;overflow:hidden;box-shadow:0 10px 30px rgba(15,23,42,0.08);">
|
||||
<div style="padding:28px 32px;background:linear-gradient(135deg,#000091 0%,#163d8f 100%);color:#ffffff;">
|
||||
<div style="font-size:12px;letter-spacing:0.08em;text-transform:uppercase;opacity:0.8;">Portail Associations</div>
|
||||
<h1 style="margin:10px 0 0 0;font-size:24px;line-height:1.25;">Transmission d'une demande traitée</h1>
|
||||
<p style="margin:10px 0 0 0;font-size:14px;line-height:1.6;opacity:0.92;">
|
||||
Ce message reprend les informations essentielles du dossier et le PDF récapitulatif est joint lorsqu'il est disponible.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="padding:28px 32px;">
|
||||
<div style="display:inline-block;padding:8px 14px;border-radius:999px;background:${badge.bg};border:1px solid ${badge.border};color:${badge.text};font-size:12px;font-weight:700;letter-spacing:0.04em;">
|
||||
${escapeHtml(badge.label)}
|
||||
</div>
|
||||
|
||||
<h2 style="margin:18px 0 6px 0;font-size:22px;line-height:1.3;">${escapeHtml(request.titre)}</h2>
|
||||
<p style="margin:0;color:#4b5563;font-size:15px;line-height:1.7;">
|
||||
Demande <strong>#${request.id}</strong> • ${escapeHtml(typeLabel)}
|
||||
</p>
|
||||
|
||||
<div style="margin-top:24px;padding:20px;border:1px solid #dbeafe;border-radius:16px;background:#f8fbff;">
|
||||
<h3 style="margin:0 0 14px 0;font-size:16px;color:#0f172a;">Décision et transmission</h3>
|
||||
<table style="width:100%;border-collapse:collapse;">
|
||||
<tr>
|
||||
<td style="padding:8px 0;color:#6b7280;font-size:14px;width:180px;">Traitée par</td>
|
||||
<td style="padding:8px 0;color:#111827;font-size:14px;font-weight:600;">${escapeHtml(traitePar)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:8px 0;color:#6b7280;font-size:14px;">Date de traitement</td>
|
||||
<td style="padding:8px 0;color:#111827;font-size:14px;font-weight:600;">${escapeHtml(formatDateTimeFr(request.dateTraitement))}</td>
|
||||
</tr>
|
||||
${serviceLabel ? `
|
||||
<tr>
|
||||
<td style="padding:8px 0;color:#6b7280;font-size:14px;">Service concerné</td>
|
||||
<td style="padding:8px 0;color:#111827;font-size:14px;font-weight:600;">${escapeHtml(serviceLabel)}</td>
|
||||
</tr>` : ""}
|
||||
${request.montantAccorde != null ? `
|
||||
<tr>
|
||||
<td style="padding:8px 0;color:#6b7280;font-size:14px;">Montant accordé</td>
|
||||
<td style="padding:8px 0;color:#111827;font-size:14px;font-weight:600;">${escapeHtml(formatAmount(request.montantAccorde))}</td>
|
||||
</tr>` : ""}
|
||||
</table>
|
||||
${request.commentaireAdmin ? `
|
||||
<div style="margin-top:14px;padding:14px 16px;border-radius:12px;background:#ffffff;border:1px solid #e5e7eb;">
|
||||
<div style="font-size:12px;color:#6b7280;text-transform:uppercase;letter-spacing:0.05em;margin-bottom:6px;">Commentaire administratif</div>
|
||||
<div style="font-size:14px;line-height:1.7;color:#111827;">${escapeHtml(request.commentaireAdmin).replace(/\n/g, "<br />")}</div>
|
||||
</div>` : ""}
|
||||
</div>
|
||||
|
||||
<div style="margin-top:24px;padding:20px;border:1px solid #e5e7eb;border-radius:16px;background:#ffffff;">
|
||||
<h3 style="margin:0 0 14px 0;font-size:16px;color:#111827;">Association</h3>
|
||||
<table style="width:100%;border-collapse:collapse;">
|
||||
<tr>
|
||||
<td style="padding:8px 0;color:#6b7280;font-size:14px;width:180px;">Nom</td>
|
||||
<td style="padding:8px 0;color:#111827;font-size:14px;font-weight:600;">${escapeHtml(association?.nomAssociation || "Non renseignée")}</td>
|
||||
</tr>
|
||||
${association?.siret ? `
|
||||
<tr>
|
||||
<td style="padding:8px 0;color:#6b7280;font-size:14px;">SIRET</td>
|
||||
<td style="padding:8px 0;color:#111827;font-size:14px;font-weight:600;">${escapeHtml(association.siret)}</td>
|
||||
</tr>` : ""}
|
||||
${(association?.adresse || association?.ville || association?.codePostal) ? `
|
||||
<tr>
|
||||
<td style="padding:8px 0;color:#6b7280;font-size:14px;">Adresse</td>
|
||||
<td style="padding:8px 0;color:#111827;font-size:14px;font-weight:600;">${escapeHtml(
|
||||
[association?.adresse, association?.codePostal, association?.ville].filter(Boolean).join(" ")
|
||||
)}</td>
|
||||
</tr>` : ""}
|
||||
${association?.nomRepresentant ? `
|
||||
<tr>
|
||||
<td style="padding:8px 0;color:#6b7280;font-size:14px;">Représentant</td>
|
||||
<td style="padding:8px 0;color:#111827;font-size:14px;font-weight:600;">${escapeHtml(association.nomRepresentant)}</td>
|
||||
</tr>` : ""}
|
||||
${association?.telephone ? `
|
||||
<tr>
|
||||
<td style="padding:8px 0;color:#6b7280;font-size:14px;">Téléphone</td>
|
||||
<td style="padding:8px 0;color:#111827;font-size:14px;font-weight:600;">${escapeHtml(association.telephone)}</td>
|
||||
</tr>` : ""}
|
||||
${association?.emailContact ? `
|
||||
<tr>
|
||||
<td style="padding:8px 0;color:#6b7280;font-size:14px;">Email</td>
|
||||
<td style="padding:8px 0;color:#111827;font-size:14px;font-weight:600;">${escapeHtml(association.emailContact)}</td>
|
||||
</tr>` : ""}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:24px;padding:20px;border:1px solid #e5e7eb;border-radius:16px;background:#ffffff;">
|
||||
<h3 style="margin:0 0 14px 0;font-size:16px;color:#111827;">Points clés du dossier</h3>
|
||||
<table style="width:100%;border-collapse:collapse;">
|
||||
<tr>
|
||||
<td style="padding:8px 0;color:#6b7280;font-size:14px;width:180px;">Date de soumission</td>
|
||||
<td style="padding:8px 0;color:#111827;font-size:14px;font-weight:600;">${escapeHtml(formatDateTimeFr(request.dateSubmission))}</td>
|
||||
</tr>
|
||||
${request.montantDemande != null ? `
|
||||
<tr>
|
||||
<td style="padding:8px 0;color:#6b7280;font-size:14px;">Montant demandé</td>
|
||||
<td style="padding:8px 0;color:#111827;font-size:14px;font-weight:600;">${escapeHtml(formatAmount(request.montantDemande))}</td>
|
||||
</tr>` : ""}
|
||||
${htmlHighlights}
|
||||
${request.description ? `
|
||||
<tr>
|
||||
<td style="padding:8px 0;color:#6b7280;font-size:14px;vertical-align:top;">Description</td>
|
||||
<td style="padding:8px 0;color:#111827;font-size:14px;line-height:1.7;">${escapeHtml(request.description).replace(/\n/g, "<br />")}</td>
|
||||
</tr>` : ""}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
${htmlDocuments}
|
||||
|
||||
<div style="margin-top:24px;padding:18px 20px;border-radius:14px;background:#f9fafb;border:1px dashed #d1d5db;">
|
||||
<p style="margin:0;color:#374151;font-size:14px;line-height:1.7;">
|
||||
Le PDF récapitulatif est joint à cet email lorsqu’il est disponible pour ce type de demande. Tu peux l’utiliser pour la transmission interne, l’archivage ou l’impression.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return {
|
||||
title: subject,
|
||||
content: textSections.filter(Boolean).join("\n"),
|
||||
html,
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue