Merge pull request 'develop' (#39) from develop into master

Reviewed-on: #39
This commit was merged in pull request #39.
This commit is contained in:
2026-03-10 15:43:56 -06:00
20 changed files with 634 additions and 38 deletions
+74 -5
View File
@@ -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<UsuarioData | null> {
const alumno = await this.alumnosService.findByCuenta(cuenta);
async getAlumnoByCuenta(
@Param('cuenta') cuenta: string,
@Query('id_evento') idEventoRaw?: string,
): Promise<UsuarioData | null> {
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;
}
+5 -1
View File
@@ -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],
+93 -10
View File
@@ -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<RegistroAlumno>,
private readonly eventoUsuarioService: EventoUsuarioService,
) {}
async findByCuenta(cuenta: string): Promise<UsuarioData | null> {
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<UsuarioData | null> {
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,
};
}
}
+2
View File
@@ -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],
+2
View File
@@ -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);
@@ -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],
@@ -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' })
@@ -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],
@@ -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,72 @@ 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 &&
respuesta.valor !== null &&
respuesta.valor !== undefined
) {
const valorStr = String(respuesta.valor).trim();
if (valorStr) {
identificador = valorStr.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 +216,7 @@ export class CuestionarioRespondidoService {
fechaFinEvento: cuestionario.evento?.fecha_fin
? new Date(cuestionario.evento.fecha_fin).toLocaleString()
: undefined,
mensajePersonalizado: cuestionario.mensaje_correo,
}),
adjuntos: [
{
@@ -374,6 +445,7 @@ export class CuestionarioRespondidoService {
fechaFinEvento: cuestionario.evento?.fecha_fin
? new Date(cuestionario.evento.fecha_fin).toLocaleString()
: undefined,
mensajePersonalizado: cuestionario.mensaje_correo,
}),
adjuntos: [
{
+3 -3
View File
@@ -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<string>('SYNCHRONIZE') === 'true',
dropSchema: configService.get<string>('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,
});
+11 -2
View File
@@ -1,10 +1,17 @@
const message = {
const messagePorTipoEvento: Record<string, string> = {
'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({
</p>
<p style="font-size: 16px; color: #333;">
${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)}
</p>
<div style="text-align: center; margin: 20px 0;">
+27
View File
@@ -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({
+41 -1
View File
@@ -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', {
+6 -2
View File
@@ -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 {}
@@ -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;
}
@@ -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 {}
@@ -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<EventoUsuario>,
@InjectRepository(RegistroAlumno, 'alumnosConnection')
private readonly alumnoRepo: Repository<RegistroAlumno>,
@InjectRepository(RegistroTrabajador, 'alumnosConnection')
private readonly trabajadorRepo: Repository<RegistroTrabajador>,
@InjectRepository(Evento)
private readonly eventoRepo: Repository<Evento>,
) {}
async eventoTieneListaRestringida(id_evento: number): Promise<boolean> {
const count = await this.eventoUsuarioRepo.count({ where: { id_evento } });
return count > 0;
}
async identificadorEnLista(
id_evento: number,
identificador: string,
): Promise<boolean> {
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),
};
}
}
+8 -2
View File
@@ -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}`);
}
+2
View File
@@ -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],
+33 -11
View File
@@ -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<RegistroTrabajador>,
private readonly eventoUsuarioService: EventoUsuarioService,
) {}
async procesarArchivo(
@@ -69,21 +71,41 @@ export class TrabajadoresService {
return { insertados, omitidos };
}
async findByRfc(rfc: string): Promise<UsuarioData | null> {
async findByRfc(
rfc: string,
id_evento?: number,
): Promise<UsuarioData | null> {
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) {