develop #39
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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: [
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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;">
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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', {
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user