From aa2a0f1073086e5e69610e6fc275e0ee4be9ee26 Mon Sep 17 00:00:00 2001
From: miguel
Date: Sun, 8 Mar 2026 19:19:30 -0600
Subject: [PATCH 1/2] =?UTF-8?q?Implementar=20carga=20de=20usuarios=20desde?=
=?UTF-8?q?=20archivo=20Excel=20y=20validaci=C3=B3n=20de=20lista=20restrin?=
=?UTF-8?q?gida=20para=20eventos?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/alumnos/alumnos.controller.ts | 79 +++++++-
src/alumnos/alumnos.module.ts | 6 +-
src/alumnos/alumnos.service.ts | 103 +++++++++-
src/app.module.ts | 2 +
src/cuestionario/cuestionario.service.ts | 2 +
.../dto/create-cuestionario.dto.ts | 9 +
.../entities/cuestionario.entity.ts | 3 +
.../cuestionario_respondido.module.ts | 2 +
.../cuestionario_respondido.service.ts | 68 ++++++-
src/db/typeorm.config.ts | 6 +-
src/emails/registro-evento.ts | 13 +-
src/evento/docs/evento.documentation.ts | 27 +++
src/evento/evento.controller.ts | 42 +++-
src/evento/evento.module.ts | 8 +-
.../entities/evento_usuario.entity.ts | 34 ++++
src/evento_usuario/evento_usuario.module.ts | 17 ++
src/evento_usuario/evento_usuario.service.ts | 189 ++++++++++++++++++
src/trabajadores/trabajadores.controller.ts | 10 +-
src/trabajadores/trabajadores.module.ts | 2 +
src/trabajadores/trabajadores.service.ts | 44 +++-
20 files changed, 628 insertions(+), 38 deletions(-)
create mode 100644 src/evento_usuario/entities/evento_usuario.entity.ts
create mode 100644 src/evento_usuario/evento_usuario.module.ts
create mode 100644 src/evento_usuario/evento_usuario.service.ts
diff --git a/src/alumnos/alumnos.controller.ts b/src/alumnos/alumnos.controller.ts
index 44c406d..bc6169a 100644
--- a/src/alumnos/alumnos.controller.ts
+++ b/src/alumnos/alumnos.controller.ts
@@ -1,23 +1,92 @@
-import { Controller, Get, Param, NotFoundException } from '@nestjs/common';
+import {
+ Controller,
+ Get,
+ NotFoundException,
+ Param,
+ Post,
+ Query,
+ UnprocessableEntityException,
+ UploadedFile,
+ UseInterceptors,
+} from '@nestjs/common';
import { AlumnosService, UsuarioData } from './alumnos.service';
-import { ApiTags, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger';
+import {
+ ApiBody,
+ ApiConsumes,
+ ApiOperation,
+ ApiParam,
+ ApiQuery,
+ ApiResponse,
+ ApiTags,
+} from '@nestjs/swagger';
import { AlumnoDto, ALUMNO_RESPONSES } from './alumnos.documentation';
+import { FileInterceptor } from '@nestjs/platform-express';
+import { diskStorage } from 'multer';
+import { extname } from 'path';
@ApiTags('alumnos')
@Controller('alumnos')
export class AlumnosController {
constructor(private readonly alumnosService: AlumnosService) {}
+ @Post('cargar')
+ @ApiOperation({ summary: 'Cargar archivo Excel con alumnos' })
+ @ApiConsumes('multipart/form-data')
+ @ApiBody({
+ schema: {
+ type: 'object',
+ properties: {
+ file: {
+ type: 'string',
+ format: 'binary',
+ description: 'Archivo Excel (.xlsx) con columnas: id_ncuenta, nombre, apellidos, carrera, genero',
+ },
+ },
+ required: ['file'],
+ },
+ })
+ @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) {
+ if (!file) {
+ throw new UnprocessableEntityException('No se ha subido ningún archivo.');
+ }
+ return this.alumnosService.procesarArchivo(file.path);
+ }
+
@Get(':cuenta')
@ApiOperation({ summary: 'Obtener datos de un alumno por su número de cuenta' })
@ApiParam({ name: 'cuenta', description: 'Número de cuenta del alumno' })
+ @ApiQuery({
+ name: 'id_evento',
+ required: false,
+ type: Number,
+ description:
+ 'ID del evento para validar si el alumno está en la lista restringida',
+ })
@ApiResponse(ALUMNO_RESPONSES[200])
@ApiResponse(ALUMNO_RESPONSES[404])
@ApiResponse(ALUMNO_RESPONSES[500])
- async getAlumnoByCuenta(@Param('cuenta') cuenta: string): Promise {
- const alumno = await this.alumnosService.findByCuenta(cuenta);
+ async getAlumnoByCuenta(
+ @Param('cuenta') cuenta: string,
+ @Query('id_evento') idEventoRaw?: string,
+ ): Promise {
+ const id_evento =
+ idEventoRaw !== undefined ? parseInt(idEventoRaw, 10) : undefined;
+ const alumno = await this.alumnosService.findByCuenta(cuenta, id_evento);
if (!alumno) {
- throw new NotFoundException(`No se encontró alumno con número de cuenta ${cuenta}`);
+ throw new NotFoundException(
+ `No se encontró alumno con número de cuenta ${cuenta}`,
+ );
}
return alumno;
}
diff --git a/src/alumnos/alumnos.module.ts b/src/alumnos/alumnos.module.ts
index ff25a76..605eade 100644
--- a/src/alumnos/alumnos.module.ts
+++ b/src/alumnos/alumnos.module.ts
@@ -3,9 +3,13 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { AlumnosController } from './alumnos.controller';
import { AlumnosService } from './alumnos.service';
import { RegistroAlumno } from './entities/registro-alumno.entity';
+import { EventoUsuarioModule } from '../evento_usuario/evento_usuario.module';
@Module({
- imports: [TypeOrmModule.forFeature([RegistroAlumno], 'alumnosConnection')],
+ imports: [
+ TypeOrmModule.forFeature([RegistroAlumno], 'alumnosConnection'),
+ EventoUsuarioModule,
+ ],
controllers: [AlumnosController],
providers: [AlumnosService],
exports: [AlumnosService],
diff --git a/src/alumnos/alumnos.service.ts b/src/alumnos/alumnos.service.ts
index bef0f1a..e5f9288 100644
--- a/src/alumnos/alumnos.service.ts
+++ b/src/alumnos/alumnos.service.ts
@@ -1,7 +1,14 @@
-import { Injectable } from '@nestjs/common';
+import {
+ ForbiddenException,
+ Injectable,
+ NotFoundException,
+} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { RegistroAlumno } from './entities/registro-alumno.entity';
+import { EventoUsuarioService } from '../evento_usuario/evento_usuario.service';
+import * as fs from 'fs';
+import * as XLSX from 'xlsx';
export type UsuarioData = {
cuenta: string | null;
@@ -10,26 +17,102 @@ export type UsuarioData = {
carrera: string | null;
genero: string | null;
rfc?: string | null; // Solo para trabajadores
-}
+};
@Injectable()
export class AlumnosService {
constructor(
@InjectRepository(RegistroAlumno, 'alumnosConnection')
private registroAlumnoRepository: Repository,
+ private readonly eventoUsuarioService: EventoUsuarioService,
) {}
- async findByCuenta(cuenta: string): Promise {
+ 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]);
+ console.log(filePath);
+
+ let insertados = 0;
+ let omitidos = 0;
+
+ for (const row of data) {
+ console.log(row);
+ const cuentaRaw = row['id_ncuenta'];
+ const cuenta = Number.isFinite(Number(cuentaRaw))
+ ? Number(cuentaRaw)
+ : null;
+
+ if (!cuenta) {
+ omitidos++;
+ continue;
+ }
+
+ const existe = await this.registroAlumnoRepository.findOne({
+ where: { id_ncuenta: cuenta },
+ });
+
+ if (existe) {
+ omitidos++;
+ continue;
+ }
+
+ const nuevo = this.registroAlumnoRepository.create({
+ id_ncuenta: cuenta,
+ nombre: (row['nombre'] || '').trim(),
+ apellidos: (row['apellidos'] || '').trim(),
+ carrera: (row['carrera'] || '').trim(),
+ genero: (row['genero'] || '').trim().substring(0, 1).toUpperCase(),
+ });
+
+ await this.registroAlumnoRepository.save(nuevo);
+ insertados++;
+ }
+
+ fs.unlinkSync(filePath);
+
+ return { insertados, omitidos };
+ }
+
+ async findByCuenta(
+ cuenta: string,
+ id_evento?: number,
+ ): 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,
+
+ if (!alumno) {
+ throw new NotFoundException(
+ `No se encontró alumno con número de cuenta ${cuenta}`,
+ );
}
+
+ if (id_evento !== undefined) {
+ const tieneLista =
+ await this.eventoUsuarioService.eventoTieneListaRestringida(id_evento);
+
+ if (tieneLista) {
+ const enLista = await this.eventoUsuarioService.identificadorEnLista(
+ id_evento,
+ alumno.id_ncuenta.toString(),
+ );
+ if (!enLista) {
+ throw new ForbiddenException(
+ `El alumno con cuenta ${cuenta} no está en la lista de usuarios habilitados para este evento.`,
+ );
+ }
+ }
+ }
+
+ return {
+ cuenta: alumno.id_ncuenta.toString(),
+ nombre: alumno.nombre,
+ apellidos: alumno.apellidos,
+ carrera: alumno.carrera,
+ genero: alumno.genero,
+ };
}
}
diff --git a/src/app.module.ts b/src/app.module.ts
index 471d82b..89100f1 100644
--- a/src/app.module.ts
+++ b/src/app.module.ts
@@ -29,6 +29,7 @@ import { ValidacionesModule } from './validaciones/validaciones.module';
import { AlumnosModule } from './alumnos/alumnos.module';
import { TrabajadoresModule } from './trabajadores/trabajadores.module';
import { TipoEventoModule } from './tipo_evento/tipo_evento.module';
+import { EventoUsuarioModule } from './evento_usuario/evento_usuario.module';
@Module({
imports: [
@@ -69,6 +70,7 @@ import { TipoEventoModule } from './tipo_evento/tipo_evento.module';
ValidacionesModule,
QrModule,
TrabajadoresModule,
+ EventoUsuarioModule,
],
controllers: [AppController],
diff --git a/src/cuestionario/cuestionario.service.ts b/src/cuestionario/cuestionario.service.ts
index b5f9ef7..1db78b7 100644
--- a/src/cuestionario/cuestionario.service.ts
+++ b/src/cuestionario/cuestionario.service.ts
@@ -257,6 +257,7 @@ export class CuestionarioService {
cuestionario.editable = true;
cuestionario.id_evento = createCuestionarioDto.id_evento;
cuestionario.cupo_maximo = createCuestionarioDto.cupo_maximo;
+ cuestionario.mensaje_correo = createCuestionarioDto.mensaje_correo;
const savedCuestionario = await queryRunner.manager.save(cuestionario);
@@ -419,6 +420,7 @@ export class CuestionarioService {
editable: true,
id_evento: eventoExistente.id_evento, // Asociar el evento creado
cupo_maximo: cuestionario.cupo_maximo,
+ mensaje_correo: cuestionario.mensaje_correo,
});
const savedCuestionario =
await queryRunner.manager.save(cuestionarioEntity);
diff --git a/src/cuestionario/dto/create-cuestionario.dto.ts b/src/cuestionario/dto/create-cuestionario.dto.ts
index 0cb2f34..53be4f6 100644
--- a/src/cuestionario/dto/create-cuestionario.dto.ts
+++ b/src/cuestionario/dto/create-cuestionario.dto.ts
@@ -89,6 +89,15 @@ export class CreateCuestionarioDto {
@IsString()
evento?: string;
+ @ApiProperty({
+ description: 'Mensaje personalizado que se incluye en el correo de confirmación de registro',
+ example: 'Favor de presentar este código QR en la entrada del evento.',
+ required: false,
+ })
+ @IsOptional()
+ @IsString()
+ mensaje_correo?: string;
+
@ApiProperty({
description: 'Secciones del cuestionario',
type: [CreateSeccionDto],
diff --git a/src/cuestionario/entities/cuestionario.entity.ts b/src/cuestionario/entities/cuestionario.entity.ts
index c4e569e..0980dca 100644
--- a/src/cuestionario/entities/cuestionario.entity.ts
+++ b/src/cuestionario/entities/cuestionario.entity.ts
@@ -51,6 +51,9 @@ export class Cuestionario {
@Column({ type: 'int' })
id_evento: number;
+ @Column({ type: 'text', nullable: true })
+ mensaje_correo?: string;
+
// Relaciones
@ManyToOne(() => Evento, (evento) => evento.cuestionarios, { nullable: true })
@JoinColumn({ name: 'id_evento' })
diff --git a/src/cuestionario_respondido/cuestionario_respondido.module.ts b/src/cuestionario_respondido/cuestionario_respondido.module.ts
index e006f18..6e37158 100644
--- a/src/cuestionario_respondido/cuestionario_respondido.module.ts
+++ b/src/cuestionario_respondido/cuestionario_respondido.module.ts
@@ -14,6 +14,7 @@ import { RespuestaParticipanteCerrada } from '../respuesta_participante_cerrada/
import { PreguntaOpcion } from '../pregunta_opcion/entities/pregunta_opcion.entity';
import { Opcion } from '../opcion/entities/opcion.entity';
import { ValidacionesModule } from '../validaciones/validaciones.module';
+import { EventoUsuarioModule } from '../evento_usuario/evento_usuario.module';
@Module({
imports: [
@@ -31,6 +32,7 @@ import { ValidacionesModule } from '../validaciones/validaciones.module';
ParticipanteModule,
ValidacionesModule,
QrModule,
+ EventoUsuarioModule,
],
controllers: [CuestionarioRespondidoController],
providers: [CuestionarioRespondidoService],
diff --git a/src/cuestionario_respondido/cuestionario_respondido.service.ts b/src/cuestionario_respondido/cuestionario_respondido.service.ts
index b2b74ad..caa4a81 100644
--- a/src/cuestionario_respondido/cuestionario_respondido.service.ts
+++ b/src/cuestionario_respondido/cuestionario_respondido.service.ts
@@ -4,7 +4,7 @@ import {
NotFoundException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
-import { Repository, DataSource } from 'typeorm';
+import { Repository, DataSource, In } from 'typeorm';
import { CreateCuestionarioRespondidoDto } from './dto/create-cuestionario_respondido.dto';
import { UpdateCuestionarioRespondidoDto } from './dto/update-cuestionario_respondido.dto';
import { SubmitRespuestasDto } from './dto/submit-respuestas.dto';
@@ -23,6 +23,9 @@ import * as QRCode from 'qrcode';
import { CuestionarioService } from 'src/cuestionario/cuestionario.service';
import { generarHtmlCorreoAsistencia } from 'src/emails/registro-evento';
import { TipoPreguntaEnum } from 'src/tipo_pregunta/entities/tipo_pregunta.entity';
+import { EventoUsuarioService } from '../evento_usuario/evento_usuario.service';
+import { TiposValidacion } from '../pregunta/entities/pregunta.entity';
+
@Injectable()
export class CuestionarioRespondidoService {
constructor(
@@ -41,6 +44,7 @@ export class CuestionarioRespondidoService {
private validadorRespuestasService: ValidadorRespuestasService,
private cuestionarioService: CuestionarioService,
private qrTokenService: QrTokenService,
+ private eventoUsuarioService: EventoUsuarioService,
) {}
create(createCuestionarioRespondidoDto: CreateCuestionarioRespondidoDto) {
@@ -79,6 +83,66 @@ export class CuestionarioRespondidoService {
}
}
+ // 1.2 Validar restricción de lista de usuarios del evento
+ if (cuestionario.evento?.id_evento) {
+ const id_evento = cuestionario.evento.id_evento;
+ const tieneLista =
+ await this.eventoUsuarioService.eventoTieneListaRestringida(id_evento);
+
+ if (tieneLista) {
+ const preguntaIds = submitDto.respuestas.map((r) => r.id_pregunta);
+ const identityPreguntas = await this.preguntaRepository.find({
+ where: [
+ {
+ id_pregunta: In(preguntaIds),
+ validacion: TiposValidacion.CUENTA_ALUMNO,
+ },
+ {
+ id_pregunta: In(preguntaIds),
+ validacion: TiposValidacion.RFC,
+ },
+ ],
+ });
+
+ let identificador: string | null = null;
+ for (const pregunta of identityPreguntas) {
+ const respuesta = submitDto.respuestas.find(
+ (r) => r.id_pregunta === pregunta.id_pregunta,
+ );
+ if (
+ respuesta &&
+ typeof respuesta.valor === 'string' &&
+ respuesta.valor.trim()
+ ) {
+ identificador = respuesta.valor.trim().toUpperCase();
+ break;
+ }
+ }
+
+ if (!identificador) {
+ await queryRunner.rollbackTransaction();
+ return {
+ success: false,
+ message:
+ 'Este evento requiere que estés en la lista de usuarios habilitados. No se encontró un número de cuenta o RFC válido en las respuestas enviadas.',
+ };
+ }
+
+ const enLista = await this.eventoUsuarioService.identificadorEnLista(
+ id_evento,
+ identificador,
+ );
+
+ if (!enLista) {
+ await queryRunner.rollbackTransaction();
+ return {
+ success: false,
+ message: `El usuario con identificador ${identificador} no está en la lista de usuarios habilitados para este evento.`,
+ };
+ }
+ }
+ }
+
// 2. Buscar o crear participante
let participante = await this.participanteRepository.findOne({
where: { correo: submitDto.correo },
@@ -146,6 +210,7 @@ export class CuestionarioRespondidoService {
fechaFinEvento: cuestionario.evento?.fecha_fin
? new Date(cuestionario.evento.fecha_fin).toLocaleString()
: undefined,
+ mensajePersonalizado: cuestionario.mensaje_correo,
}),
adjuntos: [
{
@@ -374,6 +439,7 @@ export class CuestionarioRespondidoService {
fechaFinEvento: cuestionario.evento?.fecha_fin
? new Date(cuestionario.evento.fecha_fin).toLocaleString()
: undefined,
+ mensajePersonalizado: cuestionario.mensaje_correo,
}),
adjuntos: [
{
diff --git a/src/db/typeorm.config.ts b/src/db/typeorm.config.ts
index 950c71e..7be22c6 100644
--- a/src/db/typeorm.config.ts
+++ b/src/db/typeorm.config.ts
@@ -21,6 +21,7 @@ import { TipoCuestionario } from 'src/tipo_cuestionario/entities/tipo_cuestionar
import { TipoPregunta } from 'src/tipo_pregunta/entities/tipo_pregunta.entity';
import { TipoUser } from 'src/tipo_user/entities/tipo_user.entity';
import { TipoEvento } from 'src/tipo_evento/entities/tipo_evento.entity';
+import { EventoUsuario } from 'src/evento_usuario/entities/evento_usuario.entity';
export const createMainDbConfig = async (
configService: ConfigService,
@@ -51,13 +52,13 @@ export const createMainDbConfig = async (
TipoEvento,
TipoPregunta,
TipoUser,
+ EventoUsuario,
],
retryAttempts: 5,
retryDelay: 3000,
connectTimeout: 30000,
synchronize: configService.get('SYNCHRONIZE') === 'true',
- dropSchema: configService.get('DROP_SCHEMA') === 'true',
});
export const createAlumnosDbConfig = async (
@@ -75,6 +76,5 @@ export const createAlumnosDbConfig = async (
retryDelay: 3000,
connectTimeout: 30000,
- synchronize: false,
- dropSchema: false,
+ synchronize: true,
});
diff --git a/src/emails/registro-evento.ts b/src/emails/registro-evento.ts
index cc776d8..d20a292 100644
--- a/src/emails/registro-evento.ts
+++ b/src/emails/registro-evento.ts
@@ -1,10 +1,17 @@
-const message = {
+const messagePorTipoEvento: Record = {
'Cultural / Artístico':
'Favor de presentar este código QR (en tu celular o impreso) en la mesa de atención de la Sala Emma Rizo 10 minutos antes de la hora de acceso reservada.',
'Feria / Expo':
'Favor de presentar el código QR en cualquier stand de la feria, para recibir tu constancia de asistencia.',
};
+const mensajeDefault =
+ 'Favor de presentar este código QR (en tu celular o impreso) en las mesas de juegos el día del evento, para participar por tu premio.';
+
+export function getMensajeDefaultPorTipoEvento(tipoEvento: string): string {
+ return messagePorTipoEvento[tipoEvento] ?? mensajeDefault;
+}
+
export function generarHtmlCorreoAsistencia({
nombreForm,
nombreEvento,
@@ -13,6 +20,7 @@ export function generarHtmlCorreoAsistencia({
fechaInicioEvento,
fechaFinEvento,
tipoEvento,
+ mensajePersonalizado,
}: {
nombreForm: string;
nombreEvento: string;
@@ -21,6 +29,7 @@ export function generarHtmlCorreoAsistencia({
fechaRegistro: string;
fechaInicioEvento?: string;
fechaFinEvento?: string;
+ mensajePersonalizado?: string;
}) {
const horarioEvento =
fechaInicioEvento && fechaFinEvento
@@ -43,7 +52,7 @@ export function generarHtmlCorreoAsistencia({
- ${message[tipoEvento] ? message[tipoEvento] : 'Favor de presentar este código QR (en tu celular o impreso) en las mesas de juegos el día del evento, para participar por tu premio.'}
+ ${mensajePersonalizado ?? getMensajeDefaultPorTipoEvento(tipoEvento)}
diff --git a/src/evento/docs/evento.documentation.ts b/src/evento/docs/evento.documentation.ts
index f1a9b1b..4621482 100644
--- a/src/evento/docs/evento.documentation.ts
+++ b/src/evento/docs/evento.documentation.ts
@@ -42,6 +42,33 @@ export class EventoApiDocumentation {
}),
);
+ static ApiCargarUsuarios = applyDecorators(
+ ApiOperation({
+ summary:
+ 'Cargar lista de usuarios (alumnos y/o trabajadores) para un evento desde un archivo Excel',
+ }),
+ ApiParam({
+ name: 'id',
+ type: Number,
+ description: 'ID del evento al que se carga la lista',
+ }),
+ ApiConsumes('multipart/form-data'),
+ ApiBody({
+ schema: {
+ type: 'object',
+ properties: {
+ file: {
+ type: 'string',
+ format: 'binary',
+ description:
+ 'Archivo Excel (.xlsx) con columna "cuenta" para alumnos y/o columna "rfc" para trabajadores',
+ },
+ },
+ required: ['file'],
+ },
+ }),
+ );
+
static ApiPostBanner = applyDecorators(
ApiOperation({ summary: 'Subir banner para un evento' }),
ApiParam({
diff --git a/src/evento/evento.controller.ts b/src/evento/evento.controller.ts
index c84c896..75619d9 100644
--- a/src/evento/evento.controller.ts
+++ b/src/evento/evento.controller.ts
@@ -19,11 +19,15 @@ import { FileInterceptor } from '@nestjs/platform-express';
import { diskStorage } from 'multer';
import { extname } from 'path';
import { EventoApiDocumentation } from './docs/evento.documentation';
+import { EventoUsuarioService } from '../evento_usuario/evento_usuario.service';
@Controller('evento')
@EventoApiDocumentation.ApiController
export class EventoController {
- constructor(private eventoService: EventoService) {}
+ constructor(
+ private eventoService: EventoService,
+ private eventoUsuarioService: EventoUsuarioService,
+ ) {}
@Post()
@EventoApiDocumentation.ApiCreate
@@ -64,6 +68,42 @@ export class EventoController {
return this.eventoService.getCuestionarioEvento(id_evento, id_cuestionario);
}
+ @Post(':id/usuarios')
+ @EventoApiDocumentation.ApiCargarUsuarios
+ @UseInterceptors(
+ FileInterceptor('file', {
+ storage: diskStorage({
+ destination: './uploads',
+ filename: (_, file, callback) => {
+ const uniqueName = `${Date.now()}${extname(file.originalname)}`;
+ callback(null, uniqueName);
+ },
+ }),
+ fileFilter: (_, file, callback) => {
+ const ext = extname(file.originalname).toLowerCase();
+ if (ext === '.xlsx' || ext === '.xls') {
+ callback(null, true);
+ } else {
+ callback(
+ new Error('Solo se permiten archivos Excel (.xlsx, .xls)'),
+ false,
+ );
+ }
+ },
+ }),
+ )
+ async cargarUsuarios(
+ @Param('id', ParseIntPipe) id: number,
+ @UploadedFile() file: Express.Multer.File,
+ ) {
+ if (!file) {
+ throw new UnprocessableEntityException(
+ 'No se ha subido ningún archivo o el archivo no es válido.',
+ );
+ }
+ return this.eventoUsuarioService.cargaUsuariosDesdeXlsx(id, file.path);
+ }
+
@Post(':id/banner')
@UseInterceptors(
FileInterceptor('banner', {
diff --git a/src/evento/evento.module.ts b/src/evento/evento.module.ts
index 9335a40..f88002c 100644
--- a/src/evento/evento.module.ts
+++ b/src/evento/evento.module.ts
@@ -3,11 +3,15 @@ import { EventoService } from './evento.service';
import { EventoController } from './evento.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Evento } from './entities/evento.entity';
+import { EventoUsuarioModule } from '../evento_usuario/evento_usuario.module';
@Module({
- imports: [TypeOrmModule.forFeature([Evento])],
+ imports: [
+ TypeOrmModule.forFeature([Evento]),
+ EventoUsuarioModule,
+ ],
controllers: [EventoController],
providers: [EventoService],
- exports: [EventoService]
+ exports: [EventoService],
})
export class EventoModule {}
diff --git a/src/evento_usuario/entities/evento_usuario.entity.ts b/src/evento_usuario/entities/evento_usuario.entity.ts
new file mode 100644
index 0000000..2e5cfda
--- /dev/null
+++ b/src/evento_usuario/entities/evento_usuario.entity.ts
@@ -0,0 +1,34 @@
+import { Evento } from 'src/evento/entities/evento.entity';
+import {
+ Column,
+ Entity,
+ Index,
+ JoinColumn,
+ ManyToOne,
+ PrimaryGeneratedColumn,
+} from 'typeorm';
+
+export enum TipoUsuarioEnum {
+ ALUMNO = 'alumno',
+ TRABAJADOR = 'trabajador',
+}
+
+@Entity('evento_usuario')
+@Index(['id_evento', 'tipo_usuario', 'identificador'], { unique: true })
+export class EventoUsuario {
+ @PrimaryGeneratedColumn()
+ id_evento_usuario: number;
+
+ @Column()
+ id_evento: number;
+
+ @ManyToOne(() => Evento, { onDelete: 'CASCADE' })
+ @JoinColumn({ name: 'id_evento' })
+ evento: Evento;
+
+ @Column({ type: 'enum', enum: TipoUsuarioEnum })
+ tipo_usuario: TipoUsuarioEnum;
+
+ @Column({ type: 'varchar', length: 20 })
+ identificador: string;
+}
diff --git a/src/evento_usuario/evento_usuario.module.ts b/src/evento_usuario/evento_usuario.module.ts
new file mode 100644
index 0000000..e81a846
--- /dev/null
+++ b/src/evento_usuario/evento_usuario.module.ts
@@ -0,0 +1,17 @@
+import { Module } from '@nestjs/common';
+import { TypeOrmModule } from '@nestjs/typeorm';
+import { EventoUsuario } from './entities/evento_usuario.entity';
+import { EventoUsuarioService } from './evento_usuario.service';
+import { RegistroAlumno } from '../alumnos/entities/registro-alumno.entity';
+import { RegistroTrabajador } from '../trabajadores/entities/trabajadore.entity';
+import { Evento } from '../evento/entities/evento.entity';
+
+@Module({
+ imports: [
+ TypeOrmModule.forFeature([EventoUsuario, Evento]),
+ TypeOrmModule.forFeature([RegistroAlumno, RegistroTrabajador], 'alumnosConnection'),
+ ],
+ providers: [EventoUsuarioService],
+ exports: [EventoUsuarioService],
+})
+export class EventoUsuarioModule {}
diff --git a/src/evento_usuario/evento_usuario.service.ts b/src/evento_usuario/evento_usuario.service.ts
new file mode 100644
index 0000000..e5d7360
--- /dev/null
+++ b/src/evento_usuario/evento_usuario.service.ts
@@ -0,0 +1,189 @@
+import { Injectable, NotFoundException } from '@nestjs/common';
+import { InjectRepository } from '@nestjs/typeorm';
+import { Repository } from 'typeorm';
+import {
+ EventoUsuario,
+ TipoUsuarioEnum,
+} from './entities/evento_usuario.entity';
+import { RegistroAlumno } from '../alumnos/entities/registro-alumno.entity';
+import { RegistroTrabajador } from '../trabajadores/entities/trabajadore.entity';
+import { Evento } from '../evento/entities/evento.entity';
+import * as fs from 'fs';
+import * as XLSX from 'xlsx';
+
+@Injectable()
+export class EventoUsuarioService {
+ constructor(
+ @InjectRepository(EventoUsuario)
+ private readonly eventoUsuarioRepo: Repository
,
+ @InjectRepository(RegistroAlumno, 'alumnosConnection')
+ private readonly alumnoRepo: Repository,
+ @InjectRepository(RegistroTrabajador, 'alumnosConnection')
+ private readonly trabajadorRepo: Repository,
+ @InjectRepository(Evento)
+ private readonly eventoRepo: Repository,
+ ) {}
+
+ async eventoTieneListaRestringida(id_evento: number): Promise {
+ const count = await this.eventoUsuarioRepo.count({ where: { id_evento } });
+ return count > 0;
+ }
+
+ async identificadorEnLista(
+ id_evento: number,
+ identificador: string,
+ ): Promise {
+ const row = await this.eventoUsuarioRepo.findOne({
+ where: {
+ id_evento,
+ identificador: identificador.trim().toUpperCase(),
+ },
+ });
+ return !!row;
+ }
+
+ async bulkInsert(
+ id_evento: number,
+ alumnos: string[],
+ trabajadores: string[],
+ ): Promise<{ insertados: number; omitidos: number; errores: string[] }> {
+ console.log(id_evento, alumnos, trabajadores);
+ let insertados = 0;
+ let omitidos = 0;
+ const errores: string[] = [];
+
+ for (const cuenta of alumnos) {
+ try {
+ const result = await this.eventoUsuarioRepo
+ .createQueryBuilder()
+ .insert()
+ .into(EventoUsuario)
+ .values({
+ id_evento,
+ tipo_usuario: TipoUsuarioEnum.ALUMNO,
+ identificador: cuenta.toUpperCase(),
+ })
+ .orIgnore()
+ .execute();
+ if (result.raw?.affectedRows > 0) {
+ insertados++;
+ } else {
+ omitidos++;
+ }
+ } catch {
+ omitidos++;
+ errores.push(`Error al insertar cuenta ${cuenta}`);
+ }
+ }
+
+ for (const rfc of trabajadores) {
+ try {
+ const result = await this.eventoUsuarioRepo
+ .createQueryBuilder()
+ .insert()
+ .into(EventoUsuario)
+ .values({
+ id_evento,
+ tipo_usuario: TipoUsuarioEnum.TRABAJADOR,
+ identificador: rfc.toUpperCase(),
+ })
+ .orIgnore()
+ .execute();
+ if (result.raw?.affectedRows > 0) {
+ insertados++;
+ } else {
+ omitidos++;
+ }
+ } catch {
+ omitidos++;
+ errores.push(`Error al insertar RFC ${rfc}`);
+ }
+ }
+
+ return { insertados, omitidos, errores };
+ }
+
+ async cargaUsuariosDesdeXlsx(
+ id_evento: number,
+ filePath: string,
+ ): Promise<{ insertados: number; omitidos: number; errores: string[] }> {
+ const evento = await this.eventoRepo.findOne({ where: { id_evento } });
+ if (!evento) {
+ fs.unlinkSync(filePath);
+ throw new NotFoundException(`Evento con ID ${id_evento} no encontrado.`);
+ }
+
+ const workbook = XLSX.readFile(filePath);
+ const sheetName = workbook.SheetNames[0];
+ const data: any[] = XLSX.utils.sheet_to_json(workbook.Sheets[sheetName]);
+
+ const alumnosValidos: string[] = [];
+ const trabajadoresValidos: string[] = [];
+ let omitidos = 0;
+ const errores: string[] = [];
+
+ for (const row of data) {
+ const cuentaRaw = row['cuenta'];
+ const rfcRaw = row['rfc'];
+
+ if (cuentaRaw !== undefined && cuentaRaw !== null && cuentaRaw !== '') {
+ const cuenta = String(cuentaRaw).trim();
+ const alumno = await this.alumnoRepo.findOne({
+ where: { id_ncuenta: parseInt(cuenta, 10) },
+ });
+ console.log('Alumno encontrado:', alumno);
+ if (!alumno) {
+ omitidos++;
+ errores.push(`Cuenta no encontrada en el sistema: ${cuenta}`);
+ continue;
+ }
+ alumnosValidos.push(alumno.id_ncuenta.toString());
+ continue;
+ }
+
+ if (rfcRaw !== undefined && rfcRaw !== null && rfcRaw !== '') {
+ const rfc = String(rfcRaw).trim().toUpperCase();
+ const trabajador = await this.trabajadorRepo.findOne({
+ where: { rfc },
+ });
+ if (!trabajador) {
+ omitidos++;
+ errores.push(`RFC no encontrado en el sistema: ${rfc}`);
+ continue;
+ }
+ trabajadoresValidos.push(trabajador.rfc);
+ continue;
+ }
+
+ omitidos++;
+ }
+
+ fs.unlinkSync(filePath);
+
+ const resultado = await this.bulkInsert(
+ id_evento,
+ alumnosValidos,
+ trabajadoresValidos,
+ );
+
+ return {
+ insertados: resultado.insertados,
+ omitidos: omitidos + resultado.omitidos,
+ errores: [...errores, ...resultado.errores],
+ };
+ }
+
+ async getUsuariosEvento(
+ id_evento: number,
+ ): Promise<{ alumnos: string[]; trabajadores: string[] }> {
+ const rows = await this.eventoUsuarioRepo.find({ where: { id_evento } });
+ return {
+ alumnos: rows
+ .filter((r) => r.tipo_usuario === TipoUsuarioEnum.ALUMNO)
+ .map((r) => r.identificador),
+ trabajadores: rows
+ .filter((r) => r.tipo_usuario === TipoUsuarioEnum.TRABAJADOR)
+ .map((r) => r.identificador),
+ };
+ }
+}
diff --git a/src/trabajadores/trabajadores.controller.ts b/src/trabajadores/trabajadores.controller.ts
index a10fe84..fd60f76 100644
--- a/src/trabajadores/trabajadores.controller.ts
+++ b/src/trabajadores/trabajadores.controller.ts
@@ -9,6 +9,7 @@ import {
UseInterceptors,
UploadedFile,
NotFoundException,
+ Query,
} from '@nestjs/common';
import { TrabajadoresService } from './trabajadores.service';
import { CreateTrabajadoreDto } from './dto/create-trabajadore.dto';
@@ -46,8 +47,13 @@ export class TrabajadoresController {
}
@Get(':rfc')
- findByRfc(@Param('rfc') rfc: string) {
- const trabajador = this.trabajadoresService.findByRfc(rfc);
+ async findByRfc(
+ @Param('rfc') rfc: string,
+ @Query('id_evento') idEventoRaw?: string,
+ ) {
+ const id_evento =
+ idEventoRaw !== undefined ? parseInt(idEventoRaw, 10) : undefined;
+ const trabajador = await this.trabajadoresService.findByRfc(rfc, id_evento);
if (!trabajador) {
throw new NotFoundException(`No se encontró trabajador con RFC ${rfc}`);
}
diff --git a/src/trabajadores/trabajadores.module.ts b/src/trabajadores/trabajadores.module.ts
index d9f8855..c82072c 100644
--- a/src/trabajadores/trabajadores.module.ts
+++ b/src/trabajadores/trabajadores.module.ts
@@ -3,10 +3,12 @@ import { TrabajadoresService } from './trabajadores.service';
import { TrabajadoresController } from './trabajadores.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { RegistroTrabajador } from './entities/trabajadore.entity';
+import { EventoUsuarioModule } from '../evento_usuario/evento_usuario.module';
@Module({
imports: [
TypeOrmModule.forFeature([RegistroTrabajador], 'alumnosConnection'),
+ EventoUsuarioModule,
],
controllers: [TrabajadoresController],
providers: [TrabajadoresService],
diff --git a/src/trabajadores/trabajadores.service.ts b/src/trabajadores/trabajadores.service.ts
index 4aa8355..4da3211 100644
--- a/src/trabajadores/trabajadores.service.ts
+++ b/src/trabajadores/trabajadores.service.ts
@@ -1,4 +1,4 @@
-import { Injectable } from '@nestjs/common';
+import { ForbiddenException, Injectable } from '@nestjs/common';
import { CreateTrabajadoreDto } from './dto/create-trabajadore.dto';
import { UpdateTrabajadoreDto } from './dto/update-trabajadore.dto';
import { InjectRepository } from '@nestjs/typeorm';
@@ -7,12 +7,14 @@ import { Repository } from 'typeorm';
import * as fs from 'fs';
import * as XLSX from 'xlsx';
import { UsuarioData } from 'src/alumnos/alumnos.service';
+import { EventoUsuarioService } from '../evento_usuario/evento_usuario.service';
@Injectable()
export class TrabajadoresService {
constructor(
@InjectRepository(RegistroTrabajador, 'alumnosConnection')
private readonly trabajadorRepo: Repository,
+ private readonly eventoUsuarioService: EventoUsuarioService,
) {}
async procesarArchivo(
@@ -69,21 +71,41 @@ export class TrabajadoresService {
return { insertados, omitidos };
}
- async findByRfc(rfc: string): Promise {
+ async findByRfc(
+ rfc: string,
+ id_evento?: number,
+ ): 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
+ if (!trabajador) return null;
+
+ if (id_evento !== undefined) {
+ const tieneLista =
+ await this.eventoUsuarioService.eventoTieneListaRestringida(id_evento);
+
+ if (tieneLista) {
+ const enLista = await this.eventoUsuarioService.identificadorEnLista(
+ id_evento,
+ trabajador.rfc,
+ );
+ if (!enLista) {
+ throw new ForbiddenException(
+ `El trabajador con RFC ${rfc} no está en la lista de usuarios habilitados para este evento.`,
+ );
+ }
+ }
}
+
+ return {
+ cuenta: trabajador.num_trabajador?.toString() || null,
+ nombre: trabajador.nombre || null,
+ apellidos: `${trabajador.apellidos}`.trim() || null,
+ carrera: trabajador.carrera || null,
+ genero: trabajador.genero || null,
+ rfc: trabajador.rfc || null,
+ };
}
create(createTrabajadoreDto: CreateTrabajadoreDto) {
From f4bd054aa15b3aecd542c3bdfdec9adfbb63ebb3 Mon Sep 17 00:00:00 2001
From: miguel
Date: Mon, 9 Mar 2026 19:13:26 -0600
Subject: [PATCH 2/2] Refactor respuesta validation in submitRespuestas method
for improved clarity
---
.../cuestionario_respondido.service.ts | 16 +++++++++++-----
1 file changed, 11 insertions(+), 5 deletions(-)
diff --git a/src/cuestionario_respondido/cuestionario_respondido.service.ts b/src/cuestionario_respondido/cuestionario_respondido.service.ts
index caa4a81..e7c5c19 100644
--- a/src/cuestionario_respondido/cuestionario_respondido.service.ts
+++ b/src/cuestionario_respondido/cuestionario_respondido.service.ts
@@ -87,7 +87,9 @@ export class CuestionarioRespondidoService {
if (cuestionario.evento?.id_evento) {
const id_evento = cuestionario.evento.id_evento;
const tieneLista =
- await this.eventoUsuarioService.eventoTieneListaRestringida(id_evento);
+ await this.eventoUsuarioService.eventoTieneListaRestringida(
+ id_evento,
+ );
if (tieneLista) {
const preguntaIds = submitDto.respuestas.map((r) => r.id_pregunta);
@@ -109,13 +111,17 @@ export class CuestionarioRespondidoService {
const respuesta = submitDto.respuestas.find(
(r) => r.id_pregunta === pregunta.id_pregunta,
);
+
if (
respuesta &&
- typeof respuesta.valor === 'string' &&
- respuesta.valor.trim()
+ respuesta.valor !== null &&
+ respuesta.valor !== undefined
) {
- identificador = respuesta.valor.trim().toUpperCase();
- break;
+ const valorStr = String(respuesta.valor).trim();
+ if (valorStr) {
+ identificador = valorStr.toUpperCase();
+ break;
+ }
}
}