diff --git a/package-lock.json b/package-lock.json index cd34f6e..bb96032 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,7 +33,8 @@ "qrcode": "^1.5.4", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", - "typeorm": "^0.3.21" + "typeorm": "^0.3.21", + "xlsx": "^0.18.5" }, "devDependencies": { "@eslint/eslintrc": "^3.2.0", @@ -4085,6 +4086,15 @@ "node": ">=0.4.0" } }, + "node_modules/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -4870,6 +4880,19 @@ ], "license": "CC-BY-4.0" }, + "node_modules/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -5095,6 +5118,15 @@ "node": ">= 0.12.0" } }, + "node_modules/codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/collect-v8-coverage": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", @@ -5300,6 +5332,18 @@ } } }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/create-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", @@ -6697,6 +6741,15 @@ "node": ">= 0.6" } }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/fresh": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", @@ -10536,6 +10589,18 @@ "node": ">= 0.6" } }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "license": "Apache-2.0", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -12126,6 +12191,24 @@ "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", "license": "ISC" }, + "node_modules/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -12237,6 +12320,27 @@ "dev": true, "license": "ISC" }, + "node_modules/xlsx": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 89f9c62..f56500c 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,8 @@ "qrcode": "^1.5.4", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", - "typeorm": "^0.3.21" + "typeorm": "^0.3.21", + "xlsx": "^0.18.5" }, "devDependencies": { "@eslint/eslintrc": "^3.2.0", diff --git a/src/alumnos/alumnos.controller.ts b/src/alumnos/alumnos.controller.ts index 61b8a9c..44c406d 100644 --- a/src/alumnos/alumnos.controller.ts +++ b/src/alumnos/alumnos.controller.ts @@ -1,5 +1,5 @@ import { Controller, Get, Param, NotFoundException } from '@nestjs/common'; -import { AlumnosService } from './alumnos.service'; +import { AlumnosService, UsuarioData } from './alumnos.service'; import { ApiTags, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; import { AlumnoDto, ALUMNO_RESPONSES } from './alumnos.documentation'; @@ -14,7 +14,7 @@ export class AlumnosController { @ApiResponse(ALUMNO_RESPONSES[200]) @ApiResponse(ALUMNO_RESPONSES[404]) @ApiResponse(ALUMNO_RESPONSES[500]) - async getAlumnoByCuenta(@Param('cuenta') cuenta: string): Promise { + async getAlumnoByCuenta(@Param('cuenta') cuenta: string): Promise { const alumno = await this.alumnosService.findByCuenta(cuenta); if (!alumno) { throw new NotFoundException(`No se encontró alumno con número de cuenta ${cuenta}`); diff --git a/src/alumnos/alumnos.service.ts b/src/alumnos/alumnos.service.ts index bda130a..bef0f1a 100644 --- a/src/alumnos/alumnos.service.ts +++ b/src/alumnos/alumnos.service.ts @@ -3,6 +3,15 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { RegistroAlumno } from './entities/registro-alumno.entity'; +export type UsuarioData = { + cuenta: string | null; + nombre: string | null; + apellidos: string | null; + carrera: string | null; + genero: string | null; + rfc?: string | null; // Solo para trabajadores +} + @Injectable() export class AlumnosService { constructor( @@ -10,9 +19,17 @@ export class AlumnosService { private registroAlumnoRepository: Repository, ) {} - async findByCuenta(cuenta: string): Promise { - return this.registroAlumnoRepository.findOne({ + async findByCuenta(cuenta: string): Promise { + const alumno = await this.registroAlumnoRepository.findOne({ where: { id_ncuenta: parseInt(cuenta, 10) }, }); + + return { + cuenta: alumno?.id_ncuenta.toString() || null, + nombre: alumno?.nombre || null, + apellidos: alumno?.apellidos || null, + carrera: alumno?.carrera || null, + genero: alumno?.genero || null, + } } } diff --git a/src/cuestionario_respondido/cuestionario_respondido.service.ts b/src/cuestionario_respondido/cuestionario_respondido.service.ts index ed33e89..ad16345 100644 --- a/src/cuestionario_respondido/cuestionario_respondido.service.ts +++ b/src/cuestionario_respondido/cuestionario_respondido.service.ts @@ -261,6 +261,12 @@ export class CuestionarioRespondidoService { 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: [ { diff --git a/src/emails/registro-evento.ts b/src/emails/registro-evento.ts index 1c23818..c533d66 100644 --- a/src/emails/registro-evento.ts +++ b/src/emails/registro-evento.ts @@ -3,12 +3,27 @@ export function generarHtmlCorreoAsistencia({ nombreEvento, correo, fechaRegistro, + fechaInicioEvento, + fechaFinEvento, }: { nombreForm: string; nombreEvento: string; correo: string; fechaRegistro: string; + fechaInicioEvento?: string; + fechaFinEvento?: string; }) { + const horarioEvento = fechaInicioEvento && fechaFinEvento + ? ` +
+

+ 🕒 Horario del evento:
+ ${fechaInicioEvento} al ${fechaFinEvento} +

+
+ ` + : ''; + return `

🎓 ¡Gracias por registrarte!

@@ -32,6 +47,8 @@ export function generarHtmlCorreoAsistencia({
  • Fecha de registro: ${fechaRegistro}
  • + ${horarioEvento} +

    Si tienes alguna duda, acude al módulo de información durante el evento.

    diff --git a/src/trabajadores/docs/trabajadores.docs.ts b/src/trabajadores/docs/trabajadores.docs.ts new file mode 100644 index 0000000..967720b --- /dev/null +++ b/src/trabajadores/docs/trabajadores.docs.ts @@ -0,0 +1,30 @@ +import { applyDecorators } from '@nestjs/common'; +import { + ApiTags, + ApiOperation, + ApiConsumes, + ApiBody, +} from '@nestjs/swagger'; + +export class TrabajadorApiDocumentation { + static ApiController = ApiTags('Trabajador'); + + static ApiCargarArchivo = applyDecorators( + ApiOperation({ summary: 'Cargar archivo Excel con trabajadores' }), + ApiConsumes('multipart/form-data'), + ApiBody({ + description: 'Archivo Excel (.xls o .xlsx) con columnas: rfc, num_empleado, nombre, adscripcion, correo, AP1, AP2, Nombres, sexo', + schema: { + type: 'object', + properties: { + file: { + type: 'string', + format: 'binary', + description: 'Archivo Excel con trabajadores a registrar', + }, + }, + required: ['file'], + }, + }), + ); +} diff --git a/src/trabajadores/entities/trabajadore.entity.ts b/src/trabajadores/entities/trabajadore.entity.ts index 75f1df8..4b4c415 100644 --- a/src/trabajadores/entities/trabajadore.entity.ts +++ b/src/trabajadores/entities/trabajadore.entity.ts @@ -5,8 +5,8 @@ export class RegistroTrabajador { @PrimaryGeneratedColumn() id_trabajador: number; - @Column({ type: 'varchar', length: 20, unique: true, nullable: true }) - numero_trabajador: string; + @Column({ type: 'int', unique: true, nullable: true }) + num_trabajador: number; @Column({ type: 'varchar', length: 13, unique: true }) rfc: string; @@ -19,4 +19,7 @@ export class RegistroTrabajador { @Column({ type: 'char', length: 1 }) genero: string; + + @Column({ type: 'varchar', length: 100 }) + carrera: string; } diff --git a/src/trabajadores/trabajadores.controller.ts b/src/trabajadores/trabajadores.controller.ts index ab41cdb..a10fe84 100644 --- a/src/trabajadores/trabajadores.controller.ts +++ b/src/trabajadores/trabajadores.controller.ts @@ -1,29 +1,64 @@ -import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common'; +import { + Controller, + Get, + Post, + Body, + Patch, + Param, + Delete, + UseInterceptors, + UploadedFile, + NotFoundException, +} from '@nestjs/common'; import { TrabajadoresService } from './trabajadores.service'; import { CreateTrabajadoreDto } from './dto/create-trabajadore.dto'; import { UpdateTrabajadoreDto } from './dto/update-trabajadore.dto'; +import { FileInterceptor } from '@nestjs/platform-express'; +import { diskStorage } from 'multer'; +import { extname } from 'path'; +import { TrabajadorApiDocumentation } from './docs/trabajadores.docs'; @Controller('trabajadores') +@TrabajadorApiDocumentation.ApiController export class TrabajadoresController { constructor(private readonly trabajadoresService: TrabajadoresService) {} + @Post('cargar') + @TrabajadorApiDocumentation.ApiCargarArchivo + @UseInterceptors( + FileInterceptor('file', { + storage: diskStorage({ + destination: './uploads', + filename: (_, file, callback) => { + const uniqueName = `${Date.now()}${extname(file.originalname)}`; + callback(null, uniqueName); + }, + }), + }), + ) + async cargarArchivo(@UploadedFile() file: Express.Multer.File) { + return this.trabajadoresService.procesarArchivo(file.path); + } + @Post() create(@Body() createTrabajadoreDto: CreateTrabajadoreDto) { return this.trabajadoresService.create(createTrabajadoreDto); } - @Get() - findAll() { - return this.trabajadoresService.findAll(); - } - - @Get(':id') - findOne(@Param('id') id: string) { - return this.trabajadoresService.findOne(+id); + @Get(':rfc') + findByRfc(@Param('rfc') rfc: string) { + const trabajador = this.trabajadoresService.findByRfc(rfc); + if (!trabajador) { + throw new NotFoundException(`No se encontró trabajador con RFC ${rfc}`); + } + return trabajador; } @Patch(':id') - update(@Param('id') id: string, @Body() updateTrabajadoreDto: UpdateTrabajadoreDto) { + update( + @Param('id') id: string, + @Body() updateTrabajadoreDto: UpdateTrabajadoreDto, + ) { return this.trabajadoresService.update(+id, updateTrabajadoreDto); } diff --git a/src/trabajadores/trabajadores.module.ts b/src/trabajadores/trabajadores.module.ts index 7f7d44c..d9f8855 100644 --- a/src/trabajadores/trabajadores.module.ts +++ b/src/trabajadores/trabajadores.module.ts @@ -5,9 +5,11 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { RegistroTrabajador } from './entities/trabajadore.entity'; @Module({ - imports: [TypeOrmModule.forFeature([RegistroTrabajador], 'alumnosConnection')], - + imports: [ + TypeOrmModule.forFeature([RegistroTrabajador], 'alumnosConnection'), + ], controllers: [TrabajadoresController], providers: [TrabajadoresService], + exports: [TrabajadoresService], }) export class TrabajadoresModule {} diff --git a/src/trabajadores/trabajadores.service.ts b/src/trabajadores/trabajadores.service.ts index 13a1aef..4aa8355 100644 --- a/src/trabajadores/trabajadores.service.ts +++ b/src/trabajadores/trabajadores.service.ts @@ -1,9 +1,91 @@ import { Injectable } from '@nestjs/common'; import { CreateTrabajadoreDto } from './dto/create-trabajadore.dto'; import { UpdateTrabajadoreDto } from './dto/update-trabajadore.dto'; +import { InjectRepository } from '@nestjs/typeorm'; +import { RegistroTrabajador } from './entities/trabajadore.entity'; +import { Repository } from 'typeorm'; +import * as fs from 'fs'; +import * as XLSX from 'xlsx'; +import { UsuarioData } from 'src/alumnos/alumnos.service'; @Injectable() export class TrabajadoresService { + constructor( + @InjectRepository(RegistroTrabajador, 'alumnosConnection') + private readonly trabajadorRepo: Repository, + ) {} + + async procesarArchivo( + filePath: string, + ): Promise<{ insertados: number; omitidos: number }> { + const workbook = XLSX.readFile(filePath); + const sheetName = workbook.SheetNames[0]; + const data: any[] = XLSX.utils.sheet_to_json(workbook.Sheets[sheetName]); + + let insertados = 0; + let omitidos = 0; + + for (const row of data) { + const rfc = (row['rfc'] || '').trim(); + + if (!rfc) { + omitidos++; + continue; // RFC es obligatorio + } + + const numTrabajadorRaw = row['num_empleado']; + const numTrabajador = Number.isFinite(Number(numTrabajadorRaw)) + ? Number(numTrabajadorRaw) + : undefined; + + const where: any[] = [{ rfc }]; + if (numTrabajador !== undefined) { + where.push({ num_trabajador: numTrabajador }); + } + + const existe = await this.trabajadorRepo.findOne({ where }); + + if (existe) { + omitidos++; + continue; + } + + const nuevo = this.trabajadorRepo.create({ + rfc, + num_trabajador: numTrabajador, + nombre: (row['Nombres'] || '').trim(), + apellidos: + `${(row['AP1'] || '').trim()} ${(row['AP2'] || '').trim()}`.trim(), + genero: (row['sexo'] || '').trim().substring(0, 1).toUpperCase(), + carrera: (row['adscripcion'] || '').trim(), + }); + + await this.trabajadorRepo.save(nuevo); + insertados++; + } + + fs.unlinkSync(filePath); // Limpia archivo temporal + + return { insertados, omitidos }; + } + + async findByRfc(rfc: string): Promise { + const trabajador = await this.trabajadorRepo.findOne({ + where: { rfc: rfc.trim().toUpperCase() }, + }); + + return { + cuenta: trabajador?.num_trabajador?.toString() || null, + nombre: trabajador?.nombre || null, + apellidos: trabajador + ? `${trabajador.apellidos}`.trim() + : null, + carrera: trabajador?.carrera || null, + genero: trabajador?.genero || null, + rfc: trabajador?.rfc || null, // Incluye RFC en el retorno + } + } + create(createTrabajadoreDto: CreateTrabajadoreDto) { return 'This action adds a new trabajadore'; }