Enhance email functionality by adding QR code generation and resend logic for participants who have already responded to the questionnaire. Update email template for event ticket confirmation with detailed registration information.
This commit is contained in:
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
}
|
||||
@@ -101,9 +101,80 @@ export class CuestionarioRespondidoService {
|
||||
);
|
||||
if (cuestionarioRespondidoExistente) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
|
||||
// Aunque ya haya respondido, enviamos el correo nuevamente
|
||||
if (cuestionario && cuestionario.evento) {
|
||||
try {
|
||||
// Generar datos para el QR
|
||||
const datosQR = {
|
||||
id_participante: participante.id_participante,
|
||||
id_evento: cuestionario.evento.id_evento,
|
||||
id_cuestionario: cuestionario.id_cuestionario,
|
||||
};
|
||||
|
||||
// Generar código QR
|
||||
const qrJsonData = JSON.stringify(datosQR);
|
||||
const qrBuffer = await QRCode.toBuffer(qrJsonData);
|
||||
|
||||
// Enviar correo con el QR
|
||||
const urlApiCorreos = process.env.url_api_correos;
|
||||
|
||||
if (urlApiCorreos) {
|
||||
await axios.post(
|
||||
`${urlApiCorreos}/mail/send`,
|
||||
{
|
||||
to: participante.correo,
|
||||
subject: `Evento: ${cuestionario.evento?.nombre_evento}`,
|
||||
fecha_recibido: new Date().toISOString(),
|
||||
html: generarHtmlCorreoAsistencia({
|
||||
nombreForm: cuestionario.nombre_form,
|
||||
nombreEvento: cuestionario.evento?.nombre_evento,
|
||||
correo: participante.correo,
|
||||
fechaRegistro: new Date().toLocaleString(),
|
||||
fechaInicioEvento: cuestionario.evento?.fecha_inicio
|
||||
? new Date(
|
||||
cuestionario.evento.fecha_inicio,
|
||||
).toLocaleString()
|
||||
: undefined,
|
||||
fechaFinEvento: cuestionario.evento?.fecha_fin
|
||||
? new Date(cuestionario.evento.fecha_fin).toLocaleString()
|
||||
: undefined,
|
||||
}),
|
||||
adjuntos: [
|
||||
{
|
||||
filename: 'qr.png',
|
||||
content: qrBuffer.toString('base64'),
|
||||
encoding: 'base64',
|
||||
cid: 'qrCode',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
token: process.env.SYSTEM_TOKEN_API_CORREOS,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
console.log(
|
||||
`Correo reenviado exitosamente a ${participante.correo}`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error al reenviar correo:', error);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: `El participante con correo ${submitDto.correo} ya ha respondido el cuestionario ${cuestionario.nombre_form}.`,
|
||||
success: true,
|
||||
message: `El participante con correo ${submitDto.correo} ya había respondido el cuestionario ${cuestionario.nombre_form}. Se ha reenviado el correo con el código QR.`,
|
||||
datos_qr: cuestionario.evento
|
||||
? {
|
||||
id_participante: participante.id_participante,
|
||||
id_evento: cuestionario.evento.id_evento,
|
||||
id_cuestionario: cuestionario.id_cuestionario,
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,307 @@
|
||||
export function generarHtmlCorreoAsistenciaTicket({
|
||||
nombreForm,
|
||||
nombreEvento,
|
||||
correo,
|
||||
fechaRegistro,
|
||||
fechaInicioEvento,
|
||||
fechaFinEvento,
|
||||
}: {
|
||||
nombreForm: string;
|
||||
nombreEvento: string;
|
||||
correo: string;
|
||||
fechaRegistro: string;
|
||||
fechaInicioEvento?: string;
|
||||
fechaFinEvento?: string;
|
||||
}) {
|
||||
const horarioEvento =
|
||||
fechaInicioEvento && fechaFinEvento
|
||||
? `${fechaInicioEvento} al ${fechaFinEvento}`
|
||||
: 'Por confirmar';
|
||||
|
||||
return `
|
||||
<div
|
||||
style="
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
max-width: 650px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
border-radius: 16px;
|
||||
"
|
||||
>
|
||||
<!-- Header del correo -->
|
||||
<div style="text-align: center">
|
||||
<h1>¡Registro Confirmado!</h1>
|
||||
</div>
|
||||
|
||||
<!-- Ticket Container -->
|
||||
<div
|
||||
style="
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
background: #fefefe;
|
||||
position: relative;
|
||||
"
|
||||
>
|
||||
<!-- Ticket Header -->
|
||||
<div
|
||||
style="
|
||||
background: linear-gradient(90deg, #003d79 0%, #0056b3 100%);
|
||||
color: white;
|
||||
padding: 20px;
|
||||
position: relative;
|
||||
"
|
||||
>
|
||||
<h2 style="margin: 0 0 8px 0; font-size: 24px; font-weight: bold">
|
||||
🎓 ${nombreEvento || 'Evento Especial'}
|
||||
</h2>
|
||||
<p style="margin: 0; opacity: 0.9; font-size: 14px">
|
||||
TICKET DE ENTRADA • ENTRADA GRATUITA
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Ticket Body -->
|
||||
<div style="padding: 30px 20px">
|
||||
<div style="display: flex; gap: 30px; align-items: center">
|
||||
<!-- QR Code Section -->
|
||||
<div
|
||||
style="
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
"
|
||||
>
|
||||
<div
|
||||
style="
|
||||
background: #f8f9fa;
|
||||
border: 2px dashed #dee2e6;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
"
|
||||
>
|
||||
<img
|
||||
src="cid:qrCode"
|
||||
alt="Código QR de Entrada"
|
||||
style="
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
"
|
||||
/>
|
||||
<p
|
||||
style="
|
||||
margin: 12px 0 0 0;
|
||||
font-size: 12px;
|
||||
color: #6c757d;
|
||||
font-weight: 600;
|
||||
"
|
||||
>
|
||||
ESCANEA PARA REGISTRAR ASISTENCIA
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ticket Info Section -->
|
||||
<div style="flex: 1.2">
|
||||
<div style="margin-bottom: 24px">
|
||||
<h3
|
||||
style="
|
||||
color: #003d79;
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 18px;
|
||||
border-bottom: 2px solid #e9ecef;
|
||||
padding-bottom: 8px;
|
||||
"
|
||||
>
|
||||
📋 Detalles de tu registro
|
||||
</h3>
|
||||
|
||||
<div>
|
||||
<div style="margin-bottom: 12px">
|
||||
<p
|
||||
style="
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: #6c757d;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
"
|
||||
>
|
||||
Participante
|
||||
</p>
|
||||
<p
|
||||
style="
|
||||
margin: 2px 0 0 0;
|
||||
font-size: 15px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
"
|
||||
>
|
||||
${correo}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 12px">
|
||||
<p
|
||||
style="
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: #6c757d;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
"
|
||||
>
|
||||
Formulario
|
||||
</p>
|
||||
<p
|
||||
style="
|
||||
margin: 2px 0 0 0;
|
||||
font-size: 15px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
"
|
||||
>
|
||||
${nombreForm}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 12px">
|
||||
<p
|
||||
style="
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: #6c757d;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
"
|
||||
>
|
||||
Fecha de Registro
|
||||
</p>
|
||||
<p
|
||||
style="
|
||||
margin: 2px 0 0 0;
|
||||
font-size: 15px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
"
|
||||
>
|
||||
${fechaRegistro}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 12px">
|
||||
<p
|
||||
style="
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: #6c757d;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
"
|
||||
>
|
||||
🕒 Horario del Evento
|
||||
</p>
|
||||
<p
|
||||
style="
|
||||
margin: 2px 0 0 0;
|
||||
font-size: 15px;
|
||||
color: #003d79;
|
||||
font-weight: 600;
|
||||
"
|
||||
>
|
||||
${horarioEvento}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ticket Footer -->
|
||||
<div
|
||||
style="
|
||||
background: #f8f9fa;
|
||||
padding: 16px 20px;
|
||||
border-top: 2px dashed #dee2e6;
|
||||
"
|
||||
>
|
||||
<div
|
||||
style="
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
"
|
||||
>
|
||||
<div>
|
||||
<p style="margin: 0; font-size: 12px; color: #6c757d">
|
||||
<strong>IMPORTANTE:</strong> Presenta este código QR en la mesa
|
||||
de registro
|
||||
</p>
|
||||
<p style="margin: 4px 0 0 0; font-size: 11px; color: #6c757d">
|
||||
Disponible en formato digital o impreso
|
||||
</p>
|
||||
</div>
|
||||
<div style="text-align: right">
|
||||
<p
|
||||
style="
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: #003d79;
|
||||
font-weight: bold;
|
||||
"
|
||||
>
|
||||
ENTRADA VÁLIDA
|
||||
</p>
|
||||
<p style="margin: 2px 0 0 0; font-size: 11px; color: #28a745">
|
||||
✓ Verificado
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Instrucciones adicionales -->
|
||||
<div
|
||||
style="
|
||||
margin-top: 20px;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
"
|
||||
>
|
||||
<h4 style="color: #003d79; margin: 0 0 12px 0; font-size: 16px">
|
||||
📝 Instrucciones importantes:
|
||||
</h4>
|
||||
<ul
|
||||
style="
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
"
|
||||
>
|
||||
<li>Llega 15 minutos antes del inicio del evento</li>
|
||||
<li>
|
||||
Presenta este QR en la mesa de registro para validar tu asistencia
|
||||
</li>
|
||||
<li>Tu constancia será otorgada al finalizar el evento</li>
|
||||
<li>Para dudas, acude al módulo de información durante el evento</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Footer del correo -->
|
||||
<div style="text-align: center; margin-top: 20px">
|
||||
<p style="color: rgba(255, 255, 255, 0.8); font-size: 13px; margin: 0">
|
||||
Este mensaje fue enviado automáticamente. Por favor, no respondas a
|
||||
este correo.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -1,10 +1,5 @@
|
||||
import { Cuestionario } from 'src/cuestionario/entities/cuestionario.entity';
|
||||
import {
|
||||
Column,
|
||||
Entity,
|
||||
OneToMany,
|
||||
PrimaryGeneratedColumn,
|
||||
} from 'typeorm';
|
||||
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
|
||||
|
||||
@Entity()
|
||||
export class Evento {
|
||||
|
||||
@@ -225,11 +225,19 @@ export class EventoService {
|
||||
|
||||
async getEventosActivos() {
|
||||
const now = new Date();
|
||||
return this.eventoRepository.find({
|
||||
const eventos = await this.eventoRepository.find({
|
||||
where: {
|
||||
fecha_fin: MoreThan(now),
|
||||
},
|
||||
relations: ['cuestionarios'],
|
||||
});
|
||||
|
||||
return eventos.map((evento) => ({
|
||||
...evento,
|
||||
total_cuestionarios: evento.cuestionarios
|
||||
? evento.cuestionarios.length
|
||||
: 0,
|
||||
}));
|
||||
}
|
||||
|
||||
async getCuestionarioEvento(id_evento: number, id_cuestionario: number) {
|
||||
|
||||
Reference in New Issue
Block a user