Compare commits
58 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 897eaf619b | |||
| d6da4d093b | |||
| 9ccd08acc6 | |||
| 2497488491 | |||
| f4bd054aa1 | |||
| aa2a0f1073 | |||
| 4550af6e35 | |||
| 2be866e7f9 | |||
| 6e82e7fdfd | |||
| e1ddf84166 | |||
| b244c66b1f | |||
| 29ee4e1999 | |||
| c259649a8d | |||
| 4615306784 | |||
| 258a03e7b4 | |||
| ba1f39502e | |||
| ec76bb4835 | |||
| 53fe436e34 | |||
| e81abcb665 | |||
| 12080aa83e | |||
| 41d36f540a | |||
| e4ce66684f | |||
| c0b3087d76 | |||
| 3db1076ee6 | |||
| 8196ea8a31 | |||
| e7b3540170 | |||
| 0837bdd17b | |||
| e4c2a16c8e | |||
| 00e758f0ef | |||
| d070a4bc8f | |||
| 2ea0119b87 | |||
| 3cccee58f6 | |||
| 55a96d16f6 | |||
| 2a7a10ad9b | |||
| 663be0c7c6 | |||
| 613a44c355 | |||
| 0e8961f792 | |||
| 6eb53a327a | |||
| be72ee5fa5 | |||
| 7b8c19fbd1 | |||
| 0745ddc8b4 | |||
| f346da673b | |||
| 8690c82429 | |||
| 1e09c14767 | |||
| 794d2bcd90 | |||
| 01b5c090ec | |||
| 919d94cb0e | |||
| bbdeab629d | |||
| a876c92dcf | |||
| 960163fa4a | |||
| 25872c544f | |||
| 6926e84df3 | |||
| f1fba6a6cf | |||
| 92851b240a | |||
| 6fc315450d | |||
| 574249e09a | |||
| d0ae2f14cb | |||
| ff3f799549 |
Generated
+1101
-175
File diff suppressed because it is too large
Load Diff
@@ -38,6 +38,7 @@
|
|||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.1",
|
"class-validator": "^0.14.1",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
|
"exceljs": "^4.4.0",
|
||||||
"mysql2": "^3.14.0",
|
"mysql2": "^3.14.0",
|
||||||
"passport": "^0.7.0",
|
"passport": "^0.7.0",
|
||||||
"passport-jwt": "^4.0.1",
|
"passport-jwt": "^4.0.1",
|
||||||
|
|||||||
@@ -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 { 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 { AlumnoDto, ALUMNO_RESPONSES } from './alumnos.documentation';
|
||||||
|
import { FileInterceptor } from '@nestjs/platform-express';
|
||||||
|
import { diskStorage } from 'multer';
|
||||||
|
import { extname } from 'path';
|
||||||
|
|
||||||
@ApiTags('alumnos')
|
@ApiTags('alumnos')
|
||||||
@Controller('alumnos')
|
@Controller('alumnos')
|
||||||
export class AlumnosController {
|
export class AlumnosController {
|
||||||
constructor(private readonly alumnosService: AlumnosService) {}
|
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')
|
@Get(':cuenta')
|
||||||
@ApiOperation({ summary: 'Obtener datos de un alumno por su número de cuenta' })
|
@ApiOperation({ summary: 'Obtener datos de un alumno por su número de cuenta' })
|
||||||
@ApiParam({ name: 'cuenta', description: 'Número de cuenta del alumno' })
|
@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[200])
|
||||||
@ApiResponse(ALUMNO_RESPONSES[404])
|
@ApiResponse(ALUMNO_RESPONSES[404])
|
||||||
@ApiResponse(ALUMNO_RESPONSES[500])
|
@ApiResponse(ALUMNO_RESPONSES[500])
|
||||||
async getAlumnoByCuenta(@Param('cuenta') cuenta: string): Promise<UsuarioData | null> {
|
async getAlumnoByCuenta(
|
||||||
const alumno = await this.alumnosService.findByCuenta(cuenta);
|
@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) {
|
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;
|
return alumno;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,13 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
|||||||
import { AlumnosController } from './alumnos.controller';
|
import { AlumnosController } from './alumnos.controller';
|
||||||
import { AlumnosService } from './alumnos.service';
|
import { AlumnosService } from './alumnos.service';
|
||||||
import { RegistroAlumno } from './entities/registro-alumno.entity';
|
import { RegistroAlumno } from './entities/registro-alumno.entity';
|
||||||
|
import { EventoUsuarioModule } from '../evento_usuario/evento_usuario.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([RegistroAlumno], 'alumnosConnection')],
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([RegistroAlumno], 'alumnosConnection'),
|
||||||
|
EventoUsuarioModule,
|
||||||
|
],
|
||||||
controllers: [AlumnosController],
|
controllers: [AlumnosController],
|
||||||
providers: [AlumnosService],
|
providers: [AlumnosService],
|
||||||
exports: [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 { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { RegistroAlumno } from './entities/registro-alumno.entity';
|
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 = {
|
export type UsuarioData = {
|
||||||
cuenta: string | null;
|
cuenta: string | null;
|
||||||
@@ -10,26 +17,102 @@ export type UsuarioData = {
|
|||||||
carrera: string | null;
|
carrera: string | null;
|
||||||
genero: string | null;
|
genero: string | null;
|
||||||
rfc?: string | null; // Solo para trabajadores
|
rfc?: string | null; // Solo para trabajadores
|
||||||
}
|
};
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AlumnosService {
|
export class AlumnosService {
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(RegistroAlumno, 'alumnosConnection')
|
@InjectRepository(RegistroAlumno, 'alumnosConnection')
|
||||||
private registroAlumnoRepository: Repository<RegistroAlumno>,
|
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({
|
const alumno = await this.registroAlumnoRepository.findOne({
|
||||||
where: { id_ncuenta: parseInt(cuenta, 10) },
|
where: { id_ncuenta: parseInt(cuenta, 10) },
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
if (!alumno) {
|
||||||
cuenta: alumno?.id_ncuenta.toString() || null,
|
throw new NotFoundException(
|
||||||
nombre: alumno?.nombre || null,
|
`No se encontró alumno con número de cuenta ${cuenta}`,
|
||||||
apellidos: alumno?.apellidos || null,
|
);
|
||||||
carrera: alumno?.carrera || null,
|
|
||||||
genero: alumno?.genero || null,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 { AlumnosModule } from './alumnos/alumnos.module';
|
||||||
import { TrabajadoresModule } from './trabajadores/trabajadores.module';
|
import { TrabajadoresModule } from './trabajadores/trabajadores.module';
|
||||||
import { TipoEventoModule } from './tipo_evento/tipo_evento.module';
|
import { TipoEventoModule } from './tipo_evento/tipo_evento.module';
|
||||||
|
import { EventoUsuarioModule } from './evento_usuario/evento_usuario.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -69,6 +70,7 @@ import { TipoEventoModule } from './tipo_evento/tipo_evento.module';
|
|||||||
ValidacionesModule,
|
ValidacionesModule,
|
||||||
QrModule,
|
QrModule,
|
||||||
TrabajadoresModule,
|
TrabajadoresModule,
|
||||||
|
EventoUsuarioModule,
|
||||||
],
|
],
|
||||||
|
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
|
|||||||
@@ -9,15 +9,23 @@ import {
|
|||||||
UseInterceptors,
|
UseInterceptors,
|
||||||
ParseIntPipe,
|
ParseIntPipe,
|
||||||
UploadedFile,
|
UploadedFile,
|
||||||
|
UnprocessableEntityException,
|
||||||
|
UploadedFiles,
|
||||||
|
BadRequestException,
|
||||||
|
HttpStatus,
|
||||||
|
HttpException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { CuestionarioService } from './cuestionario.service';
|
import { CuestionarioService } from './cuestionario.service';
|
||||||
import { CreateCuestionarioDto } from './dto/create-cuestionario.dto';
|
import { CreateCuestionarioDto } from './dto/create-cuestionario.dto';
|
||||||
import { UpdateCuestionarioDto } from './dto/update-cuestionario.dto';
|
import { UpdateCuestionarioDto } from './dto/update-cuestionario.dto';
|
||||||
import { FileInterceptor } from '@nestjs/platform-express';
|
import { FileInterceptor, FilesInterceptor } from '@nestjs/platform-express';
|
||||||
import { diskStorage } from 'multer';
|
import { diskStorage } from 'multer';
|
||||||
import { extname } from 'path';
|
import { extname } from 'path';
|
||||||
import { CuestionarioApiDocumentation } from './docs/cuestionario.documentation';
|
import { CuestionarioApiDocumentation } from './docs/cuestionario.documentation';
|
||||||
import { CreateEventoWithCuestionarioDto } from './dto/create-evento-cuestionario.dto';
|
import { CreateEventoWithCuestionarioDto } from './dto/create-evento-cuestionario.dto';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import { NotFoundError } from 'rxjs';
|
||||||
|
import { error } from 'console';
|
||||||
|
|
||||||
@Controller('cuestionario')
|
@Controller('cuestionario')
|
||||||
@CuestionarioApiDocumentation.ApiController
|
@CuestionarioApiDocumentation.ApiController
|
||||||
@@ -30,6 +38,92 @@ export class CuestionarioController {
|
|||||||
return this.cuestionarioService.create(createCuestionarioDto);
|
return this.cuestionarioService.create(createCuestionarioDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post('banners/bulk')
|
||||||
|
@UseInterceptors(
|
||||||
|
FilesInterceptor('banners', 20, { // puedes subir hasta 20 imágenes
|
||||||
|
storage: diskStorage({
|
||||||
|
destination: './uploads/banners',
|
||||||
|
filename: (req, file, cb) => {
|
||||||
|
cb(null, file.originalname);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
fileFilter: (req, file, cb) => {
|
||||||
|
const allowedTypes = /jpeg|jpg|png|webp/;
|
||||||
|
const ext = extname(file.originalname).toLowerCase();
|
||||||
|
if (allowedTypes.test(ext)) {
|
||||||
|
cb(null, true);
|
||||||
|
} else {
|
||||||
|
cb(new Error('Tipo de archivo no permitido'), false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)async subirBanners(@UploadedFiles() files: Express.Multer.File[]) {
|
||||||
|
if (!files || files.length === 0) {
|
||||||
|
throw new UnprocessableEntityException('No se subió ningún archivo');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retornamos lista de filenames para asociarlos después
|
||||||
|
return files.map((file) => ({
|
||||||
|
originalName: file.originalname,
|
||||||
|
filename: file.filename,
|
||||||
|
path: file.path,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('carga-masiva')
|
||||||
|
@UseInterceptors(
|
||||||
|
FileInterceptor('file', {
|
||||||
|
storage: diskStorage({
|
||||||
|
destination: (req, file, cb) => {
|
||||||
|
const uploadPath = './uploads/excel';
|
||||||
|
// Crear la carpeta si no existe
|
||||||
|
fs.mkdirSync(uploadPath, { recursive: true });
|
||||||
|
cb(null, uploadPath);
|
||||||
|
},
|
||||||
|
filename: (req, file, cb) => {
|
||||||
|
// Guardar el archivo con timestamp + nombre original
|
||||||
|
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
|
||||||
|
const fileExt = extname(file.originalname); // obtener extensión
|
||||||
|
cb(null, `${uniqueSuffix}${fileExt}`);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
fileFilter: (req, file, cb) => {
|
||||||
|
// Solo permitir archivos .xlsx
|
||||||
|
if (file.mimetype === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') {
|
||||||
|
cb(null, true);
|
||||||
|
} else {
|
||||||
|
cb(new BadRequestException('Solo se permiten archivos .xlsx'), false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
async cargaMasiva(@UploadedFile() file: Express.Multer.File) {
|
||||||
|
if (!file) {
|
||||||
|
throw new BadRequestException('No se ha subido ningún archivo');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Archivo guardado en:', file.path);
|
||||||
|
|
||||||
|
// Leer el archivo con ExcelJS
|
||||||
|
|
||||||
|
|
||||||
|
// Aquí tu lógica para procesar las hojas del Excel
|
||||||
|
const cuestionarios = await this.cuestionarioService.cargarEventosDesdeExcel(file.path); // Ejemplo de arreglo de datos procesados
|
||||||
|
// ...
|
||||||
|
|
||||||
|
if (!cuestionarios.data) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: cuestionarios.message,
|
||||||
|
total: cuestionarios.data.length,
|
||||||
|
data: cuestionarios,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@Post('withEvento')
|
@Post('withEvento')
|
||||||
@CuestionarioApiDocumentation.ApiCreateWithEvento
|
@CuestionarioApiDocumentation.ApiCreateWithEvento
|
||||||
createCuestionarioEvento(
|
createCuestionarioEvento(
|
||||||
@@ -40,6 +134,9 @@ export class CuestionarioController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@Post(':id/banner')
|
@Post(':id/banner')
|
||||||
@UseInterceptors(
|
@UseInterceptors(
|
||||||
FileInterceptor('banner', {
|
FileInterceptor('banner', {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
Injectable,
|
Injectable,
|
||||||
InternalServerErrorException,
|
InternalServerErrorException,
|
||||||
NotFoundException,
|
NotFoundException,
|
||||||
|
UnprocessableEntityException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository, DataSource, In, MoreThan } from 'typeorm';
|
import { Repository, DataSource, In, MoreThan } from 'typeorm';
|
||||||
@@ -13,7 +14,7 @@ import { UpdateCuestionarioDto } from './dto/update-cuestionario.dto';
|
|||||||
import { Cuestionario } from './entities/cuestionario.entity';
|
import { Cuestionario } from './entities/cuestionario.entity';
|
||||||
import { Seccion } from '../seccion/entities/seccion.entity';
|
import { Seccion } from '../seccion/entities/seccion.entity';
|
||||||
import { CuestionarioSeccion } from '../cuestionario_seccion/entities/cuestionario_seccion.entity';
|
import { CuestionarioSeccion } from '../cuestionario_seccion/entities/cuestionario_seccion.entity';
|
||||||
import { Pregunta } from '../pregunta/entities/pregunta.entity';
|
import { Pregunta, TiposValidacion } from '../pregunta/entities/pregunta.entity';
|
||||||
import { SeccionPregunta } from '../seccion_pregunta/entities/seccion_pregunta.entity';
|
import { SeccionPregunta } from '../seccion_pregunta/entities/seccion_pregunta.entity';
|
||||||
import { Opcion } from '../opcion/entities/opcion.entity';
|
import { Opcion } from '../opcion/entities/opcion.entity';
|
||||||
import { PreguntaOpcion } from '../pregunta_opcion/entities/pregunta_opcion.entity';
|
import { PreguntaOpcion } from '../pregunta_opcion/entities/pregunta_opcion.entity';
|
||||||
@@ -24,6 +25,8 @@ import {
|
|||||||
import { Evento } from '../evento/entities/evento.entity';
|
import { Evento } from '../evento/entities/evento.entity';
|
||||||
import { EventoService } from '../evento/evento.service';
|
import { EventoService } from '../evento/evento.service';
|
||||||
import { CreateEventoWithCuestionarioDto } from './dto/create-evento-cuestionario.dto';
|
import { CreateEventoWithCuestionarioDto } from './dto/create-evento-cuestionario.dto';
|
||||||
|
import * as ExcelJS from 'exceljs';
|
||||||
|
import { PLANTILLAS } from './plantillas';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CuestionarioService {
|
export class CuestionarioService {
|
||||||
@@ -50,6 +53,187 @@ export class CuestionarioService {
|
|||||||
private eventoService: EventoService,
|
private eventoService: EventoService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private mapTipoPregunta(tipo: string): TipoPreguntaEnum {
|
||||||
|
if (!tipo) {
|
||||||
|
console.log(tipo);
|
||||||
|
throw new Error('El campo "tipo de pregunta" está vacío en el Excel');
|
||||||
|
}
|
||||||
|
switch (tipo.toLowerCase()) {
|
||||||
|
case 'cerrada': return TipoPreguntaEnum.Cerrada;
|
||||||
|
case 'multiple': return TipoPreguntaEnum.Multiple;
|
||||||
|
case 'abierta (parrafo)': return TipoPreguntaEnum.AbiertaParrafo;
|
||||||
|
case 'abierta (respuesta corta)': return TipoPreguntaEnum.AbiertaRespuestaCorta;
|
||||||
|
default: throw new Error(`Tipo de pregunta inválido: ${tipo}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mapear validación desde string a enum
|
||||||
|
private mapValidacion(validacion?: string): TiposValidacion | undefined {
|
||||||
|
if (!validacion) return undefined;
|
||||||
|
|
||||||
|
switch (validacion.trim().toLowerCase()) {
|
||||||
|
case 'correo': return TiposValidacion.CORREO;
|
||||||
|
case 'correo_institucional': return TiposValidacion.CORREO_INSTITUCIONAL;
|
||||||
|
case 'telefono': return TiposValidacion.TELEFONO;
|
||||||
|
case 'nombre': return TiposValidacion.NOMBRE;
|
||||||
|
case 'apellidos': return TiposValidacion.APELLIDOS;
|
||||||
|
case 'entero': return TiposValidacion.ENTERO;
|
||||||
|
case 'decimal': return TiposValidacion.DECIMAL;
|
||||||
|
case 'comunidad_alumno': return TiposValidacion.COMUNIDAD_ALUMNO;
|
||||||
|
case 'cuenta_alumno': return TiposValidacion.CUENTA_ALUMNO;
|
||||||
|
case 'comunidad_trabajador': return TiposValidacion.COMUNIDAD_TRABAJADOR;
|
||||||
|
case 'cuenta_trabajador': return TiposValidacion.CUENTA_TRABAJADOR;
|
||||||
|
case 'genero': return TiposValidacion.GENERO;
|
||||||
|
case 'carrera': return TiposValidacion.CARRERA;
|
||||||
|
case 'institucion': return TiposValidacion.INSTITUCION;
|
||||||
|
case 'rfc': return TiposValidacion.RFC;
|
||||||
|
default: return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async cargarEventosDesdeExcel(filePath: string) {
|
||||||
|
const PLANTILLA_MAP: Record<number, string> = {
|
||||||
|
1: 'registro-comunidad-alumnos',
|
||||||
|
2: 'registro-comunidad-trabajadores',
|
||||||
|
3: 'registro-general',
|
||||||
|
4: 'registro-general-externos',
|
||||||
|
};
|
||||||
|
let data: any[] = [];
|
||||||
|
|
||||||
|
const workbook = new ExcelJS.Workbook();
|
||||||
|
await workbook.xlsx.readFile(filePath);
|
||||||
|
|
||||||
|
const eventosSheet = workbook.getWorksheet('Eventos');
|
||||||
|
const cuestionariosSheet = workbook.getWorksheet('Cuestionarios');
|
||||||
|
const seccionesSheet = workbook.getWorksheet('Secciones');
|
||||||
|
const preguntasSheet = workbook.getWorksheet('Preguntas');
|
||||||
|
const opcionesSheet = workbook.getWorksheet('Opciones');
|
||||||
|
|
||||||
|
if (!eventosSheet || !cuestionariosSheet || !seccionesSheet || !preguntasSheet || !opcionesSheet) {
|
||||||
|
throw new UnprocessableEntityException('El archivo Excel debe contener las hojas: Eventos, Cuestionarios, Secciones, Preguntas y Opciones.');
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 2; i <= eventosSheet.rowCount; i++) {
|
||||||
|
const row = eventosSheet.getRow(i);
|
||||||
|
|
||||||
|
// Construir objeto evento
|
||||||
|
const eventoDto = {
|
||||||
|
tipo_evento: row.getCell(2).value as string,
|
||||||
|
nombre_evento: row.getCell(3).value as string,
|
||||||
|
descripcion_evento: row.getCell(4).value as string,
|
||||||
|
fecha_inicio: new Date(row.getCell(5).value as string),
|
||||||
|
fecha_fin: new Date(row.getCell(6).value as string),
|
||||||
|
banner: row.getCell(7).value as string, // nombre del archivo subido previamente
|
||||||
|
};
|
||||||
|
|
||||||
|
// Filtrar cuestionarios de este evento
|
||||||
|
const cuestionarios = cuestionariosSheet.getRows(2, cuestionariosSheet.rowCount - 1)
|
||||||
|
?.filter(qRow => qRow.getCell(2).value === row.getCell(1).value) || [];
|
||||||
|
|
||||||
|
for (const qRow of cuestionarios) {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const plantilla = qRow.getCell(10).value ? Number(qRow.getCell(10).value) : null;
|
||||||
|
console.log("Valor de la plantilla: ", plantilla);
|
||||||
|
if (plantilla !== null && plantilla !== undefined) {
|
||||||
|
console.log("Tipo de la plantilla: ", PLANTILLA_MAP[plantilla]);
|
||||||
|
}
|
||||||
|
if (plantilla && PLANTILLA_MAP[plantilla]) {
|
||||||
|
console.log("fecha fin desde ecxel: ",qRow.getCell(5).value as string);
|
||||||
|
const fecha_inicio = new Date(qRow.getCell(5).value as string);
|
||||||
|
|
||||||
|
|
||||||
|
console.log("Fecha de inicio: ", fecha_inicio);
|
||||||
|
const fecha_fin = new Date(qRow.getCell(6).value as string);
|
||||||
|
console.log("Fecha de fin: ", fecha_fin);
|
||||||
|
const id_tipo_evento= Number(qRow.getCell(8).value);
|
||||||
|
const plantillaId = PLANTILLA_MAP[plantilla];
|
||||||
|
const base = PLANTILLAS.find(p => p.id === plantillaId);
|
||||||
|
if (!base) throw new Error(`Plantilla ${plantillaId} no encontrada`);
|
||||||
|
console.log("ID de la plantilla: ", plantillaId);
|
||||||
|
console.log("Base de la plantilla: ", base);
|
||||||
|
const nombre_form= qRow.getCell(3).value as string;
|
||||||
|
const descripcion= qRow.getCell(4).value as string;
|
||||||
|
const cupo_maximo= Number(qRow.getCell(9).value) || undefined;
|
||||||
|
|
||||||
|
const createDto: CreateEventoWithCuestionarioDto = {
|
||||||
|
evento: eventoDto,
|
||||||
|
cuestionario: {
|
||||||
|
...base.datos,
|
||||||
|
fecha_inicio,
|
||||||
|
fecha_fin,
|
||||||
|
id_tipo_evento,
|
||||||
|
nombre_form,
|
||||||
|
descripcion,
|
||||||
|
cupo_maximo, // Aquí puedes agregar más campos si es necesario
|
||||||
|
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
data.push(await this.createCuestionarioEvento(createDto));
|
||||||
|
|
||||||
|
continue; // Saltar al siguiente cuestionario
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtrar secciones de este cuestionario
|
||||||
|
const secciones = seccionesSheet.getRows(2, seccionesSheet.rowCount - 1)
|
||||||
|
?.filter(sRow => sRow.getCell(2).value === qRow.getCell(1).value)
|
||||||
|
.map(sRow => {
|
||||||
|
// Filtrar preguntas de esta sección
|
||||||
|
const preguntas = preguntasSheet.getRows(2, preguntasSheet.rowCount - 1)
|
||||||
|
?.filter(pRow => pRow.getCell(2).value === sRow.getCell(1).value)
|
||||||
|
.map(pRow => {
|
||||||
|
const opciones = opcionesSheet.getRows(2, opcionesSheet.rowCount - 1)
|
||||||
|
?.filter(oRow => oRow.getCell(2).value === pRow.getCell(1).value)
|
||||||
|
.map(oRow => ({ valor: oRow.getCell(3).value as string })) || [];
|
||||||
|
|
||||||
|
console.log("Este es el titulo: ", pRow.getCell(3).value as string);
|
||||||
|
console.log("Este es el tipo de la pregunta: ",pRow.getCell(4).value as string);
|
||||||
|
|
||||||
|
return {
|
||||||
|
titulo: pRow.getCell(3).value as string,
|
||||||
|
tipo: this.mapTipoPregunta(pRow.getCell(4).value as string),
|
||||||
|
obligatoria: !!pRow.getCell(5).value,
|
||||||
|
validacion: this.mapValidacion(pRow.getCell(6).value as string),
|
||||||
|
opciones,
|
||||||
|
};
|
||||||
|
}) || [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
titulo: sRow.getCell(3).value as string,
|
||||||
|
descripcion: sRow.getCell(4).value as string,
|
||||||
|
preguntas,
|
||||||
|
};
|
||||||
|
}) || [];
|
||||||
|
|
||||||
|
// Construir DTO completo
|
||||||
|
const createDto: CreateEventoWithCuestionarioDto = {
|
||||||
|
evento: eventoDto,
|
||||||
|
cuestionario: {
|
||||||
|
nombre_form: qRow.getCell(3).value as string,
|
||||||
|
descripcion: qRow.getCell(4).value as string,
|
||||||
|
fecha_inicio: new Date(qRow.getCell(5).value as string),
|
||||||
|
fecha_fin: new Date(qRow.getCell(6).value as string),
|
||||||
|
id_tipo_cuestionario: Number(qRow.getCell(7).value),
|
||||||
|
id_tipo_evento: Number(qRow.getCell(8).value), // obligatorio
|
||||||
|
cupo_maximo: Number(qRow.getCell(9).value) || undefined,
|
||||||
|
secciones,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Llamada a tu servicio para guardar en DB
|
||||||
|
data.push(await this.createCuestionarioEvento(createDto));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { message: 'Carga masiva completada', data };
|
||||||
|
}
|
||||||
|
|
||||||
async create(createCuestionarioDto: CreateCuestionarioDto) {
|
async create(createCuestionarioDto: CreateCuestionarioDto) {
|
||||||
// Verificar que el evento exista
|
// Verificar que el evento exista
|
||||||
await this.eventoService.getEventoOrFail(createCuestionarioDto.id_evento);
|
await this.eventoService.getEventoOrFail(createCuestionarioDto.id_evento);
|
||||||
@@ -73,6 +257,7 @@ export class CuestionarioService {
|
|||||||
cuestionario.editable = true;
|
cuestionario.editable = true;
|
||||||
cuestionario.id_evento = createCuestionarioDto.id_evento;
|
cuestionario.id_evento = createCuestionarioDto.id_evento;
|
||||||
cuestionario.cupo_maximo = createCuestionarioDto.cupo_maximo;
|
cuestionario.cupo_maximo = createCuestionarioDto.cupo_maximo;
|
||||||
|
cuestionario.mensaje_correo = createCuestionarioDto.mensaje_correo;
|
||||||
|
|
||||||
const savedCuestionario = await queryRunner.manager.save(cuestionario);
|
const savedCuestionario = await queryRunner.manager.save(cuestionario);
|
||||||
|
|
||||||
@@ -209,17 +394,19 @@ export class CuestionarioService {
|
|||||||
await queryRunner.startTransaction();
|
await queryRunner.startTransaction();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
/*
|
||||||
// Buscar si ya existe un evento con el nombre proporcionado
|
// Buscar si ya existe un evento con el nombre proporcionado
|
||||||
let eventoExistente = await this.eventoRepository.findOne({
|
let eventoExistente = await this.eventoRepository.findOne({
|
||||||
where: { nombre_evento: evento.nombre_evento },
|
where: { nombre_evento: evento.nombre_evento },
|
||||||
});
|
});
|
||||||
|
**/
|
||||||
|
|
||||||
// Si el evento no existe, crearlo
|
// Si el evento no existe, crearlo
|
||||||
if (!eventoExistente) {
|
//if (!eventoExistente) {
|
||||||
eventoExistente = queryRunner.manager.create(Evento, evento);
|
let eventoExistente = queryRunner.manager.create(Evento, evento);
|
||||||
await queryRunner.manager.save(eventoExistente);
|
await queryRunner.manager.save(eventoExistente);
|
||||||
console.log(`Creando nuevo evento: ${evento.nombre_evento}`);
|
console.log(`Creando nuevo evento: ${evento.nombre_evento}`);
|
||||||
}
|
//}
|
||||||
|
|
||||||
// Crear el cuestionario asociado al evento
|
// Crear el cuestionario asociado al evento
|
||||||
const cuestionarioEntity = queryRunner.manager.create(Cuestionario, {
|
const cuestionarioEntity = queryRunner.manager.create(Cuestionario, {
|
||||||
@@ -233,6 +420,7 @@ export class CuestionarioService {
|
|||||||
editable: true,
|
editable: true,
|
||||||
id_evento: eventoExistente.id_evento, // Asociar el evento creado
|
id_evento: eventoExistente.id_evento, // Asociar el evento creado
|
||||||
cupo_maximo: cuestionario.cupo_maximo,
|
cupo_maximo: cuestionario.cupo_maximo,
|
||||||
|
mensaje_correo: cuestionario.mensaje_correo,
|
||||||
});
|
});
|
||||||
const savedCuestionario =
|
const savedCuestionario =
|
||||||
await queryRunner.manager.save(cuestionarioEntity);
|
await queryRunner.manager.save(cuestionarioEntity);
|
||||||
|
|||||||
@@ -89,6 +89,15 @@ export class CreateCuestionarioDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
evento?: string;
|
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({
|
@ApiProperty({
|
||||||
description: 'Secciones del cuestionario',
|
description: 'Secciones del cuestionario',
|
||||||
type: [CreateSeccionDto],
|
type: [CreateSeccionDto],
|
||||||
|
|||||||
@@ -51,6 +51,9 @@ export class Cuestionario {
|
|||||||
@Column({ type: 'int' })
|
@Column({ type: 'int' })
|
||||||
id_evento: number;
|
id_evento: number;
|
||||||
|
|
||||||
|
@Column({ type: 'text', nullable: true })
|
||||||
|
mensaje_correo?: string;
|
||||||
|
|
||||||
// Relaciones
|
// Relaciones
|
||||||
@ManyToOne(() => Evento, (evento) => evento.cuestionarios, { nullable: true })
|
@ManyToOne(() => Evento, (evento) => evento.cuestionarios, { nullable: true })
|
||||||
@JoinColumn({ name: 'id_evento' })
|
@JoinColumn({ name: 'id_evento' })
|
||||||
|
|||||||
@@ -0,0 +1,165 @@
|
|||||||
|
import { TiposValidacion } from 'src/pregunta/entities/pregunta.entity';
|
||||||
|
import {
|
||||||
|
TipoPreguntaEnum,
|
||||||
|
} from '../tipo_pregunta/entities/tipo_pregunta.entity';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export const PLANTILLAS = [
|
||||||
|
{
|
||||||
|
imagen: '/plantilla_trabajadores.png',
|
||||||
|
nombre: 'Registro Comunidad Trabajador',
|
||||||
|
id: 'registro-comunidad-trabajadores',
|
||||||
|
datos: {
|
||||||
|
nombre_form: 'Registro',
|
||||||
|
descripcion:
|
||||||
|
'Con este formulario estaras reservando tu lugar en el evento. Por favor, completa todos los campos obligatorios.',
|
||||||
|
id_tipo_cuestionario: 2,
|
||||||
|
secciones: [
|
||||||
|
{
|
||||||
|
titulo: 'Información Personal',
|
||||||
|
descripcion:
|
||||||
|
'Proporciona tus datos personales para poder contactarte y enviarte información relevante sobre la Feria de la Sexualidad.',
|
||||||
|
preguntas: [
|
||||||
|
{
|
||||||
|
titulo: '¿Eres parte de la comunidad de la FES Acatlán?',
|
||||||
|
tipo: TipoPreguntaEnum.Cerrada,
|
||||||
|
opciones: [{ valor: 'Si' }, { valor: 'No' }],
|
||||||
|
obligatoria: true,
|
||||||
|
validacion: TiposValidacion.COMUNIDAD_TRABAJADOR,
|
||||||
|
},
|
||||||
|
{ titulo: 'RFC', tipo: TipoPreguntaEnum.AbiertaRespuestaCorta, obligatoria: false, validacion: TiposValidacion.RFC },
|
||||||
|
{ titulo: 'Correo electrónico', tipo: TipoPreguntaEnum.AbiertaRespuestaCorta, obligatoria: true, validacion: TiposValidacion.CORREO },
|
||||||
|
{ titulo: 'Nombre(s)', tipo: TipoPreguntaEnum.AbiertaRespuestaCorta, obligatoria: true, validacion: TiposValidacion.NOMBRE },
|
||||||
|
{ titulo: 'Apellidos', tipo: TipoPreguntaEnum.AbiertaRespuestaCorta, obligatoria: true, validacion: TiposValidacion.APELLIDOS },
|
||||||
|
{
|
||||||
|
titulo: 'Género',
|
||||||
|
tipo: TipoPreguntaEnum.Cerrada,
|
||||||
|
obligatoria: true,
|
||||||
|
validacion: TiposValidacion.GENERO,
|
||||||
|
opciones: [
|
||||||
|
{ valor: 'Masculino' },
|
||||||
|
{ valor: 'Femenino' },
|
||||||
|
{ valor: 'Prefiero no decirlo' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ titulo: 'Carrera', tipo: TipoPreguntaEnum.AbiertaRespuestaCorta, obligatoria: true, validacion: TiposValidacion.CARRERA },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
imagen: '/plantilla_alumnos.png',
|
||||||
|
nombre: 'Registro Comunidad Alumnos',
|
||||||
|
id: 'registro-comunidad-alumnos',
|
||||||
|
datos: {
|
||||||
|
nombre_form: 'Registro',
|
||||||
|
descripcion:
|
||||||
|
'Con este formulario estaras reservando tu lugar en el evento. Por favor, completa todos los campos obligatorios.',
|
||||||
|
id_tipo_cuestionario: 1,
|
||||||
|
secciones: [
|
||||||
|
{
|
||||||
|
titulo: 'Información Personal',
|
||||||
|
descripcion:
|
||||||
|
'Proporciona tus datos personales para poder contactarte y enviarte información relevante sobre la Feria de la Sexualidad.',
|
||||||
|
preguntas: [
|
||||||
|
{
|
||||||
|
titulo: '¿Eres parte de la comunidad de la FES Acatlán?',
|
||||||
|
tipo: TipoPreguntaEnum.Cerrada,
|
||||||
|
opciones: [{ valor: 'Si' }, { valor: 'No' }],
|
||||||
|
obligatoria: true,
|
||||||
|
validacion: TiposValidacion.COMUNIDAD_ALUMNO,
|
||||||
|
},
|
||||||
|
{ titulo: 'Numero de cuenta', tipo: TipoPreguntaEnum.AbiertaRespuestaCorta, obligatoria: false, validacion: TiposValidacion.CUENTA_ALUMNO },
|
||||||
|
{ titulo: 'Correo electrónico', tipo: TipoPreguntaEnum.AbiertaRespuestaCorta, obligatoria: true, validacion: TiposValidacion.CORREO },
|
||||||
|
{ titulo: 'Nombre(s)', tipo: TipoPreguntaEnum.AbiertaRespuestaCorta, obligatoria: true, validacion: TiposValidacion.NOMBRE },
|
||||||
|
{ titulo: 'Apellidos', tipo: TipoPreguntaEnum.AbiertaRespuestaCorta, obligatoria: true, validacion: TiposValidacion.APELLIDOS },
|
||||||
|
{
|
||||||
|
titulo: 'Género',
|
||||||
|
tipo: TipoPreguntaEnum.Cerrada,
|
||||||
|
obligatoria: true,
|
||||||
|
validacion: TiposValidacion.GENERO,
|
||||||
|
opciones: [
|
||||||
|
{ valor: 'Masculino' },
|
||||||
|
{ valor: 'Femenino' },
|
||||||
|
{ valor: 'Prefiero no decirlo' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ titulo: 'Institución de procedencia', tipo: TipoPreguntaEnum.AbiertaRespuestaCorta, obligatoria: true, validacion: TiposValidacion.INSTITUCION },
|
||||||
|
{ titulo: 'Carrera', tipo: TipoPreguntaEnum.AbiertaRespuestaCorta, obligatoria: true, validacion: TiposValidacion.CARRERA },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
imagen: '/plantilla_general.png',
|
||||||
|
nombre: 'Registro General',
|
||||||
|
id: 'registro-general',
|
||||||
|
datos: {
|
||||||
|
nombre_form: 'Registro Para Asistencia General',
|
||||||
|
descripcion:
|
||||||
|
'Con este formulario estaras reservando tu lugar en el evento. Por favor, completa todos los campos obligatorios.',
|
||||||
|
id_tipo_cuestionario: 1,
|
||||||
|
secciones: [
|
||||||
|
{
|
||||||
|
titulo: 'Información Personal',
|
||||||
|
descripcion:
|
||||||
|
'Proporciona tus datos personales para poder contactarte y enviarte información relevante sobre la Feria de la Sexualidad.',
|
||||||
|
preguntas: [
|
||||||
|
{ titulo: 'Número de cuenta / trabajador', tipo: TipoPreguntaEnum.AbiertaRespuestaCorta, obligatoria: false },
|
||||||
|
{ titulo: 'Correo electrónico', tipo: TipoPreguntaEnum.AbiertaRespuestaCorta, obligatoria: true, validacion: TiposValidacion.CORREO },
|
||||||
|
{ titulo: 'Nombre(s)', tipo: TipoPreguntaEnum.AbiertaRespuestaCorta, obligatoria: true, validacion: TiposValidacion.NOMBRE },
|
||||||
|
{ titulo: 'Apellidos', tipo: TipoPreguntaEnum.AbiertaRespuestaCorta, obligatoria: true, validacion: TiposValidacion.APELLIDOS },
|
||||||
|
{
|
||||||
|
titulo: 'Tipo de Asistente',
|
||||||
|
tipo: TipoPreguntaEnum.Cerrada,
|
||||||
|
obligatoria: true,
|
||||||
|
opciones: [
|
||||||
|
{ valor: 'Alumno' },
|
||||||
|
{ valor: 'Profesor' },
|
||||||
|
{ valor: 'Trabajador' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
imagen: '/plantilla_general.png',
|
||||||
|
nombre: 'Registro General Externos',
|
||||||
|
id: 'registro-general-externos',
|
||||||
|
datos: {
|
||||||
|
nombre_form: 'Registro Para Asistencia General',
|
||||||
|
descripcion:
|
||||||
|
'Con este formulario estaras reservando tu lugar en el evento. Por favor, completa todos los campos obligatorios.',
|
||||||
|
id_tipo_cuestionario: 1,
|
||||||
|
secciones: [
|
||||||
|
{
|
||||||
|
titulo: 'Información Personal',
|
||||||
|
descripcion:
|
||||||
|
'Proporciona tus datos personales para poder contactarte y enviarte información relevante sobre la Feria de la Sexualidad.',
|
||||||
|
preguntas: [
|
||||||
|
{ titulo: 'Número de cuenta / trabajador', tipo: TipoPreguntaEnum.AbiertaRespuestaCorta, obligatoria: false },
|
||||||
|
{ titulo: 'Correo electrónico', tipo: TipoPreguntaEnum.AbiertaRespuestaCorta, obligatoria: true, validacion: TiposValidacion.CORREO },
|
||||||
|
{ titulo: 'Nombre(s)', tipo: TipoPreguntaEnum.AbiertaRespuestaCorta, obligatoria: true, validacion: TiposValidacion.NOMBRE },
|
||||||
|
{ titulo: 'Apellidos', tipo: TipoPreguntaEnum.AbiertaRespuestaCorta, obligatoria: true, validacion: TiposValidacion.APELLIDOS },
|
||||||
|
{
|
||||||
|
titulo: 'Tipo de Asistente',
|
||||||
|
tipo: TipoPreguntaEnum.Cerrada,
|
||||||
|
obligatoria: true,
|
||||||
|
opciones: [
|
||||||
|
{ valor: 'Alumno' },
|
||||||
|
{ valor: 'Profesor' },
|
||||||
|
{ valor: 'Trabajador' },
|
||||||
|
{ valor: 'Externo'},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -14,6 +14,7 @@ import { RespuestaParticipanteCerrada } from '../respuesta_participante_cerrada/
|
|||||||
import { PreguntaOpcion } from '../pregunta_opcion/entities/pregunta_opcion.entity';
|
import { PreguntaOpcion } from '../pregunta_opcion/entities/pregunta_opcion.entity';
|
||||||
import { Opcion } from '../opcion/entities/opcion.entity';
|
import { Opcion } from '../opcion/entities/opcion.entity';
|
||||||
import { ValidacionesModule } from '../validaciones/validaciones.module';
|
import { ValidacionesModule } from '../validaciones/validaciones.module';
|
||||||
|
import { EventoUsuarioModule } from '../evento_usuario/evento_usuario.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -31,6 +32,7 @@ import { ValidacionesModule } from '../validaciones/validaciones.module';
|
|||||||
ParticipanteModule,
|
ParticipanteModule,
|
||||||
ValidacionesModule,
|
ValidacionesModule,
|
||||||
QrModule,
|
QrModule,
|
||||||
|
EventoUsuarioModule,
|
||||||
],
|
],
|
||||||
controllers: [CuestionarioRespondidoController],
|
controllers: [CuestionarioRespondidoController],
|
||||||
providers: [CuestionarioRespondidoService],
|
providers: [CuestionarioRespondidoService],
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
NotFoundException,
|
NotFoundException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
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 { CreateCuestionarioRespondidoDto } from './dto/create-cuestionario_respondido.dto';
|
||||||
import { UpdateCuestionarioRespondidoDto } from './dto/update-cuestionario_respondido.dto';
|
import { UpdateCuestionarioRespondidoDto } from './dto/update-cuestionario_respondido.dto';
|
||||||
import { SubmitRespuestasDto } from './dto/submit-respuestas.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 { CuestionarioService } from 'src/cuestionario/cuestionario.service';
|
||||||
import { generarHtmlCorreoAsistencia } from 'src/emails/registro-evento';
|
import { generarHtmlCorreoAsistencia } from 'src/emails/registro-evento';
|
||||||
import { TipoPreguntaEnum } from 'src/tipo_pregunta/entities/tipo_pregunta.entity';
|
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()
|
@Injectable()
|
||||||
export class CuestionarioRespondidoService {
|
export class CuestionarioRespondidoService {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -41,6 +44,7 @@ export class CuestionarioRespondidoService {
|
|||||||
private validadorRespuestasService: ValidadorRespuestasService,
|
private validadorRespuestasService: ValidadorRespuestasService,
|
||||||
private cuestionarioService: CuestionarioService,
|
private cuestionarioService: CuestionarioService,
|
||||||
private qrTokenService: QrTokenService,
|
private qrTokenService: QrTokenService,
|
||||||
|
private eventoUsuarioService: EventoUsuarioService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
create(createCuestionarioRespondidoDto: CreateCuestionarioRespondidoDto) {
|
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
|
// 2. Buscar o crear participante
|
||||||
let participante = await this.participanteRepository.findOne({
|
let participante = await this.participanteRepository.findOne({
|
||||||
where: { correo: submitDto.correo },
|
where: { correo: submitDto.correo },
|
||||||
@@ -116,6 +186,7 @@ export class CuestionarioRespondidoService {
|
|||||||
participante.id_participante,
|
participante.id_participante,
|
||||||
cuestionario.evento.id_evento,
|
cuestionario.evento.id_evento,
|
||||||
cuestionario.id_cuestionario,
|
cuestionario.id_cuestionario,
|
||||||
|
cuestionario.fecha_fin,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Generar código QR con el token
|
// Generar código QR con el token
|
||||||
@@ -135,6 +206,7 @@ export class CuestionarioRespondidoService {
|
|||||||
nombreForm: cuestionario.nombre_form,
|
nombreForm: cuestionario.nombre_form,
|
||||||
nombreEvento: cuestionario.evento?.nombre_evento,
|
nombreEvento: cuestionario.evento?.nombre_evento,
|
||||||
correo: participante.correo,
|
correo: participante.correo,
|
||||||
|
tipoEvento: cuestionario.evento.tipo_evento,
|
||||||
fechaRegistro: new Date().toLocaleString(),
|
fechaRegistro: new Date().toLocaleString(),
|
||||||
fechaInicioEvento: cuestionario.evento?.fecha_inicio
|
fechaInicioEvento: cuestionario.evento?.fecha_inicio
|
||||||
? new Date(
|
? new Date(
|
||||||
@@ -144,6 +216,7 @@ export class CuestionarioRespondidoService {
|
|||||||
fechaFinEvento: cuestionario.evento?.fecha_fin
|
fechaFinEvento: cuestionario.evento?.fecha_fin
|
||||||
? new Date(cuestionario.evento.fecha_fin).toLocaleString()
|
? new Date(cuestionario.evento.fecha_fin).toLocaleString()
|
||||||
: undefined,
|
: undefined,
|
||||||
|
mensajePersonalizado: cuestionario.mensaje_correo,
|
||||||
}),
|
}),
|
||||||
adjuntos: [
|
adjuntos: [
|
||||||
{
|
{
|
||||||
@@ -340,6 +413,7 @@ export class CuestionarioRespondidoService {
|
|||||||
participante.id_participante,
|
participante.id_participante,
|
||||||
cuestionario.evento.id_evento,
|
cuestionario.evento.id_evento,
|
||||||
cuestionario.id_cuestionario,
|
cuestionario.id_cuestionario,
|
||||||
|
cuestionario.fecha_fin,
|
||||||
);
|
);
|
||||||
|
|
||||||
qrBuffer = await QRCode.toBuffer(qrToken);
|
qrBuffer = await QRCode.toBuffer(qrToken);
|
||||||
@@ -363,6 +437,7 @@ export class CuestionarioRespondidoService {
|
|||||||
nombreForm: cuestionario.nombre_form,
|
nombreForm: cuestionario.nombre_form,
|
||||||
nombreEvento: cuestionario.evento?.nombre_evento,
|
nombreEvento: cuestionario.evento?.nombre_evento,
|
||||||
correo: participante.correo,
|
correo: participante.correo,
|
||||||
|
tipoEvento: cuestionario.evento.tipo_evento,
|
||||||
fechaRegistro: new Date().toLocaleString(),
|
fechaRegistro: new Date().toLocaleString(),
|
||||||
fechaInicioEvento: cuestionario.evento?.fecha_inicio
|
fechaInicioEvento: cuestionario.evento?.fecha_inicio
|
||||||
? new Date(cuestionario.evento.fecha_inicio).toLocaleString()
|
? new Date(cuestionario.evento.fecha_inicio).toLocaleString()
|
||||||
@@ -370,6 +445,7 @@ export class CuestionarioRespondidoService {
|
|||||||
fechaFinEvento: cuestionario.evento?.fecha_fin
|
fechaFinEvento: cuestionario.evento?.fecha_fin
|
||||||
? new Date(cuestionario.evento.fecha_fin).toLocaleString()
|
? new Date(cuestionario.evento.fecha_fin).toLocaleString()
|
||||||
: undefined,
|
: undefined,
|
||||||
|
mensajePersonalizado: cuestionario.mensaje_correo,
|
||||||
}),
|
}),
|
||||||
adjuntos: [
|
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 { TipoPregunta } from 'src/tipo_pregunta/entities/tipo_pregunta.entity';
|
||||||
import { TipoUser } from 'src/tipo_user/entities/tipo_user.entity';
|
import { TipoUser } from 'src/tipo_user/entities/tipo_user.entity';
|
||||||
import { TipoEvento } from 'src/tipo_evento/entities/tipo_evento.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 (
|
export const createMainDbConfig = async (
|
||||||
configService: ConfigService,
|
configService: ConfigService,
|
||||||
@@ -51,13 +52,13 @@ export const createMainDbConfig = async (
|
|||||||
TipoEvento,
|
TipoEvento,
|
||||||
TipoPregunta,
|
TipoPregunta,
|
||||||
TipoUser,
|
TipoUser,
|
||||||
|
EventoUsuario,
|
||||||
],
|
],
|
||||||
retryAttempts: 5,
|
retryAttempts: 5,
|
||||||
retryDelay: 3000,
|
retryDelay: 3000,
|
||||||
connectTimeout: 30000,
|
connectTimeout: 30000,
|
||||||
|
|
||||||
synchronize: configService.get<string>('SYNCHRONIZE') === 'true',
|
synchronize: configService.get<string>('SYNCHRONIZE') === 'true',
|
||||||
dropSchema: configService.get<string>('DROP_SCHEMA') === 'true',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const createAlumnosDbConfig = async (
|
export const createAlumnosDbConfig = async (
|
||||||
@@ -75,6 +76,5 @@ export const createAlumnosDbConfig = async (
|
|||||||
retryDelay: 3000,
|
retryDelay: 3000,
|
||||||
connectTimeout: 30000,
|
connectTimeout: 30000,
|
||||||
|
|
||||||
synchronize: false,
|
synchronize: true,
|
||||||
dropSchema: false,
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,17 @@
|
|||||||
|
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({
|
export function generarHtmlCorreoAsistencia({
|
||||||
nombreForm,
|
nombreForm,
|
||||||
nombreEvento,
|
nombreEvento,
|
||||||
@@ -5,16 +19,21 @@ export function generarHtmlCorreoAsistencia({
|
|||||||
fechaRegistro,
|
fechaRegistro,
|
||||||
fechaInicioEvento,
|
fechaInicioEvento,
|
||||||
fechaFinEvento,
|
fechaFinEvento,
|
||||||
|
tipoEvento,
|
||||||
|
mensajePersonalizado,
|
||||||
}: {
|
}: {
|
||||||
nombreForm: string;
|
nombreForm: string;
|
||||||
nombreEvento: string;
|
nombreEvento: string;
|
||||||
correo: string;
|
correo: string;
|
||||||
|
tipoEvento: string;
|
||||||
fechaRegistro: string;
|
fechaRegistro: string;
|
||||||
fechaInicioEvento?: string;
|
fechaInicioEvento?: string;
|
||||||
fechaFinEvento?: string;
|
fechaFinEvento?: string;
|
||||||
|
mensajePersonalizado?: string;
|
||||||
}) {
|
}) {
|
||||||
const horarioEvento = fechaInicioEvento && fechaFinEvento
|
const horarioEvento =
|
||||||
? `
|
fechaInicioEvento && fechaFinEvento
|
||||||
|
? `
|
||||||
<div style="margin: 20px 0; padding: 12px; background-color: #e8f0fe; border-left: 4px solid #003d79;">
|
<div style="margin: 20px 0; padding: 12px; background-color: #e8f0fe; border-left: 4px solid #003d79;">
|
||||||
<p style="margin: 0; font-size: 15px; color: #003d79;">
|
<p style="margin: 0; font-size: 15px; color: #003d79;">
|
||||||
<strong>🕒 Horario del evento:</strong><br/>
|
<strong>🕒 Horario del evento:</strong><br/>
|
||||||
@@ -22,7 +41,7 @@ export function generarHtmlCorreoAsistencia({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 24px; border: 1px solid #ccc; border-radius: 8px; background-color: #f9f9f9;">
|
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 24px; border: 1px solid #ccc; border-radius: 8px; background-color: #f9f9f9;">
|
||||||
@@ -33,7 +52,7 @@ export function generarHtmlCorreoAsistencia({
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p style="font-size: 16px; color: #333;">
|
<p style="font-size: 16px; color: #333;">
|
||||||
Favor de presentar este código QR (celular o impreso) en la mesa de registro. el día del evento, para validar su asistencia y tener derecho a la constancia.
|
${mensajePersonalizado ?? getMensajeDefaultPorTipoEvento(tipoEvento)}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div style="text-align: center; margin: 20px 0;">
|
<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(
|
static ApiPostBanner = applyDecorators(
|
||||||
ApiOperation({ summary: 'Subir banner para un evento' }),
|
ApiOperation({ summary: 'Subir banner para un evento' }),
|
||||||
ApiParam({
|
ApiParam({
|
||||||
|
|||||||
@@ -23,4 +23,8 @@ export class CreateEventoDto {
|
|||||||
@IsDate({ message: 'La fecha de fin debe ser una fecha válida.' })
|
@IsDate({ message: 'La fecha de fin debe ser una fecha válida.' })
|
||||||
@IsNotEmpty({ message: 'La fecha de fin es obligatoria.' })
|
@IsNotEmpty({ message: 'La fecha de fin es obligatoria.' })
|
||||||
fecha_fin: Date;
|
fecha_fin: Date;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
banner?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,11 +19,15 @@ import { FileInterceptor } from '@nestjs/platform-express';
|
|||||||
import { diskStorage } from 'multer';
|
import { diskStorage } from 'multer';
|
||||||
import { extname } from 'path';
|
import { extname } from 'path';
|
||||||
import { EventoApiDocumentation } from './docs/evento.documentation';
|
import { EventoApiDocumentation } from './docs/evento.documentation';
|
||||||
|
import { EventoUsuarioService } from '../evento_usuario/evento_usuario.service';
|
||||||
|
|
||||||
@Controller('evento')
|
@Controller('evento')
|
||||||
@EventoApiDocumentation.ApiController
|
@EventoApiDocumentation.ApiController
|
||||||
export class EventoController {
|
export class EventoController {
|
||||||
constructor(private eventoService: EventoService) {}
|
constructor(
|
||||||
|
private eventoService: EventoService,
|
||||||
|
private eventoUsuarioService: EventoUsuarioService,
|
||||||
|
) {}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@EventoApiDocumentation.ApiCreate
|
@EventoApiDocumentation.ApiCreate
|
||||||
@@ -64,6 +68,42 @@ export class EventoController {
|
|||||||
return this.eventoService.getCuestionarioEvento(id_evento, id_cuestionario);
|
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')
|
@Post(':id/banner')
|
||||||
@UseInterceptors(
|
@UseInterceptors(
|
||||||
FileInterceptor('banner', {
|
FileInterceptor('banner', {
|
||||||
|
|||||||
@@ -3,11 +3,15 @@ import { EventoService } from './evento.service';
|
|||||||
import { EventoController } from './evento.controller';
|
import { EventoController } from './evento.controller';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { Evento } from './entities/evento.entity';
|
import { Evento } from './entities/evento.entity';
|
||||||
|
import { EventoUsuarioModule } from '../evento_usuario/evento_usuario.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([Evento])],
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([Evento]),
|
||||||
|
EventoUsuarioModule,
|
||||||
|
],
|
||||||
controllers: [EventoController],
|
controllers: [EventoController],
|
||||||
providers: [EventoService],
|
providers: [EventoService],
|
||||||
exports: [EventoService]
|
exports: [EventoService],
|
||||||
})
|
})
|
||||||
export class EventoModule {}
|
export class EventoModule {}
|
||||||
|
|||||||
@@ -127,7 +127,10 @@ export class EventoService {
|
|||||||
relations: ['cuestionarios', 'cuestionarios.cuestionariosRespondidos'],
|
relations: ['cuestionarios', 'cuestionarios.cuestionariosRespondidos'],
|
||||||
});
|
});
|
||||||
|
|
||||||
const eventosConCupos = eventos.map((evento) => {
|
const eventosFiltrados = eventos.filter(
|
||||||
|
(evento) => !(evento.id_evento >= 10 && evento.id_evento <= 148),
|
||||||
|
);
|
||||||
|
const eventosConCupos = eventosFiltrados.map((evento) => {
|
||||||
return {
|
return {
|
||||||
...evento,
|
...evento,
|
||||||
cuestionarios: evento.cuestionarios.map((cuest) => {
|
cuestionarios: evento.cuestionarios.map((cuest) => {
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
@Column({ type: 'int', nullable: true })
|
||||||
|
contador: number | null;
|
||||||
|
}
|
||||||
@@ -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,222 @@
|
|||||||
|
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: { identificador: string; contador: number | null }[],
|
||||||
|
trabajadores: { identificador: string; contador: number | null }[],
|
||||||
|
): Promise<{ insertados: number; omitidos: number; errores: string[] }> {
|
||||||
|
let insertados = 0;
|
||||||
|
let omitidos = 0;
|
||||||
|
const errores: string[] = [];
|
||||||
|
|
||||||
|
for (const alumno of alumnos) {
|
||||||
|
try {
|
||||||
|
const existing = await this.eventoUsuarioRepo.findOne({
|
||||||
|
where: {
|
||||||
|
id_evento,
|
||||||
|
tipo_usuario: TipoUsuarioEnum.ALUMNO,
|
||||||
|
identificador: alumno.identificador.toUpperCase(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
if (existing.contador !== alumno.contador) {
|
||||||
|
await this.eventoUsuarioRepo.update(existing.id_evento_usuario, {
|
||||||
|
contador: alumno.contador,
|
||||||
|
});
|
||||||
|
insertados++;
|
||||||
|
} else {
|
||||||
|
omitidos++;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await this.eventoUsuarioRepo.insert({
|
||||||
|
id_evento,
|
||||||
|
tipo_usuario: TipoUsuarioEnum.ALUMNO,
|
||||||
|
identificador: alumno.identificador.toUpperCase(),
|
||||||
|
contador: alumno.contador,
|
||||||
|
});
|
||||||
|
insertados++;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
omitidos++;
|
||||||
|
errores.push(`Error al insertar cuenta ${alumno.identificador}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const trabajador of trabajadores) {
|
||||||
|
try {
|
||||||
|
const existing = await this.eventoUsuarioRepo.findOne({
|
||||||
|
where: {
|
||||||
|
id_evento,
|
||||||
|
tipo_usuario: TipoUsuarioEnum.TRABAJADOR,
|
||||||
|
identificador: trabajador.identificador.toUpperCase(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
if (existing.contador !== trabajador.contador) {
|
||||||
|
await this.eventoUsuarioRepo.update(existing.id_evento_usuario, {
|
||||||
|
contador: trabajador.contador,
|
||||||
|
});
|
||||||
|
insertados++;
|
||||||
|
} else {
|
||||||
|
omitidos++;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await this.eventoUsuarioRepo.insert({
|
||||||
|
id_evento,
|
||||||
|
tipo_usuario: TipoUsuarioEnum.TRABAJADOR,
|
||||||
|
identificador: trabajador.identificador.toUpperCase(),
|
||||||
|
contador: trabajador.contador,
|
||||||
|
});
|
||||||
|
insertados++;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
omitidos++;
|
||||||
|
errores.push(`Error al insertar RFC ${trabajador.identificador}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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: { identificador: string; contador: number | null }[] =
|
||||||
|
[];
|
||||||
|
const trabajadoresValidos: {
|
||||||
|
identificador: string;
|
||||||
|
contador: number | null;
|
||||||
|
}[] = [];
|
||||||
|
let omitidos = 0;
|
||||||
|
const errores: string[] = [];
|
||||||
|
|
||||||
|
for (const row of data) {
|
||||||
|
const cuentaRaw = row['cuenta'];
|
||||||
|
const rfcRaw = row['rfc'];
|
||||||
|
const contadorRaw = row['contador'];
|
||||||
|
const contador =
|
||||||
|
contadorRaw !== undefined &&
|
||||||
|
contadorRaw !== null &&
|
||||||
|
contadorRaw !== '' &&
|
||||||
|
!isNaN(Number(contadorRaw))
|
||||||
|
? Number(contadorRaw)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (cuentaRaw !== undefined && cuentaRaw !== null && cuentaRaw !== '') {
|
||||||
|
const cuenta = String(cuentaRaw).trim();
|
||||||
|
const alumno = await this.alumnoRepo.findOne({
|
||||||
|
where: { id_ncuenta: parseInt(cuenta, 10) },
|
||||||
|
});
|
||||||
|
if (!alumno) {
|
||||||
|
omitidos++;
|
||||||
|
errores.push(`Cuenta no encontrada en el sistema: ${cuenta}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
alumnosValidos.push({
|
||||||
|
identificador: alumno.id_ncuenta.toString(),
|
||||||
|
contador,
|
||||||
|
});
|
||||||
|
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({ identificador: trabajador.rfc, contador });
|
||||||
|
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),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -233,6 +233,56 @@ export class ParticipanteEventoController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ApiOperation({
|
||||||
|
summary: 'Registrar asistencia de un participante a un evento',
|
||||||
|
})
|
||||||
|
@ApiParam({
|
||||||
|
name: 'idParticipante',
|
||||||
|
description: 'ID del participante',
|
||||||
|
type: 'number',
|
||||||
|
})
|
||||||
|
@ApiParam({ name: 'idEvento', description: 'ID del evento', type: 'number' })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Asistencia registrada correctamente',
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 404, description: 'Registro no encontrado' })
|
||||||
|
@Post('asistencia/:idParticipante/:idEvento')
|
||||||
|
registrarAsistenciaSinToken(
|
||||||
|
@Param('idParticipante', ParseIntPipe) idParticipante: number,
|
||||||
|
@Param('idEvento', ParseIntPipe) idEvento: number,
|
||||||
|
) {
|
||||||
|
return this.participanteEventoService.registrarAsistenciaSinToken(
|
||||||
|
idParticipante,
|
||||||
|
idEvento,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({
|
||||||
|
summary: 'Retornala info del token QR',
|
||||||
|
})
|
||||||
|
@ApiBody({
|
||||||
|
description: 'Token JWT generado por el código QR',
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
token: { type: 'string' },
|
||||||
|
},
|
||||||
|
example: {
|
||||||
|
token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Token válido, información decodificada retornada',
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 400, description: 'Token inválido o expirado' })
|
||||||
|
@Post('decode-token')
|
||||||
|
decodeToken(@Body() body: { token: string }) {
|
||||||
|
return this.participanteEventoService.validToken(body.token);
|
||||||
|
}
|
||||||
|
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: 'Registrar asistencia usando token QR',
|
summary: 'Registrar asistencia usando token QR',
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -7,10 +7,13 @@ import { Evento } from 'src/evento/entities/evento.entity';
|
|||||||
import { Participante } from 'src/participante/entities/participante.entity';
|
import { Participante } from 'src/participante/entities/participante.entity';
|
||||||
import { CuestionarioModule } from 'src/cuestionario/cuestionario.module';
|
import { CuestionarioModule } from 'src/cuestionario/cuestionario.module';
|
||||||
import { QrModule } from 'src/qr/qr.module';
|
import { QrModule } from 'src/qr/qr.module';
|
||||||
|
import { EventoUsuario } from 'src/evento_usuario/entities/evento_usuario.entity';
|
||||||
|
import { RespuestaParticipanteAbierta } from 'src/respuesta_participante_abierta/entities/respuesta_participante_abierta.entity';
|
||||||
|
import { Cuestionario } from 'src/cuestionario/entities/cuestionario.entity';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
TypeOrmModule.forFeature([ParticipanteEvento, Evento, Participante]),
|
TypeOrmModule.forFeature([ParticipanteEvento, Evento, Participante, Cuestionario, EventoUsuario, RespuestaParticipanteAbierta]),
|
||||||
CuestionarioModule,
|
CuestionarioModule,
|
||||||
QrModule,
|
QrModule,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ import { Evento } from 'src/evento/entities/evento.entity';
|
|||||||
import { Participante } from 'src/participante/entities/participante.entity';
|
import { Participante } from 'src/participante/entities/participante.entity';
|
||||||
import { CuestionarioService } from 'src/cuestionario/cuestionario.service';
|
import { CuestionarioService } from 'src/cuestionario/cuestionario.service';
|
||||||
import { QrTokenService } from 'src/qr/qr-token.service';
|
import { QrTokenService } from 'src/qr/qr-token.service';
|
||||||
|
import { EventoUsuario } from 'src/evento_usuario/entities/evento_usuario.entity';
|
||||||
|
import { RespuestaParticipanteAbierta } from 'src/respuesta_participante_abierta/entities/respuesta_participante_abierta.entity';
|
||||||
|
import { TiposValidacion } from 'src/pregunta/entities/pregunta.entity';
|
||||||
|
import { Cuestionario } from 'src/cuestionario/entities/cuestionario.entity';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ParticipanteEventoService {
|
export class ParticipanteEventoService {
|
||||||
@@ -17,6 +21,12 @@ export class ParticipanteEventoService {
|
|||||||
@InjectRepository(Evento) private eventoRepository: Repository<Evento>,
|
@InjectRepository(Evento) private eventoRepository: Repository<Evento>,
|
||||||
@InjectRepository(Participante)
|
@InjectRepository(Participante)
|
||||||
private participanteRepository: Repository<Participante>,
|
private participanteRepository: Repository<Participante>,
|
||||||
|
@InjectRepository(Cuestionario)
|
||||||
|
private cuestionarioRepository: Repository<Cuestionario>,
|
||||||
|
@InjectRepository(EventoUsuario)
|
||||||
|
private eventoUsuarioRepository: Repository<EventoUsuario>,
|
||||||
|
@InjectRepository(RespuestaParticipanteAbierta)
|
||||||
|
private respuestaAbiertaRepository: Repository<RespuestaParticipanteAbierta>,
|
||||||
private readonly cuestionarioService: CuestionarioService,
|
private readonly cuestionarioService: CuestionarioService,
|
||||||
private readonly qrTokenService: QrTokenService,
|
private readonly qrTokenService: QrTokenService,
|
||||||
) {}
|
) {}
|
||||||
@@ -160,10 +170,9 @@ export class ParticipanteEventoService {
|
|||||||
return this.participanteEventoRepository.save(participante_evento);
|
return this.participanteEventoRepository.save(participante_evento);
|
||||||
}
|
}
|
||||||
|
|
||||||
async registrarAsistencia(
|
async registrarAsistenciaSinToken(
|
||||||
id_participante: number,
|
id_participante: number,
|
||||||
id_cuestionario: number,
|
id_cuestionario: number,
|
||||||
token: string,
|
|
||||||
) {
|
) {
|
||||||
const participante_eventoFound =
|
const participante_eventoFound =
|
||||||
await this.participanteEventoRepository.findOne({
|
await this.participanteEventoRepository.findOne({
|
||||||
@@ -179,7 +188,55 @@ export class ParticipanteEventoService {
|
|||||||
|
|
||||||
participante_eventoFound.fecha_asistencia = new Date();
|
participante_eventoFound.fecha_asistencia = new Date();
|
||||||
participante_eventoFound.asistio = true;
|
participante_eventoFound.asistio = true;
|
||||||
return this.participanteEventoRepository.save(participante_eventoFound);
|
const resultado =
|
||||||
|
await this.participanteEventoRepository.save(participante_eventoFound);
|
||||||
|
|
||||||
|
const cuestionario = await this.cuestionarioRepository.findOne({
|
||||||
|
where: { id_cuestionario },
|
||||||
|
});
|
||||||
|
|
||||||
|
let contador: number | null | undefined = undefined;
|
||||||
|
|
||||||
|
if (cuestionario?.id_evento) {
|
||||||
|
const tieneListaRestringida =
|
||||||
|
(await this.eventoUsuarioRepository.count({
|
||||||
|
where: { id_evento: cuestionario.id_evento },
|
||||||
|
})) > 0;
|
||||||
|
|
||||||
|
if (tieneListaRestringida) {
|
||||||
|
const respuesta =
|
||||||
|
(await this.respuestaAbiertaRepository.findOne({
|
||||||
|
where: {
|
||||||
|
cuestionarioRespondido: {
|
||||||
|
participante: { id_participante },
|
||||||
|
cuestionario: { id_cuestionario },
|
||||||
|
},
|
||||||
|
pregunta: { validacion: TiposValidacion.CUENTA_ALUMNO },
|
||||||
|
},
|
||||||
|
relations: ['pregunta', 'cuestionarioRespondido'],
|
||||||
|
})) ??
|
||||||
|
(await this.respuestaAbiertaRepository.findOne({
|
||||||
|
where: {
|
||||||
|
cuestionarioRespondido: {
|
||||||
|
participante: { id_participante },
|
||||||
|
cuestionario: { id_cuestionario },
|
||||||
|
},
|
||||||
|
pregunta: { validacion: TiposValidacion.RFC },
|
||||||
|
},
|
||||||
|
relations: ['pregunta', 'cuestionarioRespondido'],
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (respuesta) {
|
||||||
|
const identificador = respuesta.respuesta.trim().toUpperCase();
|
||||||
|
const eventoUsuario = await this.eventoUsuarioRepository.findOne({
|
||||||
|
where: { id_evento: cuestionario.id_evento, identificador },
|
||||||
|
});
|
||||||
|
contador = eventoUsuario?.contador ?? null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...resultado, contador };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -189,16 +246,18 @@ export class ParticipanteEventoService {
|
|||||||
*/
|
*/
|
||||||
async registrarAsistenciaConToken(qrToken: string) {
|
async registrarAsistenciaConToken(qrToken: string) {
|
||||||
// Validar el token QR
|
// Validar el token QR
|
||||||
const tokenData = this.qrTokenService.validateQrToken(qrToken);
|
const tokenData = await this.qrTokenService.validateQrToken(qrToken);
|
||||||
|
|
||||||
if (!tokenData) {
|
if (!tokenData) {
|
||||||
throw new HttpException(
|
return{
|
||||||
'Token QR inválido o expirado',
|
success: false,
|
||||||
HttpStatus.BAD_REQUEST,
|
message:
|
||||||
);
|
'El token ya caduco',
|
||||||
|
data: {}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
const { id_participante, id_evento, id_cuestionario } = tokenData;
|
|
||||||
|
const { id_participante, id_evento, id_cuestionario, contador } = tokenData;
|
||||||
|
|
||||||
// Verificar que el participante existe
|
// Verificar que el participante existe
|
||||||
const participante = await this.participanteRepository.findOne({
|
const participante = await this.participanteRepository.findOne({
|
||||||
@@ -231,13 +290,14 @@ export class ParticipanteEventoService {
|
|||||||
// Verificar si ya había registrado asistencia
|
// Verificar si ya había registrado asistencia
|
||||||
if (participanteEvento.asistio && participanteEvento.fecha_asistencia) {
|
if (participanteEvento.asistio && participanteEvento.fecha_asistencia) {
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: false,
|
||||||
message:
|
message:
|
||||||
'El participante ya había registrado su asistencia previamente',
|
'El participante ya había registrado su asistencia previamente',
|
||||||
data: {
|
data: {
|
||||||
participante: participante.correo,
|
participante: participante.correo,
|
||||||
fecha_asistencia_previa: participanteEvento.fecha_asistencia,
|
fecha_asistencia_previa: participanteEvento.fecha_asistencia,
|
||||||
fecha_actual: new Date(),
|
fecha_actual: new Date(),
|
||||||
|
contador,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -257,7 +317,19 @@ export class ParticipanteEventoService {
|
|||||||
fecha_asistencia: resultado.fecha_asistencia,
|
fecha_asistencia: resultado.fecha_asistencia,
|
||||||
id_evento,
|
id_evento,
|
||||||
id_cuestionario,
|
id_cuestionario,
|
||||||
|
contador,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async validToken(qrToken: string) {
|
||||||
|
// Validar el token QR
|
||||||
|
const tokenData = this.qrTokenService.validateQrToken(qrToken);
|
||||||
|
|
||||||
|
if (!tokenData) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokenData
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+117
-7
@@ -1,12 +1,30 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||||
import { JwtService } from '@nestjs/jwt';
|
import { JwtService } from '@nestjs/jwt';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { Asistencia } from 'src/asistencia/entities/asistencia.entity';
|
||||||
|
import { Cuestionario } from 'src/cuestionario/entities/cuestionario.entity';
|
||||||
|
import { Evento } from 'src/evento/entities/evento.entity';
|
||||||
|
import { EventoUsuario } from 'src/evento_usuario/entities/evento_usuario.entity';
|
||||||
|
import { RespuestaParticipanteAbierta } from 'src/respuesta_participante_abierta/entities/respuesta_participante_abierta.entity';
|
||||||
|
import { TiposValidacion } from 'src/pregunta/entities/pregunta.entity';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class QrTokenService {
|
export class QrTokenService {
|
||||||
constructor(
|
constructor(
|
||||||
private jwtService: JwtService,
|
private jwtService: JwtService,
|
||||||
private configService: ConfigService,
|
private configService: ConfigService,
|
||||||
|
@InjectRepository(Asistencia)
|
||||||
|
private readonly asistenciaRepository: Repository<Asistencia>,
|
||||||
|
@InjectRepository(Cuestionario)
|
||||||
|
private readonly cuestionarioRepository: Repository<Cuestionario>,
|
||||||
|
@InjectRepository(Evento)
|
||||||
|
private readonly eventoREpo: Repository<Evento>,
|
||||||
|
@InjectRepository(EventoUsuario)
|
||||||
|
private readonly eventoUsuarioRepo: Repository<EventoUsuario>,
|
||||||
|
@InjectRepository(RespuestaParticipanteAbierta)
|
||||||
|
private readonly respuestaAbiertaRepo: Repository<RespuestaParticipanteAbierta>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -20,6 +38,7 @@ export class QrTokenService {
|
|||||||
id_participante: number,
|
id_participante: number,
|
||||||
id_evento: number,
|
id_evento: number,
|
||||||
id_cuestionario: number,
|
id_cuestionario: number,
|
||||||
|
fecha_fin: Date,
|
||||||
): string {
|
): string {
|
||||||
const payload = {
|
const payload = {
|
||||||
id_participante,
|
id_participante,
|
||||||
@@ -34,9 +53,20 @@ export class QrTokenService {
|
|||||||
this.configService.get<string>('QR_JWT_SECRET') ||
|
this.configService.get<string>('QR_JWT_SECRET') ||
|
||||||
this.configService.get<string>('JWT_SECRET', 'tu_clave_secreta');
|
this.configService.get<string>('JWT_SECRET', 'tu_clave_secreta');
|
||||||
|
|
||||||
|
console.log('fecha de caducidad:', fecha_fin);
|
||||||
|
// Convert fecha_fin to seconds from now for expiresIn
|
||||||
|
const expiresInSeconds = Math.floor(
|
||||||
|
(fecha_fin.getTime() - Date.now()) / 1000,
|
||||||
|
);
|
||||||
|
console.log('expiresInSeconds:', expiresInSeconds);
|
||||||
|
console.log(
|
||||||
|
'Caducidad token: ',
|
||||||
|
Math.floor((fecha_fin.getTime() - Date.now()) / 1000),
|
||||||
|
);
|
||||||
|
|
||||||
return this.jwtService.sign(payload, {
|
return this.jwtService.sign(payload, {
|
||||||
secret,
|
secret,
|
||||||
expiresIn: '30d', // El QR puede ser válido por 30 días
|
expiresIn: expiresInSeconds > 0 ? expiresInSeconds : 3600, // El QR puede ser válido por 30 días
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,34 +75,114 @@ export class QrTokenService {
|
|||||||
* @param token Token JWT a validar
|
* @param token Token JWT a validar
|
||||||
* @returns Datos decodificados del token o null si es inválido
|
* @returns Datos decodificados del token o null si es inválido
|
||||||
*/
|
*/
|
||||||
validateQrToken(token: string): {
|
async validateQrToken(token: string): Promise<{
|
||||||
id_participante: number;
|
id_participante: number;
|
||||||
id_evento: number;
|
id_evento: number;
|
||||||
id_cuestionario: number;
|
id_cuestionario: number;
|
||||||
type: string;
|
type: string;
|
||||||
iat: number;
|
iat: number;
|
||||||
} | null {
|
exp?: number;
|
||||||
|
contador?: number | null;
|
||||||
|
} | null> {
|
||||||
try {
|
try {
|
||||||
const secret =
|
const secret =
|
||||||
this.configService.get<string>('QR_JWT_SECRET') ||
|
this.configService.get<string>('QR_JWT_SECRET') ||
|
||||||
this.configService.get<string>('JWT_SECRET', 'tu_clave_secreta');
|
this.configService.get<string>('JWT_SECRET', 'tu_clave_secreta');
|
||||||
|
|
||||||
|
console.log(secret);
|
||||||
|
|
||||||
const decoded = this.jwtService.verify(token, { secret });
|
const decoded = this.jwtService.verify(token, { secret });
|
||||||
|
|
||||||
// Verificar que sea un token de tipo QR de asistencia
|
console.log('este es el token', decoded);
|
||||||
|
|
||||||
|
const evento = await this.eventoREpo.findOne({
|
||||||
|
where: { id_evento: decoded.id_evento },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!evento) {
|
||||||
|
throw new UnauthorizedException('Evento no encontrado');
|
||||||
|
}
|
||||||
|
const fechaFinEvento = new Date(evento.fecha_fin);
|
||||||
|
const now = new Date();
|
||||||
|
if (fechaFinEvento.getTime() < now.getTime()) {
|
||||||
|
throw new Error('El evento ha finalizado');
|
||||||
|
}
|
||||||
|
|
||||||
if (decoded.type !== 'qr_asistencia') {
|
if (decoded.type !== 'qr_asistencia') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
const result: {
|
||||||
|
id_participante: number;
|
||||||
|
id_evento: number;
|
||||||
|
id_cuestionario: number;
|
||||||
|
type: string;
|
||||||
|
iat: number;
|
||||||
|
contador?: number | null;
|
||||||
|
exp: number;
|
||||||
|
} = {
|
||||||
id_participante: decoded.id_participante,
|
id_participante: decoded.id_participante,
|
||||||
id_evento: decoded.id_evento,
|
id_evento: decoded.id_evento,
|
||||||
id_cuestionario: decoded.id_cuestionario,
|
id_cuestionario: decoded.id_cuestionario,
|
||||||
type: decoded.type,
|
type: decoded.type,
|
||||||
iat: decoded.iat,
|
iat: decoded.iat,
|
||||||
|
exp: decoded.exp,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.log({ result });
|
||||||
|
|
||||||
|
const tieneListaRestringida =
|
||||||
|
(await this.eventoUsuarioRepo.count({
|
||||||
|
where: { id_evento: decoded.id_evento },
|
||||||
|
})) > 0;
|
||||||
|
|
||||||
|
if (tieneListaRestringida) {
|
||||||
|
const respuesta = await this.respuestaAbiertaRepo.findOne({
|
||||||
|
where: {
|
||||||
|
cuestionarioRespondido: {
|
||||||
|
participante: { id_participante: decoded.id_participante },
|
||||||
|
cuestionario: { id_cuestionario: decoded.id_cuestionario },
|
||||||
|
},
|
||||||
|
pregunta: {
|
||||||
|
validacion: TiposValidacion.CUENTA_ALUMNO,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
relations: ['pregunta', 'cuestionarioRespondido'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const respuestaRfc = respuesta
|
||||||
|
? null
|
||||||
|
: await this.respuestaAbiertaRepo.findOne({
|
||||||
|
where: {
|
||||||
|
cuestionarioRespondido: {
|
||||||
|
participante: { id_participante: decoded.id_participante },
|
||||||
|
cuestionario: { id_cuestionario: decoded.id_cuestionario },
|
||||||
|
},
|
||||||
|
pregunta: {
|
||||||
|
validacion: TiposValidacion.RFC,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
relations: ['pregunta', 'cuestionarioRespondido'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const identificadorRespuesta = respuesta ?? respuestaRfc;
|
||||||
|
|
||||||
|
if (identificadorRespuesta) {
|
||||||
|
const identificador = identificadorRespuesta.respuesta
|
||||||
|
.trim()
|
||||||
|
.toUpperCase();
|
||||||
|
|
||||||
|
const eventoUsuario = await this.eventoUsuarioRepo.findOne({
|
||||||
|
where: { id_evento: decoded.id_evento, identificador },
|
||||||
|
});
|
||||||
|
|
||||||
|
result.contador = eventoUsuario?.contador ?? null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error validando token QR:', error.message);
|
console.error('Error validando token QR:', error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,20 +69,23 @@ export class QrController {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('generate-token/:idParticipante/:idEvento/:idCuestionario')
|
@Get('generate-token/:idParticipante/:idEvento/:idCuestionario/:fechaFin')
|
||||||
@ApiOperation({ summary: 'Genera un token JWT para QR de asistencia' })
|
@ApiOperation({ summary: 'Genera un token JWT para QR de asistencia' })
|
||||||
@ApiParam({ name: 'idParticipante', description: 'ID del participante' })
|
@ApiParam({ name: 'idParticipante', description: 'ID del participante' })
|
||||||
@ApiParam({ name: 'idEvento', description: 'ID del evento' })
|
@ApiParam({ name: 'idEvento', description: 'ID del evento' })
|
||||||
@ApiParam({ name: 'idCuestionario', description: 'ID del cuestionario' })
|
@ApiParam({ name: 'idCuestionario', description: 'ID del cuestionario' })
|
||||||
|
@ApiParam({ name: 'fechaFin', description: 'Fecha de fin de validez del token (ISO 8601)' })
|
||||||
async generateQrToken(
|
async generateQrToken(
|
||||||
@Param('idParticipante', ParseIntPipe) idParticipante: number,
|
@Param('idParticipante', ParseIntPipe) idParticipante: number,
|
||||||
@Param('idEvento', ParseIntPipe) idEvento: number,
|
@Param('idEvento', ParseIntPipe) idEvento: number,
|
||||||
@Param('idCuestionario', ParseIntPipe) idCuestionario: number,
|
@Param('idCuestionario', ParseIntPipe) idCuestionario: number,
|
||||||
|
@Param('fechaFin') fechaFin: Date,
|
||||||
) {
|
) {
|
||||||
const token = this.qrTokenService.generateQrToken(
|
const token = await this.qrTokenService.generateQrToken(
|
||||||
idParticipante,
|
idParticipante,
|
||||||
idEvento,
|
idEvento,
|
||||||
idCuestionario,
|
idCuestionario,
|
||||||
|
fechaFin
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
+6
-1
@@ -6,9 +6,14 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
|||||||
import { Qr } from './qr.entity';
|
import { Qr } from './qr.entity';
|
||||||
import { AuthModule } from '../auth/auth.module';
|
import { AuthModule } from '../auth/auth.module';
|
||||||
import { ConfigModule } from '@nestjs/config';
|
import { ConfigModule } from '@nestjs/config';
|
||||||
|
import { Asistencia } from 'src/asistencia/entities/asistencia.entity';
|
||||||
|
import { Cuestionario } from 'src/cuestionario/entities/cuestionario.entity';
|
||||||
|
import { Evento } from 'src/evento/entities/evento.entity';
|
||||||
|
import { EventoUsuario } from 'src/evento_usuario/entities/evento_usuario.entity';
|
||||||
|
import { RespuestaParticipanteAbierta } from 'src/respuesta_participante_abierta/entities/respuesta_participante_abierta.entity';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([Qr]), AuthModule, ConfigModule],
|
imports: [TypeOrmModule.forFeature([Qr, Asistencia, Cuestionario, Evento, EventoUsuario, RespuestaParticipanteAbierta]), AuthModule, ConfigModule],
|
||||||
controllers: [QrController],
|
controllers: [QrController],
|
||||||
providers: [QrService, QrTokenService],
|
providers: [QrService, QrTokenService],
|
||||||
exports: [QrTokenService],
|
exports: [QrTokenService],
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
UseInterceptors,
|
UseInterceptors,
|
||||||
UploadedFile,
|
UploadedFile,
|
||||||
NotFoundException,
|
NotFoundException,
|
||||||
|
Query,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { TrabajadoresService } from './trabajadores.service';
|
import { TrabajadoresService } from './trabajadores.service';
|
||||||
import { CreateTrabajadoreDto } from './dto/create-trabajadore.dto';
|
import { CreateTrabajadoreDto } from './dto/create-trabajadore.dto';
|
||||||
@@ -46,8 +47,13 @@ export class TrabajadoresController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get(':rfc')
|
@Get(':rfc')
|
||||||
findByRfc(@Param('rfc') rfc: string) {
|
async findByRfc(
|
||||||
const trabajador = this.trabajadoresService.findByRfc(rfc);
|
@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) {
|
if (!trabajador) {
|
||||||
throw new NotFoundException(`No se encontró trabajador con RFC ${rfc}`);
|
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 { TrabajadoresController } from './trabajadores.controller';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { RegistroTrabajador } from './entities/trabajadore.entity';
|
import { RegistroTrabajador } from './entities/trabajadore.entity';
|
||||||
|
import { EventoUsuarioModule } from '../evento_usuario/evento_usuario.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
TypeOrmModule.forFeature([RegistroTrabajador], 'alumnosConnection'),
|
TypeOrmModule.forFeature([RegistroTrabajador], 'alumnosConnection'),
|
||||||
|
EventoUsuarioModule,
|
||||||
],
|
],
|
||||||
controllers: [TrabajadoresController],
|
controllers: [TrabajadoresController],
|
||||||
providers: [TrabajadoresService],
|
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 { CreateTrabajadoreDto } from './dto/create-trabajadore.dto';
|
||||||
import { UpdateTrabajadoreDto } from './dto/update-trabajadore.dto';
|
import { UpdateTrabajadoreDto } from './dto/update-trabajadore.dto';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
@@ -7,12 +7,14 @@ import { Repository } from 'typeorm';
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as XLSX from 'xlsx';
|
import * as XLSX from 'xlsx';
|
||||||
import { UsuarioData } from 'src/alumnos/alumnos.service';
|
import { UsuarioData } from 'src/alumnos/alumnos.service';
|
||||||
|
import { EventoUsuarioService } from '../evento_usuario/evento_usuario.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TrabajadoresService {
|
export class TrabajadoresService {
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(RegistroTrabajador, 'alumnosConnection')
|
@InjectRepository(RegistroTrabajador, 'alumnosConnection')
|
||||||
private readonly trabajadorRepo: Repository<RegistroTrabajador>,
|
private readonly trabajadorRepo: Repository<RegistroTrabajador>,
|
||||||
|
private readonly eventoUsuarioService: EventoUsuarioService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async procesarArchivo(
|
async procesarArchivo(
|
||||||
@@ -69,21 +71,41 @@ export class TrabajadoresService {
|
|||||||
return { insertados, omitidos };
|
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({
|
const trabajador = await this.trabajadorRepo.findOne({
|
||||||
where: { rfc: rfc.trim().toUpperCase() },
|
where: { rfc: rfc.trim().toUpperCase() },
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
if (!trabajador) return null;
|
||||||
cuenta: trabajador?.num_trabajador?.toString() || null,
|
|
||||||
nombre: trabajador?.nombre || null,
|
if (id_evento !== undefined) {
|
||||||
apellidos: trabajador
|
const tieneLista =
|
||||||
? `${trabajador.apellidos}`.trim()
|
await this.eventoUsuarioService.eventoTieneListaRestringida(id_evento);
|
||||||
: null,
|
|
||||||
carrera: trabajador?.carrera || null,
|
if (tieneLista) {
|
||||||
genero: trabajador?.genero || null,
|
const enLista = await this.eventoUsuarioService.identificadorEnLista(
|
||||||
rfc: trabajador?.rfc || null, // Incluye RFC en el retorno
|
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) {
|
create(createTrabajadoreDto: CreateTrabajadoreDto) {
|
||||||
|
|||||||
Reference in New Issue
Block a user