diff --git a/package-lock.json b/package-lock.json index 21ca84b..eb25f65 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "blob-stream": "^0.1.3", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", + "date-fns": "^2.30.0", "exceljs": "^4.3.0", "moment": "^2.29.4", "multer": "^1.4.5-lts.1", @@ -745,6 +746,17 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/runtime": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.5.tgz", + "integrity": "sha512-ecjvYlnAaZ/KVneE/OdKYBYfgXV3Ptu6zQWmgEF7vwKhQnvVS6bjMD2XYgj+SNvQ1GfK/pjgokfPkC/2CO8CuA==", + "dependencies": { + "regenerator-runtime": "^0.13.11" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/template": { "version": "7.20.7", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", @@ -4212,9 +4224,12 @@ "dev": true }, "node_modules/date-fns": { - "version": "2.29.3", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz", - "integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==", + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, "engines": { "node": ">=0.11" }, @@ -8919,6 +8934,11 @@ "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==" }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + }, "node_modules/regexp.prototype.flags": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", @@ -11784,6 +11804,14 @@ "@babel/helper-plugin-utils": "^7.19.0" } }, + "@babel/runtime": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.5.tgz", + "integrity": "sha512-ecjvYlnAaZ/KVneE/OdKYBYfgXV3Ptu6zQWmgEF7vwKhQnvVS6bjMD2XYgj+SNvQ1GfK/pjgokfPkC/2CO8CuA==", + "requires": { + "regenerator-runtime": "^0.13.11" + } + }, "@babel/template": { "version": "7.20.7", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", @@ -14464,9 +14492,12 @@ "dev": true }, "date-fns": { - "version": "2.29.3", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz", - "integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==" + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "requires": { + "@babel/runtime": "^7.21.0" + } }, "dayjs": { "version": "1.11.7", @@ -18066,6 +18097,11 @@ "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==" }, + "regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + }, "regexp.prototype.flags": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", diff --git a/package.json b/package.json index d3d3bd6..655206f 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "blob-stream": "^0.1.3", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", + "date-fns": "^2.30.0", "exceljs": "^4.3.0", "moment": "^2.29.4", "multer": "^1.4.5-lts.1", diff --git a/public/carrusel/carrusel2.jpg b/public/carrusel/carrusel1.jpg similarity index 100% rename from public/carrusel/carrusel2.jpg rename to public/carrusel/carrusel1.jpg diff --git a/src/app.module.ts b/src/app.module.ts index 98eab76..52a4042 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -46,8 +46,6 @@ import { EmailService } from './email/email.service'; import { EmailController } from './email/email.controller'; import { EmailModule } from './email/email.module'; import { MulterModule } from '@nestjs/platform-express'; -import { CarouselController } from './carousel/carousel.controller'; -import { CarouselService } from './carousel/carousel.service'; import { ServeStaticModule } from '@nestjs/serve-static'; import { join } from 'path'; import { CarruselModule } from './carrusel/carrusel.module'; @@ -128,9 +126,8 @@ import { Carrusel } from './entities/carrusel.entity'; AppController, ParticipanteController, InscripcionStatusController, - CarouselController, ], - providers: [AppService, InscripcionStatusService, CarouselService], + providers: [AppService, InscripcionStatusService], }) export class AppModule { static port: number; diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index 2943e37..5b2b25a 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -54,9 +54,11 @@ export class AuthService { /* console.log(token); */ const data = { - usuario: usuario.nombre, - id: usuario.id_miembro, - email: usuario.email, + user: { + usuario: usuario.nombre, + id: usuario.id_miembro, + email: usuario.email, + }, token, }; diff --git a/src/carousel/carousel.controller.ts b/src/carousel/carousel.controller.ts deleted file mode 100644 index 3f670d0..0000000 --- a/src/carousel/carousel.controller.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { - BadRequestException, - Controller, - Delete, - Get, - HttpException, - HttpStatus, - Param, - Post, - Res, - UploadedFile, - UseInterceptors, -} from '@nestjs/common'; -import { FileInterceptor } from '@nestjs/platform-express'; -import { Response } from 'express'; -import * as fs from 'fs'; -import { diskStorage } from 'multer'; -import * as path from 'path'; - -@Controller('carousel') -export class CarouselController { - private images: string[] = []; - - @Post('/imagenes') - @UseInterceptors( - FileInterceptor('file', { - storage: diskStorage({ - destination: './public', - filename: function (req, file, cb) { - const directory = './public'; - const filePath = `${directory}/${file.originalname}`; - if (fs.existsSync(filePath)) { - cb( - new BadRequestException( - `Ya existe un archivo con el nombre: '${file.originalname}'. Si está seguro de que no es el mismo, cambie el nombre del archivo.`, - ), - null, - ); - } else { - cb(null, file.originalname); - } - }, - }), - fileFilter: function (req, file, cb) { - const filetypes = /jpeg|jpg|webp|png/; - const extname = filetypes.test( - path.extname(file.originalname).toLowerCase(), - ); - const mimetype = filetypes.test(file.mimetype); - if (mimetype && extname) { - return cb(null, true); - } - cb( - new BadRequestException( - 'Lo sentimos, para mejorar la calidad de las imagenes en el carousel solo se aceptan imagenes con terminación: .jpeg .jpg .png .webp', - ), - false, - ); - }, - }), - ) - async addImage(@UploadedFile() file) { - return { msg: 'Imagen guardada con exito :)' }; - } - - @Delete('/imagenes/:nombreArchivo') - async deleteImage(@Param('nombreArchivo') nombreArchivo: string) { - try { - await fs.promises.unlink(`./public/${nombreArchivo}`); - return { message: 'Imagen eliminada exitosamente' }; - } catch (err) { - throw new Error('Error al eliminar la imagen'); - } - } - - @Get('/imagenes') - async getImages(@Res() res: Response) { - // Leer el directorio 'public' para obtener el nombre de todos los archivos - - const fileNames = await fs.promises.readdir('./public'); - // Generar la URL para cada archivo y agregarla a un arreglo - - // Filtrar solo las imágenes con las terminaciones "jpg", "webp" y "png" - const imageNames = fileNames.filter((fileName) => - ['.jpg', '.webp', '.png'].includes(path.extname(fileName)), - ); - - const imageUrls = imageNames.map((imageName) => `public/${imageName}`); - // Devolver el arreglo de URLs como respuesta - return res.json(imageUrls); - } -} diff --git a/src/carousel/carousel.service.ts b/src/carousel/carousel.service.ts deleted file mode 100644 index a6c6fbb..0000000 --- a/src/carousel/carousel.service.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -@Injectable() -export class CarouselService { - - uploadImage(file){ - /* console.log(file) */ - } -} diff --git a/src/email/DisenoConstancia.pdf b/src/email/DisenoConstancia.pdf new file mode 100644 index 0000000..5918a99 Binary files /dev/null and b/src/email/DisenoConstancia.pdf differ diff --git a/src/email/email.controller.ts b/src/email/email.controller.ts index d958447..256d533 100644 --- a/src/email/email.controller.ts +++ b/src/email/email.controller.ts @@ -8,6 +8,7 @@ import { Post, Res, UploadedFile, + UseGuards, UseInterceptors, } from '@nestjs/common'; import { FileInterceptor } from '@nestjs/platform-express'; @@ -15,27 +16,36 @@ import { EmailDto } from './dto/emailDto.dto'; import { EmailService } from './email.service'; import { Response } from 'express'; import * as xlsx from 'xlsx'; - +import { AuthGuard } from '@nestjs/passport'; @Controller('email') export class EmailController { constructor(private readonly emailService: EmailService) {} + @UseGuards(AuthGuard('jwt')) @Post('/evento/:id') async sendEmail( @Param('id', ParseIntPipe) id: number, @Body() emailDto: EmailDto, + @Res() res, ): Promise { - return this.emailService.sendEmail(id, emailDto); + return this.emailService.sendEmail(id, emailDto, res); } - @Post('constancias') + @UseGuards(AuthGuard('jwt')) + @Post('constancias/:id') @UseInterceptors(FileInterceptor('file')) - async sendConstancias(@UploadedFile() file, @Body('nombreEvento') nombreEvento: string,) { - if(!file) { - throw new HttpException("No se ha cargado ningun archivo", HttpStatus.BAD_REQUEST) + async sendConstancias( + @UploadedFile() file, + @Param('id', ParseIntPipe) id: number, + @Res() res, + ) { + if (!file) { + throw new HttpException( + 'No se ha cargado ningun archivo', + HttpStatus.BAD_REQUEST, + ); } - /* console.log(file) */ - return this.emailService.sendConstancias(file, nombreEvento); + return this.emailService.sendConstancias(file, id, res); } } diff --git a/src/email/email.module.ts b/src/email/email.module.ts index 39df2ed..941deba 100644 --- a/src/email/email.module.ts +++ b/src/email/email.module.ts @@ -4,10 +4,13 @@ import { EventosModule } from 'src/eventos/eventos.module'; import { Participante } from 'src/participante/participante.entity'; import { EmailController } from './email.controller'; import { EmailService } from './email.service'; +import { EventosService } from 'src/eventos/eventos.service'; +import { EventoParticipante } from 'src/evento-participante/evento-participante/eventoParticipante.entity'; +import { Evento } from 'src/eventos/evento.entity'; @Module({ - imports:[TypeOrmModule.forFeature([Participante])], + imports:[TypeOrmModule.forFeature([Participante, EventoParticipante, Evento])], controllers:[EmailController], - providers:[EmailService] + providers:[EmailService, EventosService] }) export class EmailModule {} diff --git a/src/email/email.service.ts b/src/email/email.service.ts index 3ec96e4..8cb0f14 100644 --- a/src/email/email.service.ts +++ b/src/email/email.service.ts @@ -1,4 +1,11 @@ -import { HttpException, HttpStatus, Inject, Injectable, Res, UploadedFile } from '@nestjs/common'; +import { + HttpException, + HttpStatus, + Inject, + Injectable, + Res, + UploadedFile, +} from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { InjectRepository } from '@nestjs/typeorm'; import * as nodemailer from 'nodemailer'; @@ -13,6 +20,8 @@ import * as pdfFonts from 'pdfmake/build/vfs_fonts'; import * as fs from 'fs'; import * as path from 'path'; import { PDFDocument, StandardFonts, rgb } from 'pdf-lib'; +import { Response } from 'express'; +import { format } from 'date-fns'; @Injectable() export class EmailService { @@ -22,6 +31,7 @@ export class EmailService { @InjectRepository(Participante) private participanteRepository: Repository, private configService: ConfigService, + private readonly eventoService: EventosService, ) { this.transporter = nodemailer.createTransport({ service: this.configService.get('NODEMAILER_SERVICE'), @@ -32,7 +42,7 @@ export class EmailService { }); } - async sendEmail(id: number, message: EmailDto) { + async sendEmail(id: number, message: EmailDto, res: Response) { const participantes = await this.getEmailDeParticipantes(id); participantes.map((res) => { const emailDestinatario = res.email; @@ -41,8 +51,8 @@ export class EmailService { subject: message.subject, text: message.text, }); - /*console.log("Email enviado a: ", res)*/ }); + return res.status(201).json({ message: 'Correos enviados con exito' }); } async getEmailDeParticipantes(idEvento: number) { @@ -57,63 +67,113 @@ export class EmailService { return participantes; } - async sendConstancias(file, nombreEvento) { - /* console.log(file) */ + async sendConstancias(file, id_evento, res: Response) { + //traemos la info del evento + const evento = await this.eventoService.getEventosPorId(id_evento); + const workbook = xlsx.read(file.buffer); const worksheet = workbook.Sheets[workbook.SheetNames[0]]; const data = xlsx.utils.sheet_to_json(worksheet); - for (const row of data as Row[]) { - const pdfDoc = await PDFDocument.create(); - const page = pdfDoc.addPage(); - page.setSize(792, 612); + // Carga el PDF pre-diseñado + const existingPdfBytes = fs.readFileSync('src/email/DisenoConstancia.pdf'); - const font = await pdfDoc.embedFont(StandardFonts.HelveticaBold); - const fontSize = 30; - const text = 'Constancia de asistencia'; - const textWidth = font.widthOfTextAtSize(text, fontSize); - const textHeight = font.heightAtSize(fontSize); + for (let i = 0; i < data.length; i++) { + const row = data[i] as Row; - page.drawText(text, { - x: (page.getWidth() - textWidth) / 2, - y: (page.getHeight() - textHeight) / 2, + if(row.Email == undefined){ + return res.status(400).json({ message: `El correo del renglon ${ i + 2} puede estar vacio o ser undefined`, solucion: `Seleccione el renglon donde ocurre el problema y borre todo lo de este renglon, Aunque parezca que el renglon esta vacio puede que haya un espacio en algun campo y esto genera que el sistema haga la lectura de este renglon`}); + } + + // Carga el PDF pre-diseñado en un documento PDF editable + const pdfDoc = await PDFDocument.load(existingPdfBytes); + + const pagina = pdfDoc.getPages(); + const primeraPagina = pagina[0]; + + // Obtén el ancho y alto de la página + const { width, height } = primeraPagina.getSize(); + + // Obtén la fuente del texto + const font = await pdfDoc.embedFont(StandardFonts.Helvetica); + + // Configura el tamaño del texto + const fontSize = 26; + + const nombreParticipante = `${row['Apellido Paterno']} ${row['Apellido Materno']} ${row.Nombre}`; + + // Calcula la anchura del texto del nombre del participante + const textWidth = font.widthOfTextAtSize(nombreParticipante, fontSize); + + const centerX = (width - textWidth) / 2; + const centerY = (height - fontSize) / 2; + + // Dibuja el texto centrado en la página + primeraPagina.drawText(nombreParticipante, { + x: centerX, + y: centerY + 70, size: fontSize, - font: font, - color: rgb(0/255, 0/255, 0/255), + color: rgb(0, 0, 0), }); - const { width, height } = page.getSize(); - page.drawText( - `Se le otorga la constancia por su asistencia al evento: ${nombreEvento}`, - { - x: 50, - y: height - 50, - size: 12, - font: await pdfDoc.embedFont('Helvetica'), - }, - ); - page.drawText( - `A ${row['Apellido Paterno']} ${row['Apellido Materno']} ${row.Nombre}`, - { - x: 50, - y: height - 80, - size: 12, - font: await pdfDoc.embedFont('Helvetica'), - }, - ); + const nombreCurso = `${evento.nombre}`; + + // Calcula la anchura del texto del nombre del evento + const nombreCursoWidth = font.widthOfTextAtSize(nombreCurso, fontSize); + + const centerXNombreCurso = (width - nombreCursoWidth) / 2; + const centerYNombreCurso = (height - fontSize) / 2; + + primeraPagina.drawText(nombreCurso, { + x: centerXNombreCurso, + y: centerYNombreCurso - 17, + size: fontSize, + color: rgb(0, 0, 0), + }); + + const fechaCurso = `del ${this.formatoFecha( + evento.fecha_inicio, + )} al ${this.formatoFecha(evento.fecha_fin)}`; + + // Calcula la anchura del texto del nombre del participante + const fechaCursoWidth = font.widthOfTextAtSize(fechaCurso, 20); + + const centerXFechaCurso = (width - fechaCursoWidth) / 2; + const centerYFechaCurso = (height - 20) / 2; + + primeraPagina.drawText(fechaCurso, { + x: centerXFechaCurso, + y: centerYFechaCurso - 55, + size: 20, + color: rgb(0, 0, 0), + }); + + // Guarda el PDF modificado en forma de bytes const pdfBytes = await pdfDoc.save(); + + // Crea el objeto de adjunto con el nombre y contenido del PDF const attachment = { - filename: `Constancia ${nombreEvento} - ${row.Nombre}_${row['Apellido Paterno']}.pdf`, + filename: `Constancia ${evento.nombre} - ${row.Nombre}_${row['Apellido Paterno']}.pdf`, content: pdfBytes, }; - /* const blob = new Blob([pdfBytes], { type: 'application/pdf' }); - const url = URL.createObjectURL(blob); */ + + // Crea el objeto Blob y la URL para el PDF + const blob = new Blob([pdfBytes], { type: 'application/pdf' }); + + // Envía el correo electrónico con la constancia adjunta this.transporter.sendMail({ to: row.Email, - subject: `Constancia de asistencia a "${nombreEvento}"`, - text: `¡Un saludo ${row.Nombre}! Aquí está tu constancia de asistencia por el evento "${nombreEvento}". Agradecemos tu asistencia y constancia, esperamos seguir viéndote en eventos futuros.`, + subject: `Constancia de asistencia a "${evento.nombre}"`, + text: `¡Un saludo ${row.Nombre}! Aquí está tu constancia de asistencia por el evento "${evento.nombre}". Agradecemos tu asistencia y constancia, esperamos seguir viéndote en eventos futuros.`, attachments: [attachment], }); } + + return res.status(200).json({ message: 'Constancias enviadas con éxito' }); + } + + formatoFecha(fecha: Date) { + const fechaLimpia = format(fecha, 'dd/MM/yyyy'); + return fechaLimpia; } } diff --git a/src/eventos/eventos.controller.ts b/src/eventos/eventos.controller.ts index 858b073..a484583 100644 --- a/src/eventos/eventos.controller.ts +++ b/src/eventos/eventos.controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post, Res } from '@nestjs/common'; +import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post, Res, UseGuards } from '@nestjs/common'; import { Response } from 'express'; import { InjectRepository } from '@nestjs/typeorm'; import { EventoParticipante } from 'src/evento-participante/evento-participante/eventoParticipante.entity'; @@ -7,6 +7,7 @@ import { actualizarEventoDto } from './dto/actualizarEvento.dto'; import { crearEventoDto } from './dto/crearEvento.dto'; import { Evento } from './evento.entity'; import { EventosService } from './eventos.service'; +import { AuthGuard } from '@nestjs/passport'; @Controller('eventos') export class EventosController { @@ -23,16 +24,19 @@ export class EventosController { return this.eventoService.getEventosPorId(id) } + @UseGuards(AuthGuard('jwt')) @Post() postEvento(@Body() nuevoEvento: crearEventoDto){ return this.eventoService.postEvento(nuevoEvento) } + @UseGuards(AuthGuard('jwt')) @Patch(':id') patchEvento(@Param('id', ParseIntPipe)id:number, @Body() actualizacion: actualizarEventoDto){ return this.eventoService.updateEvento(id,actualizacion) } - + + @UseGuards(AuthGuard('jwt')) @Delete(':id') deleteEvento(@Param('id', ParseIntPipe)id: number){ return this.eventoService.deleteEventoParticipante(id) @@ -43,11 +47,13 @@ export class EventosController { return this.eventoService.getCupos(id) } + @UseGuards(AuthGuard('jwt')) @Get(':id/participante') async getParticipnates(@Param('id', ParseIntPipe)id:number){ return this.eventoService.getParticipantes(id) } + @UseGuards(AuthGuard('jwt')) @Get(':id/exportExcel') async exportarExcel (@Param('id', ParseIntPipe)id:number ,@Res() res: Response){ this.eventoService.exportarParticipantes(id, res) diff --git a/src/eventos/eventos.service.ts b/src/eventos/eventos.service.ts index e322a52..394fbe6 100644 --- a/src/eventos/eventos.service.ts +++ b/src/eventos/eventos.service.ts @@ -23,7 +23,7 @@ export class EventosService { return this.eventoRepository.find(); } - getEventosPorId(id: number) { + public getEventosPorId(id: number) { return this.eventoRepository.findOne({ where: { id_evento: id,