Initial local backup snapshot

This commit is contained in:
Selecta Keke 2026-06-24 00:02:55 -03:00
commit acd9e14ba5
367 changed files with 118038 additions and 0 deletions

343
server/emailRecap.ts Normal file
View 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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
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 lorsquil est disponible pour ce type de demande. Tu peux lutiliser pour la transmission interne, larchivage ou limpression.
</p>
</div>
</div>
</div>
</div>
`;
return {
title: subject,
content: textSections.filter(Boolean).join("\n"),
html,
};
}