develop #36

Merged
jalvarado merged 7 commits from develop into master 2025-09-10 16:18:14 +00:00
42 changed files with 1317 additions and 129 deletions
+3
View File
@@ -0,0 +1,3 @@
{
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
+24 -35
View File
@@ -1,35 +1,24 @@
// @ts-check
import eslint from '@eslint/js';
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
import globals from 'globals';
import tseslint from 'typescript-eslint';
export default tseslint.config(
{
ignores: ['eslint.config.mjs'],
},
eslint.configs.recommended,
...tseslint.configs.recommendedTypeChecked,
eslintPluginPrettierRecommended,
{
languageOptions: {
globals: {
...globals.node,
...globals.jest,
},
ecmaVersion: 5,
sourceType: 'module',
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
},
{
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-floating-promises': 'warn',
'@typescript-eslint/no-unsafe-argument': 'warn'
},
},
);
module.exports = {
// parser: '@typescript-eslint/parser',
// parserOptions: {
// project: 'tsconfig.json',
// sourceType: 'module',
// },
// plugins: ['@typescript-eslint/eslint-plugin'],
// extends: [
// 'plugin:@typescript-eslint/recommended',
// 'plugin:prettier/recommended',
// ],
// root: true,
// env: {
// node: true,
// jest: true,
// },
// ignorePatterns: ['.eslintrc.js'],
// rules: {
// '@typescript-eslint/interface-name-prefix': 'off',
// '@typescript-eslint/explicit-function-return-type': 'off',
// '@typescript-eslint/explicit-module-boundary-types': 'off',
// '@typescript-eslint/no-explicit-any': 'off',
// },
};
+14
View File
@@ -0,0 +1,14 @@
INSERT INTO tipo_evento (tipo_evento) VALUES
('Académico'),
('Taller / Capacitación'),
('Conferencia / Charla'),
('Panel / Mesa redonda'),
('Feria / Expo'),
('Networking / Vinculación'),
('Entrevista / Sesión 1:1'),
('Cultural / Artístico'),
('Deportivo / Recreativo'),
('Institucional / Ceremonial'),
('Social / Comunitario'),
('Aniversario'),
('Otro');
+23 -9
View File
@@ -15,9 +15,12 @@ import { CreateAdministradorDto } from './dto/create-administrador.dto';
import { UpdateAdministradorDto } from './dto/update.administrador.dto';
import { LoginAdministradorDto } from './dto/login-administrador.dto';
import { ChangePasswordDto } from './dto/change-password.dto';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { ApiBearerAuth } from '@nestjs/swagger';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { AdministradorApiDocumentation } from './administrador.documentation';
import { AdministradorApiDocumentation } from './docs/administrador.documentation';
import { JwtValidationGuard } from 'src/auth/guards/jwt-validation.guard';
import { Roles } from 'src/auth/decorators/roles.decorator';
import { RolesGuard } from 'src/auth/guards/roles.guard';
@AdministradorApiDocumentation.ApiController
@ApiBearerAuth()
@@ -26,20 +29,25 @@ export class AdministradorController {
constructor(private administradorService: AdministradorService) {}
@Get()
@UseGuards(JwtAuthGuard)
@UseGuards(RolesGuard)
@UseGuards(JwtValidationGuard)
@Roles('administrador')
@AdministradorApiDocumentation.ApiGetAll
getAdministradores(): Promise<Administrador[]> {
return this.administradorService.getAdministradores();
}
@Get(':id')
@UseGuards(JwtAuthGuard)
@UseGuards(RolesGuard)
@UseGuards(JwtValidationGuard)
@Roles('administrador')
@AdministradorApiDocumentation.ApiGetOne
getAdministrador(@Param('id', ParseIntPipe) id: number) {
return this.administradorService.getAdministrador(id);
return this.administradorService.getAdminByIdOrFail(id);
}
@Post()
@Roles('administrador')
@AdministradorApiDocumentation.ApiCreate
createAdministrador(@Body() newAdministrador: CreateAdministradorDto) {
return this.administradorService.createAdministrador(newAdministrador);
@@ -49,13 +57,15 @@ export class AdministradorController {
@AdministradorApiDocumentation.ApiLogin
login(@Body() loginData: LoginAdministradorDto) {
return this.administradorService.login(
loginData.correo,
loginData.nombre_usuario,
loginData.password,
);
}
@Patch(':id/change-password')
@UseGuards(JwtAuthGuard)
@UseGuards(RolesGuard)
@UseGuards(JwtValidationGuard)
@Roles('administrador')
@AdministradorApiDocumentation.ApiChangePassword
changePassword(
@Param('id', ParseIntPipe) id: number,
@@ -69,14 +79,18 @@ export class AdministradorController {
}
@Delete(':id')
@UseGuards(JwtAuthGuard)
@UseGuards(RolesGuard)
@UseGuards(JwtValidationGuard)
@Roles('administrador')
@AdministradorApiDocumentation.ApiRemove
deleteAdministrador(@Param('id', ParseIntPipe) id: number) {
return this.administradorService.deleteAdministrador(id);
}
@Patch(':id')
@UseGuards(JwtAuthGuard)
@UseGuards(RolesGuard)
@UseGuards(JwtValidationGuard)
@Roles('administrador')
@AdministradorApiDocumentation.ApiUpdate
updateAdministrador(
@Param('id', ParseIntPipe) id: number,
+28 -22
View File
@@ -35,9 +35,10 @@ export class AdministradorService {
// Crear y guardar el nuevo administrador
const newAdministrador = this.administradorRepository.create({
nombre_usuario: administrador.nombre_usuario,
correo: administrador.correo,
password: hashedPassword,
id_tipo_user: administrador.id_tipo_user,
tipoUser: { id_tipo_user: administrador.id_tipo_user }, // Asignar el tipo de usuario
});
const savedAdministrador =
@@ -48,10 +49,10 @@ export class AdministradorService {
return result;
}
async login(correo: string, password: string) {
// Buscar administrador por correo
async login(nombre_usuario: string, password: string) {
// Buscar administrador por nombre de usuario
const administrador = await this.administradorRepository.findOne({
where: { correo },
where: { nombre_usuario },
relations: ['tipoUser'],
});
@@ -76,24 +77,24 @@ export class AdministradorService {
// Generar token JWT
const payload = {
sub: administrador.id_admnistrador,
sub: administrador.id_administrador,
correo: administrador.correo,
id_tipo_user: administrador.id_tipo_user,
};
return {
token: this.jwtService.sign(payload),
tipo_usuario: administrador.tipoUser.tipo,
};
}
async changePassword(
id_admnistrador: number,
id_administrador: number,
currentPassword: string,
newPassword: string,
) {
// Buscar administrador
const administrador = await this.administradorRepository.findOne({
where: { id_admnistrador },
where: { id_administrador },
});
if (!administrador) {
@@ -131,23 +132,17 @@ export class AdministradorService {
return this.administradorRepository.find({
relations: ['tipoUser'],
select: {
id_admnistrador: true,
nombre_usuario: true,
id_administrador: true,
correo: true,
id_tipo_user: true,
},
});
}
async getAdministrador(id_admnistrador: number) {
async getAdminByIdOrFail(id_administrador: number) {
const administradorFound = await this.administradorRepository.findOne({
where: {
id_admnistrador,
},
relations: ['tipoUser'],
select: {
id_admnistrador: true,
correo: true,
id_tipo_user: true,
id_administrador,
},
});
@@ -161,9 +156,20 @@ export class AdministradorService {
return administradorFound;
}
async deleteAdministrador(id_admnistrador: number) {
async getAdminById(id_administrador: number) {
const administradorFound = await this.administradorRepository.findOne({
where: {
id_administrador,
},
relations: ['tipoUser'],
});
return administradorFound;
}
async deleteAdministrador(id_administrador: number) {
const result = await this.administradorRepository.delete({
id_admnistrador,
id_administrador,
});
if (result.affected === 0) {
@@ -177,12 +183,12 @@ export class AdministradorService {
}
async updateAdministrador(
id_admnistrador: number,
id_administrador: number,
administrador: UpdateAdministradorDto,
) {
const administradorFound = await this.administradorRepository.findOne({
where: {
id_admnistrador,
id_administrador,
},
});
@@ -10,9 +10,7 @@ import { applyDecorators } from '@nestjs/common';
export class AdministradorApiDocumentation {
// Decorador para toda la clase del controlador
static ApiController = applyDecorators(
ApiTags('Administrador'),
);
static ApiController = applyDecorators(ApiTags('Administrador'));
// Documentación para crear un administrador
static ApiCreate = applyDecorators(
ApiOperation({
@@ -26,6 +24,11 @@ export class AdministradorApiDocumentation {
type: 'object',
required: ['correo', 'password', 'id_tipo_user'],
properties: {
nombre_usuario: {
type: 'string',
example: 'mike',
description: 'Nombre de usuario del administrador',
},
correo: {
type: 'string',
format: 'email',
@@ -2,18 +2,31 @@ import { IsEmail, IsNotEmpty, IsNumber, MinLength } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class CreateAdministradorDto {
@ApiProperty({ example: 'admin@example.com', description: 'Correo del administrador' })
@IsEmail({}, { message: 'El correo no es válido' })
@IsNotEmpty({ message: 'El correo es requerido' })
correo: string;
@ApiProperty({
example: 'admin@example.com',
description: 'Correo del administrador',
})
@IsEmail({}, { message: 'El correo no es válido' })
@IsNotEmpty({ message: 'El correo es requerido' })
correo: string;
@ApiProperty({ example: 'Password123', description: 'Contraseña del administrador' })
@MinLength(6, { message: 'La contraseña debe tener al menos 6 caracteres' })
@IsNotEmpty({ message: 'La contraseña es requerida' })
password: string;
@ApiProperty({
example: 'mike',
description: 'Nombre de usuario del administrador',
})
@IsNotEmpty({ message: 'El nombre de usuario es requerido' })
nombre_usuario: string;
@ApiProperty({ example: 1, description: 'ID del tipo de usuario' })
@IsNumber({}, { message: 'El tipo de usuario debe ser un número' })
@IsNotEmpty({ message: 'El tipo de usuario es requerido' })
id_tipo_user: number;
}
@ApiProperty({
example: 'Password123',
description: 'Contraseña del administrador',
})
@MinLength(6, { message: 'La contraseña debe tener al menos 6 caracteres' })
@IsNotEmpty({ message: 'La contraseña es requerida' })
password: string;
@ApiProperty({ example: 1, description: 'ID del tipo de usuario' })
@IsNumber({}, { message: 'El tipo de usuario debe ser un número' })
@IsNotEmpty({ message: 'El tipo de usuario es requerido' })
id_tipo_user: number;
}
@@ -1,13 +1,18 @@
import { IsEmail, IsNotEmpty, MinLength } from 'class-validator';
import { IsEmail, IsNotEmpty } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class LoginAdministradorDto {
@ApiProperty({ example: 'admin@example.com', description: 'Correo del administrador' })
@IsEmail({}, { message: 'El correo no es válido' })
@IsNotEmpty({ message: 'El correo es requerido' })
correo: string;
@ApiProperty({
example: 'mike',
description: 'Nombre de usuario del administrador',
})
@IsNotEmpty({ message: 'El nombre de usuario es requerido' })
nombre_usuario: string;
@ApiProperty({ example: 'Password123', description: 'Contraseña del administrador' })
@IsNotEmpty({ message: 'La contraseña es requerida' })
password: string;
}
@ApiProperty({
example: 'Password123',
description: 'Contraseña del administrador',
})
@IsNotEmpty({ message: 'La contraseña es requerida' })
password: string;
}
@@ -1,10 +1,19 @@
import { TipoUser } from 'src/tipo_user/entities/tipo_user.entity';
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
import {
Column,
Entity,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
} from 'typeorm';
@Entity('administrador')
export class Administrador {
@PrimaryGeneratedColumn()
id_admnistrador: number;
id_administrador: number;
@Column({ unique: true })
nombre_usuario: string;
@Column({ unique: true })
correo: string;
@@ -12,9 +21,10 @@ export class Administrador {
@Column()
password: string;
@Column()
@Column({ type: 'int' })
id_tipo_user: number;
@ManyToOne(() => TipoUser, (tipoUser) => tipoUser.administrador)
tipoUser: TipoUser[];
@JoinColumn({ name: 'id_tipo_user' })
tipoUser: TipoUser;
}
+2
View File
@@ -28,6 +28,7 @@ import { ParticipanteEventoModule } from './participante_evento/participante_eve
import { ValidacionesModule } from './validaciones/validaciones.module';
import { AlumnosModule } from './alumnos/alumnos.module';
import { TrabajadoresModule } from './trabajadores/trabajadores.module';
import { TipoEventoModule } from './tipo_evento/tipo_evento.module';
@Module({
imports: [
@@ -62,6 +63,7 @@ import { TrabajadoresModule } from './trabajadores/trabajadores.module';
SeccionModule,
SeccionPreguntaModule,
TipoCuestionarioModule,
TipoEventoModule,
TipoPreguntaModule,
TipoUserModule,
ValidacionesModule,
+5
View File
@@ -0,0 +1,5 @@
import { SetMetadata } from '@nestjs/common';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: ('administrador' | 'staff')[]) =>
SetMetadata(ROLES_KEY, roles);
+52
View File
@@ -0,0 +1,52 @@
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { AdministradorService } from 'src/administrador/administrador.service';
@Injectable()
export class JwtValidationGuard implements CanActivate {
constructor(
private jwtService: JwtService,
private administradorService: AdministradorService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const authHeader = request.headers.authorization;
if (!authHeader) {
throw new UnauthorizedException('Token no proporcionado');
}
const token = authHeader.split(' ')[1];
try {
const decodedToken = this.jwtService.verify(token, {
secret: process.env.JWT_SECRET,
});
const administrador = await this.administradorService.getAdminById(
decodedToken.sub,
);
if (!administrador) {
throw new UnauthorizedException('Usuario no encontrado');
}
request.user = administrador;
} catch (error) {
if (error.name === 'TokenExpiredError') {
throw new UnauthorizedException(
`La sesión ha expirado, inicia sesión nuevamente.`,
);
}
throw new UnauthorizedException('Token inválido');
}
return true;
}
}
+46
View File
@@ -0,0 +1,46 @@
import {
Injectable,
CanActivate,
ExecutionContext,
ForbiddenException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY } from '../decorators/roles.decorator';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.get<string[]>(
ROLES_KEY,
context.getHandler(),
);
if (!requiredRoles) return true; // Si no hay roles definidos, permitir acceso.
const request = context.switchToHttp().getRequest();
const user = request.user; // Usuario ya validado en JwtValidationGuard
if (!user) {
throw new ForbiddenException('Usuario no autenticado');
}
// Obtener el tipo de usuario
const tipo_usuario = user.tipoUser.tipo;
if (!tipo_usuario) {
throw new ForbiddenException('No se pudo determinar el tipo de usuario');
}
// Validar si el usuario tiene uno de los roles requeridos
const hasRole = requiredRoles.includes(tipo_usuario);
if (!hasRole) {
throw new ForbiddenException(
'No tienes permisos para acceder a este recurso',
);
}
return true;
}
}
+6 -7
View File
@@ -19,15 +19,14 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
}
async validate(payload: any) {
const { sub: id_admnistrador } = payload;
const { sub: id_administrador } = payload;
const administrador = await this.administradorRepository.findOne({
where: { id_admnistrador },
where: { id_administrador },
select: {
id_admnistrador: true,
id_administrador: true,
correo: true,
id_tipo_user: true
}
},
});
if (!administrador) {
@@ -36,4 +35,4 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
return administrador;
}
}
}
+2
View File
@@ -67,6 +67,7 @@ export class CuestionarioService {
cuestionario.fecha_fin = new Date(createCuestionarioDto.fecha_fin);
cuestionario.id_tipo_cuestionario =
createCuestionarioDto.id_tipo_cuestionario;
cuestionario.id_tipo_evento = createCuestionarioDto.id_tipo_evento;
cuestionario.contador_secciones =
createCuestionarioDto.secciones?.length || 0;
cuestionario.editable = true;
@@ -227,6 +228,7 @@ export class CuestionarioService {
fecha_inicio: new Date(cuestionario.fecha_inicio),
fecha_fin: new Date(cuestionario.fecha_fin),
id_tipo_cuestionario: cuestionario.id_tipo_cuestionario,
id_tipo_evento: cuestionario.id_tipo_evento,
contador_secciones: cuestionario.secciones?.length || 0,
editable: true,
id_evento: eventoExistente.id_evento, // Asociar el evento creado
@@ -63,6 +63,14 @@ export class CreateCuestionarioDto {
@IsNumber()
id_tipo_cuestionario: number;
@ApiProperty({
description: 'ID del tipo de evento',
example: 1,
})
@IsOptional()
@IsNumber()
id_tipo_evento: number;
@ApiProperty({
description: 'Cupo máximo de participantes',
example: 100,
@@ -11,6 +11,7 @@ import { Evento } from 'src/evento/entities/evento.entity';
import { ParticipanteEvento } from 'src/participante_evento/entities/participante_evento.entity';
import { TipoCuestionario } from 'src/tipo_cuestionario/entities/tipo_cuestionario.entity';
import { CuestionarioRespondido } from 'src/cuestionario_respondido/entities/cuestionario_respondido.entity';
import { TipoEvento } from 'src/tipo_evento/entities/tipo_evento.entity';
@Entity('cuestionario')
export class Cuestionario {
@@ -44,14 +45,13 @@ export class Cuestionario {
@Column({ type: 'int' })
id_tipo_cuestionario: number;
@Column({ type: 'int', nullable: true })
id_tipo_evento: number;
@Column({ type: 'int' })
id_evento: number;
// Relaciones
@ManyToOne(() => Cuestionario, { nullable: true })
@JoinColumn({ name: 'id_cuestionario_original' })
cuestionarioOriginal?: Cuestionario;
@ManyToOne(() => Evento, (evento) => evento.cuestionarios, { nullable: true })
@JoinColumn({ name: 'id_evento' })
evento?: Evento;
@@ -62,6 +62,12 @@ export class Cuestionario {
@JoinColumn({ name: 'id_tipo_cuestionario' })
tipoCuestionario: TipoCuestionario;
@ManyToOne(() => TipoEvento, (tipo) => tipo.cuestionarios, {
eager: true,
})
@JoinColumn({ name: 'id_tipo_evento' })
tipoEvento: TipoEvento;
@OneToMany(
() => CuestionarioSeccion,
(cuestionarioSeccion) => cuestionarioSeccion.cuestionario,
@@ -5,6 +5,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { CuestionarioRespondido } from './entities/cuestionario_respondido.entity';
import { CuestionarioModule } from '../cuestionario/cuestionario.module';
import { ParticipanteModule } from '../participante/participante.module';
import { QrModule } from '../qr/qr.module';
import { Participante } from '../participante/entities/participante.entity';
import { Cuestionario } from '../cuestionario/entities/cuestionario.entity';
import { Pregunta } from '../pregunta/entities/pregunta.entity';
@@ -29,6 +30,7 @@ import { ValidacionesModule } from '../validaciones/validaciones.module';
CuestionarioModule,
ParticipanteModule,
ValidacionesModule,
QrModule,
],
controllers: [CuestionarioRespondidoController],
providers: [CuestionarioRespondidoService],
@@ -17,6 +17,7 @@ import { RespuestaParticipanteCerrada } from '../respuesta_participante_cerrada/
import { PreguntaOpcion } from '../pregunta_opcion/entities/pregunta_opcion.entity';
import { Opcion } from '../opcion/entities/opcion.entity';
import { ValidadorRespuestasService } from '../validaciones/validador-respuestas.service';
import { QrTokenService } from '../qr/qr-token.service';
import axios from 'axios';
import * as QRCode from 'qrcode';
import { CuestionarioService } from 'src/cuestionario/cuestionario.service';
@@ -39,6 +40,7 @@ export class CuestionarioRespondidoService {
private dataSource: DataSource,
private validadorRespuestasService: ValidadorRespuestasService,
private cuestionarioService: CuestionarioService,
private qrTokenService: QrTokenService,
) {}
create(createCuestionarioRespondidoDto: CreateCuestionarioRespondidoDto) {
@@ -99,11 +101,81 @@ export class CuestionarioRespondidoService {
relations: ['cuestionario', 'participante'],
},
);
if (cuestionarioRespondidoExistente) {
await queryRunner.rollbackTransaction();
let qrBuffer: Buffer | undefined;
let qrToken: string | undefined;
// Aunque ya haya respondido, enviamos el correo nuevamente
if (cuestionario && cuestionario.evento) {
try {
// Generar token para el QR
qrToken = this.qrTokenService.generateQrToken(
participante.id_participante,
cuestionario.evento.id_evento,
cuestionario.id_cuestionario,
);
// Generar código QR con el token
qrBuffer = await QRCode.toBuffer(qrToken);
// Enviar correo con el QR
const urlApiCorreos = process.env.url_api_correos;
if (urlApiCorreos) {
await axios.post(
`${urlApiCorreos}/mail/send`,
{
to: participante.correo,
subject: `Evento: ${cuestionario.evento?.nombre_evento}`,
fecha_recibido: new Date().toISOString(),
html: generarHtmlCorreoAsistencia({
nombreForm: cuestionario.nombre_form,
nombreEvento: cuestionario.evento?.nombre_evento,
correo: participante.correo,
fechaRegistro: new Date().toLocaleString(),
fechaInicioEvento: cuestionario.evento?.fecha_inicio
? new Date(
cuestionario.evento.fecha_inicio,
).toLocaleString()
: undefined,
fechaFinEvento: cuestionario.evento?.fecha_fin
? new Date(cuestionario.evento.fecha_fin).toLocaleString()
: undefined,
}),
adjuntos: [
{
filename: 'qr.png',
content: qrBuffer.toString('base64'),
encoding: 'base64',
cid: 'qrCode',
},
],
},
{
headers: {
token: process.env.SYSTEM_TOKEN_API_CORREOS,
},
},
);
console.log(
`Correo reenviado exitosamente a ${participante.correo}`,
);
}
} catch (error) {
console.error('Error al reenviar correo:', error);
}
}
return {
success: false,
message: `El participante con correo ${submitDto.correo} ya ha respondido el cuestionario ${cuestionario.nombre_form}.`,
success: true,
registrado: true,
message: `El participante con correo ${submitDto.correo} ya había respondido el cuestionario ${cuestionario.nombre_form}. Se ha reenviado el correo con el código QR.`,
qr_token: qrToken,
qr_buffer: qrBuffer ? qrBuffer.toString('base64') : undefined,
};
}
@@ -215,6 +287,9 @@ export class CuestionarioRespondidoService {
id_cuestionario: number;
} | null = null;
let qrBuffer: Buffer | undefined;
let qrToken: string | undefined;
// Capturamos los datos para el QR
if (cuestionario.evento && cuestionario.evento.id_evento) {
datosQR = {
@@ -260,9 +335,14 @@ export class CuestionarioRespondidoService {
);
}
// Generar código QR
const qrJsonData = JSON.stringify(datosQR);
const qrBuffer = await QRCode.toBuffer(qrJsonData);
// Generar token para el QR
qrToken = this.qrTokenService.generateQrToken(
participante.id_participante,
cuestionario.evento.id_evento,
cuestionario.id_cuestionario,
);
qrBuffer = await QRCode.toBuffer(qrToken);
// Enviar correo con el QR
const urlApiCorreos = process.env.url_api_correos;
@@ -317,9 +397,12 @@ export class CuestionarioRespondidoService {
// Preparar respuesta
const response: any = {
success: true,
registrado: false,
message: 'Respuestas guardadas correctamente',
cuestionarioRespondidoId:
savedCuestionarioRespondido.idCuestionarioRespondido,
qr_token: qrToken,
qr_buffer: qrBuffer ? qrBuffer.toString('base64') : undefined,
};
// Incluir IDs para generar QR si hay evento asociado
+2
View File
@@ -20,6 +20,7 @@ import { SeccionPregunta } from 'src/seccion_pregunta/entities/seccion_pregunta.
import { TipoCuestionario } from 'src/tipo_cuestionario/entities/tipo_cuestionario.entity';
import { TipoPregunta } from 'src/tipo_pregunta/entities/tipo_pregunta.entity';
import { TipoUser } from 'src/tipo_user/entities/tipo_user.entity';
import { TipoEvento } from 'src/tipo_evento/entities/tipo_evento.entity';
export const createMainDbConfig = async (
configService: ConfigService,
@@ -47,6 +48,7 @@ export const createMainDbConfig = async (
Seccion,
SeccionPregunta,
TipoCuestionario,
TipoEvento,
TipoPregunta,
TipoUser,
],
+307
View File
@@ -0,0 +1,307 @@
export function generarHtmlCorreoAsistenciaTicket({
nombreForm,
nombreEvento,
correo,
fechaRegistro,
fechaInicioEvento,
fechaFinEvento,
}: {
nombreForm: string;
nombreEvento: string;
correo: string;
fechaRegistro: string;
fechaInicioEvento?: string;
fechaFinEvento?: string;
}) {
const horarioEvento =
fechaInicioEvento && fechaFinEvento
? `${fechaInicioEvento} al ${fechaFinEvento}`
: 'Por confirmar';
return `
<div
style="
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
max-width: 650px;
margin: 0 auto;
padding: 20px;
border-radius: 16px;
"
>
<!-- Header del correo -->
<div style="text-align: center">
<h1>¡Registro Confirmado!</h1>
</div>
<!-- Ticket Container -->
<div
style="
background: white;
border-radius: 12px;
overflow: hidden;
background: #fefefe;
position: relative;
"
>
<!-- Ticket Header -->
<div
style="
background: linear-gradient(90deg, #003d79 0%, #0056b3 100%);
color: white;
padding: 20px;
position: relative;
"
>
<h2 style="margin: 0 0 8px 0; font-size: 24px; font-weight: bold">
🎓 ${nombreEvento || 'Evento Especial'}
</h2>
<p style="margin: 0; opacity: 0.9; font-size: 14px">
TICKET DE ENTRADA • ENTRADA GRATUITA
</p>
</div>
<!-- Ticket Body -->
<div style="padding: 30px 20px">
<div style="display: flex; gap: 30px; align-items: center">
<!-- QR Code Section -->
<div
style="
flex: 1;
display: flex;
justify-content: center;
align-items: center;
"
>
<div
style="
background: #f8f9fa;
border: 2px dashed #dee2e6;
border-radius: 8px;
padding: 20px;
display: inline-block;
text-align: center;
"
>
<img
src="cid:qrCode"
alt="Código QR de Entrada"
style="
width: 180px;
height: 180px;
display: block;
margin: 0 auto;
"
/>
<p
style="
margin: 12px 0 0 0;
font-size: 12px;
color: #6c757d;
font-weight: 600;
"
>
ESCANEA PARA REGISTRAR ASISTENCIA
</p>
</div>
</div>
<!-- Ticket Info Section -->
<div style="flex: 1.2">
<div style="margin-bottom: 24px">
<h3
style="
color: #003d79;
margin: 0 0 16px 0;
font-size: 18px;
border-bottom: 2px solid #e9ecef;
padding-bottom: 8px;
"
>
📋 Detalles de tu registro
</h3>
<div>
<div style="margin-bottom: 12px">
<p
style="
margin: 0;
font-size: 12px;
color: #6c757d;
text-transform: uppercase;
font-weight: 600;
"
>
Participante
</p>
<p
style="
margin: 2px 0 0 0;
font-size: 15px;
color: #333;
font-weight: 500;
"
>
${correo}
</p>
</div>
<div style="margin-bottom: 12px">
<p
style="
margin: 0;
font-size: 12px;
color: #6c757d;
text-transform: uppercase;
font-weight: 600;
"
>
Formulario
</p>
<p
style="
margin: 2px 0 0 0;
font-size: 15px;
color: #333;
font-weight: 500;
"
>
${nombreForm}
</p>
</div>
<div style="margin-bottom: 12px">
<p
style="
margin: 0;
font-size: 12px;
color: #6c757d;
text-transform: uppercase;
font-weight: 600;
"
>
Fecha de Registro
</p>
<p
style="
margin: 2px 0 0 0;
font-size: 15px;
color: #333;
font-weight: 500;
"
>
${fechaRegistro}
</p>
</div>
<div style="margin-bottom: 12px">
<p
style="
margin: 0;
font-size: 12px;
color: #6c757d;
text-transform: uppercase;
font-weight: 600;
"
>
🕒 Horario del Evento
</p>
<p
style="
margin: 2px 0 0 0;
font-size: 15px;
color: #003d79;
font-weight: 600;
"
>
${horarioEvento}
</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Ticket Footer -->
<div
style="
background: #f8f9fa;
padding: 16px 20px;
border-top: 2px dashed #dee2e6;
"
>
<div
style="
display: flex;
justify-content: space-between;
align-items: center;
"
>
<div>
<p style="margin: 0; font-size: 12px; color: #6c757d">
<strong>IMPORTANTE:</strong> Presenta este código QR en la mesa
de registro
</p>
<p style="margin: 4px 0 0 0; font-size: 11px; color: #6c757d">
Disponible en formato digital o impreso
</p>
</div>
<div style="text-align: right">
<p
style="
margin: 0;
font-size: 14px;
color: #003d79;
font-weight: bold;
"
>
ENTRADA VÁLIDA
</p>
<p style="margin: 2px 0 0 0; font-size: 11px; color: #28a745">
✓ Verificado
</p>
</div>
</div>
</div>
</div>
<!-- Instrucciones adicionales -->
<div
style="
margin-top: 20px;
background: rgba(255, 255, 255, 0.95);
border-radius: 8px;
padding: 16px;
"
>
<h4 style="color: #003d79; margin: 0 0 12px 0; font-size: 16px">
📝 Instrucciones importantes:
</h4>
<ul
style="
margin: 0;
padding-left: 20px;
color: #333;
font-size: 14px;
line-height: 1.6;
"
>
<li>Llega 15 minutos antes del inicio del evento</li>
<li>
Presenta este QR en la mesa de registro para validar tu asistencia
</li>
<li>Tu constancia será otorgada al finalizar el evento</li>
<li>Para dudas, acude al módulo de información durante el evento</li>
</ul>
</div>
<!-- Footer del correo -->
<div style="text-align: center; margin-top: 20px">
<p style="color: rgba(255, 255, 255, 0.8); font-size: 13px; margin: 0">
Este mensaje fue enviado automáticamente. Por favor, no respondas a
este correo.
</p>
</div>
</div>
`;
}
+5
View File
@@ -1,5 +1,6 @@
import { Expose, Type } from 'class-transformer';
import { TipoCuestionarioOutputDto } from 'src/tipo_cuestionario/dto/outputs.dto';
import { TipoEventoOutputDto } from 'src/tipo_evento/dto/outputs.dto';
export class CuestionarioConCupoDto {
@Expose()
@@ -35,6 +36,10 @@ export class CuestionarioConCupoDto {
@Expose()
@Type(() => TipoCuestionarioOutputDto)
tipoCuestionario: TipoCuestionarioOutputDto;
@Expose()
@Type(() => TipoEventoOutputDto)
tipoEvento: TipoEventoOutputDto;
}
export class EventoConCuestionariosDto {
+9
View File
@@ -1,7 +1,10 @@
import { Cuestionario } from 'src/cuestionario/entities/cuestionario.entity';
import { TipoEvento } from 'src/tipo_evento/entities/tipo_evento.entity';
import {
Column,
Entity,
JoinColumn,
ManyToOne,
OneToMany,
PrimaryGeneratedColumn,
} from 'typeorm';
@@ -34,4 +37,10 @@ export class Evento {
@OneToMany(() => Cuestionario, (cuestionario) => cuestionario.evento)
cuestionarios: Cuestionario[];
@ManyToOne(() => TipoEvento, (tipo) => tipo.eventos, {
eager: true,
})
@JoinColumn({ name: 'id_tipo_evento' })
tipoEvento: TipoEvento;
}
+9 -1
View File
@@ -225,11 +225,19 @@ export class EventoService {
async getEventosActivos() {
const now = new Date();
return this.eventoRepository.find({
const eventos = await this.eventoRepository.find({
where: {
fecha_fin: MoreThan(now),
},
relations: ['cuestionarios'],
});
return eventos.map((evento) => ({
...evento,
total_cuestionarios: evento.cuestionarios
? evento.cuestionarios.length
: 0,
}));
}
async getCuestionarioEvento(id_evento: number, id_cuestionario: number) {
@@ -0,0 +1,12 @@
import { IsString, IsNotEmpty } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class RegistrarAsistenciaQrDto {
@ApiProperty({
description: 'Token JWT del código QR escaneado',
example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
})
@IsString()
@IsNotEmpty()
qr_token: string;
}
@@ -12,7 +12,14 @@ import { ParticipanteEventoService } from './participante_evento.service';
import { ParticipanteEvento } from './entities/participante_evento.entity';
import { CreateParticipanteEventoDto } from './dto/create-participante_evento.dto';
import { UpdateParticipanteEventoDto } from './dto/update.participante_evento.dto';
import { ApiTags, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger';
import { RegistrarAsistenciaQrDto } from './dto/registrar-asistencia-qr.dto';
import {
ApiTags,
ApiOperation,
ApiParam,
ApiResponse,
ApiBody,
} from '@nestjs/swagger';
@ApiTags('Participantes-Eventos')
@Controller('participante-evento')
@@ -20,7 +27,11 @@ export class ParticipanteEventoController {
constructor(private participanteEventoService: ParticipanteEventoService) {}
@ApiOperation({ summary: 'Obtener participantes por evento' })
@ApiParam({ name: 'id_cuestionario', description: 'ID del evento', type: 'number' })
@ApiParam({
name: 'id_cuestionario',
description: 'ID del evento',
type: 'number',
})
@ApiResponse({
status: 200,
description: 'Lista de participantes que están registrados en el evento',
@@ -195,6 +206,33 @@ export class ParticipanteEventoController {
);
}
@ApiOperation({
summary: 'Registrar asistencia de un participante a un evento',
})
@ApiBody({
description: 'Token JWT generado por el código QR',
schema: {
type: 'object',
properties: {
token: { type: 'string' },
},
example: {
token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
},
},
})
@ApiResponse({
status: 200,
description: 'Asistencia registrada correctamente',
})
@ApiResponse({ status: 404, description: 'Registro no encontrado' })
@Post('asistencia')
registrarAsistencia(@Body() body: { token: string }) {
return this.participanteEventoService.registrarAsistenciaConToken(
body.token,
);
}
@ApiOperation({
summary: 'Registrar asistencia de un participante a un evento',
})
@@ -210,16 +248,64 @@ export class ParticipanteEventoController {
})
@ApiResponse({ status: 404, description: 'Registro no encontrado' })
@Post('asistencia/:idParticipante/:idEvento')
registrarAsistencia(
registrarAsistenciaSinToken(
@Param('idParticipante', ParseIntPipe) idParticipante: number,
@Param('idEvento', ParseIntPipe) idEvento: number,
) {
return this.participanteEventoService.registrarAsistencia(
return this.participanteEventoService.registrarAsistenciaSinToken(
idParticipante,
idEvento,
);
}
@ApiOperation({
summary: 'Retornala info del token QR',
})
@Post('decode-token')
decodeToken(@Body() body: { token: string }) {
return this.participanteEventoService.validToken(
body.token,
);
}
@ApiOperation({
summary: 'Registrar asistencia usando token QR',
})
@ApiResponse({
status: 200,
description: 'Asistencia registrada correctamente mediante token QR',
schema: {
type: 'object',
properties: {
success: { type: 'boolean' },
message: { type: 'string' },
data: {
type: 'object',
properties: {
participante: { type: 'string' },
fecha_asistencia: { type: 'string', format: 'date-time' },
id_evento: { type: 'number' },
id_cuestionario: { type: 'number' },
},
},
},
},
})
@ApiResponse({
status: 400,
description: 'Token QR inválido o expirado',
})
@ApiResponse({
status: 404,
description: 'Participante no encontrado o no registrado en el evento',
})
@Post('asistencia-qr')
registrarAsistenciaConToken(@Body() body: { qr_token: string }) {
return this.participanteEventoService.registrarAsistenciaConToken(
body.qr_token,
);
}
@ApiOperation({ summary: 'Eliminar un registro participante-evento' })
@ApiParam({ name: 'id', description: 'ID del participante', type: 'number' })
@ApiResponse({
@@ -6,11 +6,13 @@ import { ParticipanteEvento } from './entities/participante_evento.entity';
import { Evento } from 'src/evento/entities/evento.entity';
import { Participante } from 'src/participante/entities/participante.entity';
import { CuestionarioModule } from 'src/cuestionario/cuestionario.module';
import { QrModule } from 'src/qr/qr.module';
@Module({
imports: [
TypeOrmModule.forFeature([ParticipanteEvento, Evento, Participante]),
CuestionarioModule,
QrModule,
],
controllers: [ParticipanteEventoController],
providers: [ParticipanteEventoService],
@@ -7,6 +7,7 @@ import { UpdateParticipanteEventoDto } from './dto/update.participante_evento.dt
import { Evento } from 'src/evento/entities/evento.entity';
import { Participante } from 'src/participante/entities/participante.entity';
import { CuestionarioService } from 'src/cuestionario/cuestionario.service';
import { QrTokenService } from 'src/qr/qr-token.service';
@Injectable()
export class ParticipanteEventoService {
@@ -17,6 +18,7 @@ export class ParticipanteEventoService {
@InjectRepository(Participante)
private participanteRepository: Repository<Participante>,
private readonly cuestionarioService: CuestionarioService,
private readonly qrTokenService: QrTokenService,
) {}
async createParticipanteEvento(
@@ -158,7 +160,10 @@ export class ParticipanteEventoService {
return this.participanteEventoRepository.save(participante_evento);
}
async registrarAsistencia(id_participante: number, id_cuestionario: number) {
async registrarAsistenciaSinToken(
id_participante: number,
id_cuestionario: number,
) {
const participante_eventoFound =
await this.participanteEventoRepository.findOne({
where: {
@@ -175,4 +180,97 @@ export class ParticipanteEventoService {
participante_eventoFound.asistio = true;
return this.participanteEventoRepository.save(participante_eventoFound);
}
/**
* Registra la asistencia usando un token QR
* @param qrToken Token JWT del código QR
* @returns Resultado del registro de asistencia
*/
async registrarAsistenciaConToken(qrToken: string) {
// Validar el token QR
const tokenData = this.qrTokenService.validateQrToken(qrToken);
if (!tokenData) {
throw new HttpException(
'Token QR inválido o expirado',
HttpStatus.BAD_REQUEST,
);
}
const { id_participante, id_evento, id_cuestionario } = tokenData;
// Verificar que el participante existe
const participante = await this.participanteRepository.findOne({
where: { id_participante },
});
if (!participante) {
throw new HttpException(
`Participante con ID ${id_participante} no encontrado`,
HttpStatus.NOT_FOUND,
);
}
// Buscar el registro participante-evento
const participanteEvento = await this.participanteEventoRepository.findOne({
where: {
id_participante,
id_cuestionario,
},
relations: ['participante'],
});
if (!participanteEvento) {
throw new HttpException(
'El participante no está registrado en este evento',
HttpStatus.NOT_FOUND,
);
}
// Verificar si ya había registrado asistencia
if (participanteEvento.asistio && participanteEvento.fecha_asistencia) {
return {
success: true,
message:
'El participante ya había registrado su asistencia previamente',
data: {
participante: participante.correo,
fecha_asistencia_previa: participanteEvento.fecha_asistencia,
fecha_actual: new Date(),
},
};
}
// Registrar la asistencia
participanteEvento.fecha_asistencia = new Date();
participanteEvento.asistio = true;
const resultado =
await this.participanteEventoRepository.save(participanteEvento);
return {
success: true,
message: 'Asistencia registrada exitosamente',
data: {
participante: participante.correo,
fecha_asistencia: resultado.fecha_asistencia,
id_evento,
id_cuestionario,
},
};
}
async validToken(qrToken: string) {
// Validar el token QR
const tokenData = this.qrTokenService.validateQrToken(qrToken);
if (!tokenData) {
throw new HttpException(
'Token QR inválido o expirado',
HttpStatus.BAD_REQUEST,
);
}
return tokenData
}
}
+12
View File
@@ -0,0 +1,12 @@
import { IsString, IsNotEmpty } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class ValidateQrTokenDto {
@ApiProperty({
description: 'Token JWT del código QR',
example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
})
@IsString()
@IsNotEmpty()
token: string;
}
+91
View File
@@ -0,0 +1,91 @@
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class QrTokenService {
constructor(
private jwtService: JwtService,
private configService: ConfigService,
) {}
/**
* Genera un token JWT específico para el QR de asistencia
* @param id_participante ID del participante
* @param id_evento ID del evento
* @param id_cuestionario ID del cuestionario
* @returns Token JWT codificado
*/
generateQrToken(
id_participante: number,
id_evento: number,
id_cuestionario: number,
): string {
const payload = {
id_participante,
id_evento,
id_cuestionario,
type: 'qr_asistencia',
iat: Math.floor(Date.now() / 1000),
};
// Usar una clave secreta específica para QR o la misma que JWT general
const secret =
this.configService.get<string>('QR_JWT_SECRET') ||
this.configService.get<string>('JWT_SECRET', 'tu_clave_secreta');
return this.jwtService.sign(payload, {
secret,
expiresIn: '30d', // El QR puede ser válido por 30 días
});
}
/**
* Valida y decodifica un token de QR
* @param token Token JWT a validar
* @returns Datos decodificados del token o null si es inválido
*/
validateQrToken(token: string): {
id_participante: number;
id_evento: number;
id_cuestionario: number;
type: string;
iat: number;
} | null {
try {
const secret =
this.configService.get<string>('QR_JWT_SECRET') ||
this.configService.get<string>('JWT_SECRET', 'tu_clave_secreta');
console.log(secret)
const decoded = this.jwtService.verify(token, { secret });
// Verificar que sea un token de tipo QR de asistencia
if (decoded.type !== 'qr_asistencia') {
return null;
}
return {
id_participante: decoded.id_participante,
id_evento: decoded.id_evento,
id_cuestionario: decoded.id_cuestionario,
type: decoded.type,
iat: decoded.iat,
};
} catch (error) {
console.error('Error validando token QR:', error.message);
return null;
}
}
/**
* Verifica si un token QR es válido y no ha expirado
* @param token Token a verificar
* @returns true si es válido, false si no
*/
isTokenValid(token: string): boolean {
const decoded = this.validateQrToken(token);
return decoded !== null;
}
}
+58 -4
View File
@@ -10,14 +10,20 @@ import {
Query,
} from '@nestjs/common';
import { QrService } from './qr.service';
import { QrTokenService } from './qr-token.service';
import { Qr } from './qr.entity';
import { CreateQrDto } from './dto/create-qr.dto';
import { UpdateQrDto } from './dto/update.qr.dto';
import { ApiOperation, ApiParam, ApiQuery } from '@nestjs/swagger';
import { ValidateQrTokenDto } from './dto/validate-qr-token.dto';
import { ApiOperation, ApiParam, ApiQuery, ApiTags } from '@nestjs/swagger';
@ApiTags('QR')
@Controller('qr')
export class QrController {
constructor(private qrService: QrService) {}
constructor(
private qrService: QrService,
private qrTokenService: QrTokenService,
) {}
@Get('generate')
@ApiOperation({ summary: 'Genera un código QR a partir de un texto' })
@@ -31,16 +37,64 @@ export class QrController {
}
@Get('asistencia/:idParticipante/:idEvento')
@ApiOperation({ summary: 'Genera un código QR para asistencia con IDs de participante y evento' })
@ApiOperation({
summary:
'Genera un código QR para asistencia con IDs de participante y evento',
})
@ApiParam({ name: 'idParticipante', description: 'ID del participante' })
@ApiParam({ name: 'idEvento', description: 'ID del evento' })
async generateAsistenciaQR(
@Param('idParticipante', ParseIntPipe) idParticipante: number,
@Param('idEvento', ParseIntPipe) idEvento: number
@Param('idEvento', ParseIntPipe) idEvento: number,
): Promise<string> {
return this.qrService.generateAsistenciaQR(idParticipante, idEvento);
}
@Post('validate-token')
@ApiOperation({ summary: 'Valida un token de QR para asistencia' })
async validateQrToken(@Body() validateDto: ValidateQrTokenDto) {
const tokenData = this.qrTokenService.validateQrToken(validateDto.token);
if (!tokenData) {
return {
valid: false,
message: 'Token inválido o expirado',
};
}
return {
valid: true,
data: tokenData,
message: 'Token válido',
};
}
@Get('generate-token/:idParticipante/:idEvento/:idCuestionario')
@ApiOperation({ summary: 'Genera un token JWT para QR de asistencia' })
@ApiParam({ name: 'idParticipante', description: 'ID del participante' })
@ApiParam({ name: 'idEvento', description: 'ID del evento' })
@ApiParam({ name: 'idCuestionario', description: 'ID del cuestionario' })
async generateQrToken(
@Param('idParticipante', ParseIntPipe) idParticipante: number,
@Param('idEvento', ParseIntPipe) idEvento: number,
@Param('idCuestionario', ParseIntPipe) idCuestionario: number,
) {
const token = this.qrTokenService.generateQrToken(
idParticipante,
idEvento,
idCuestionario,
);
return {
token,
qr_data: {
id_participante: idParticipante,
id_evento: idEvento,
id_cuestionario: idCuestionario,
},
};
}
@Get()
getQrs(): Promise<Qr[]> {
return this.qrService.getQrs();
+6 -2
View File
@@ -1,12 +1,16 @@
import { Module } from '@nestjs/common';
import { QrService } from './qr.service';
import { QrController } from './qr.controller';
import { QrTokenService } from './qr-token.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Qr } from './qr.entity';
import { AuthModule } from '../auth/auth.module';
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [TypeOrmModule.forFeature([Qr])],
imports: [TypeOrmModule.forFeature([Qr]), AuthModule, ConfigModule],
controllers: [QrController],
providers: [QrService]
providers: [QrService, QrTokenService],
exports: [QrTokenService],
})
export class QrModule {}
+5 -5
View File
@@ -1,9 +1,9 @@
import { Expose } from 'class-transformer';
export class TipoCuestionarioOutputDto {
@Expose()
id_tipo_cuestionario: number;
@Expose()
id_tipo_cuestionario: number;
@Expose()
tipo_cuestionario: string;
}
@Expose()
tipo_cuestionario: string;
}
@@ -0,0 +1,8 @@
import { IsEnum, IsNotEmpty } from 'class-validator';
import { TipoEventoEnum } from '../entities/tipo_evento.entity';
export class CreateTipoEventoDto {
@IsNotEmpty()
@IsEnum(TipoEventoEnum)
tipo_evento: TipoEventoEnum;
}
+9
View File
@@ -0,0 +1,9 @@
import { Expose } from 'class-transformer';
export class TipoEventoOutputDto {
@Expose()
id_tipo_evento: number;
@Expose()
tipo_evento: string;
}
@@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/mapped-types';
import { CreateTipoEventoDto } from './create-tipo_evento.dto';
export class UpdateTipoEventoDto extends PartialType(CreateTipoEventoDto) {}
@@ -0,0 +1,34 @@
import { Cuestionario } from 'src/cuestionario/entities/cuestionario.entity';
import { Evento } from 'src/evento/entities/evento.entity';
import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm';
export enum TipoEventoEnum {
ACADEMICO = 'Académico',
TALLER_CAPACITACION = 'Taller / Capacitación',
CONFERENCIA_CHARLA = 'Conferencia / Charla',
PANEL_MESA_REDONDA = 'Panel / Mesa redonda',
FERIA_EXPO = 'Feria / Expo',
NETWORKING = 'Networking / Vinculación',
ENTREVISTA = 'Entrevista / Sesión 1:1',
CULTURAL_ARTISTICO = 'Cultural / Artístico',
DEPORTIVO = 'Deportivo / Recreativo',
INSTITUCIONAL = 'Institucional / Ceremonial',
SOCIAL_COMUNITARIO = 'Social / Comunitario',
ANIVERSARIO = 'Aniversario',
OTRO = 'Otro',
}
@Entity('tipo_evento')
export class TipoEvento {
@PrimaryGeneratedColumn()
id_tipo_evento: number;
@Column({ type: 'enum', enum: TipoEventoEnum })
tipo_evento: TipoEventoEnum;
@OneToMany(() => Cuestionario, (cuestionario) => cuestionario.tipoEvento)
cuestionarios: Cuestionario[];
@OneToMany(() => Evento, (evento) => evento.tipoEvento)
eventos: Evento[];
}
+46
View File
@@ -0,0 +1,46 @@
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
ParseIntPipe,
} from '@nestjs/common';
import { TipoEventoService } from './tipo_evento.service';
import { CreateTipoEventoDto } from './dto/create-tipo_evento.dto';
import { UpdateTipoEventoDto } from './dto/update-tipo_evento.dto';
@Controller('tipo-evento')
export class TipoEventoController {
constructor(private readonly tipoEventoService: TipoEventoService) {}
@Post()
create(@Body() createTipoEventoDto: CreateTipoEventoDto) {
return this.tipoEventoService.create(createTipoEventoDto);
}
@Get()
findAll() {
return this.tipoEventoService.findAll();
}
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
return this.tipoEventoService.findOne(id);
}
@Patch(':id')
update(
@Param('id', ParseIntPipe) id: number,
@Body() updateTipoEventoDto: UpdateTipoEventoDto,
) {
return this.tipoEventoService.update(id, updateTipoEventoDto);
}
@Delete(':id')
remove(@Param('id', ParseIntPipe) id: number) {
return this.tipoEventoService.remove(id);
}
}
+13
View File
@@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TipoEvento } from './entities/tipo_evento.entity';
import { TipoEventoController } from './tipo_evento.controller';
import { TipoEventoService } from './tipo_evento.service';
@Module({
imports: [TypeOrmModule.forFeature([TipoEvento])],
controllers: [TipoEventoController],
providers: [TipoEventoService],
exports: [TipoEventoService],
})
export class TipoEventoModule {}
+98
View File
@@ -0,0 +1,98 @@
import {
Injectable,
NotFoundException,
ConflictException,
OnModuleInit,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { TipoEvento, TipoEventoEnum } from './entities/tipo_evento.entity';
import { CreateTipoEventoDto } from './dto/create-tipo_evento.dto';
import { UpdateTipoEventoDto } from './dto/update-tipo_evento.dto';
@Injectable()
export class TipoEventoService implements OnModuleInit {
constructor(
@InjectRepository(TipoEvento)
private tipoEventoRepository: Repository<TipoEvento>,
) {}
async onModuleInit() {
await this.seedTipoEventos();
}
private async seedTipoEventos() {
const enumValues = Object.values(TipoEventoEnum);
for (const tipoEvento of enumValues) {
const existingTipo = await this.tipoEventoRepository.findOne({
where: { tipo_evento: tipoEvento },
});
if (!existingTipo) {
await this.tipoEventoRepository.save({
tipo_evento: tipoEvento,
});
}
}
}
async create(createTipoEventoDto: CreateTipoEventoDto): Promise<TipoEvento> {
const existingTipo = await this.tipoEventoRepository.findOne({
where: { tipo_evento: createTipoEventoDto.tipo_evento },
});
if (existingTipo) {
throw new ConflictException('Este tipo de evento ya existe');
}
const tipoEvento = this.tipoEventoRepository.create(createTipoEventoDto);
return this.tipoEventoRepository.save(tipoEvento);
}
async findAll(): Promise<TipoEvento[]> {
return this.tipoEventoRepository.find({
order: { id_tipo_evento: 'ASC' },
});
}
async findOne(id: number): Promise<TipoEvento> {
const tipoEvento = await this.tipoEventoRepository.findOne({
where: { id_tipo_evento: id },
});
if (!tipoEvento) {
throw new NotFoundException(`Tipo de evento con ID ${id} no encontrado`);
}
return tipoEvento;
}
async update(
id: number,
updateTipoEventoDto: UpdateTipoEventoDto,
): Promise<TipoEvento> {
const tipoEvento = await this.findOne(id);
if (updateTipoEventoDto.tipo_evento) {
const existingTipo = await this.tipoEventoRepository.findOne({
where: { tipo_evento: updateTipoEventoDto.tipo_evento },
});
if (existingTipo && existingTipo.id_tipo_evento !== id) {
throw new ConflictException('Este tipo de evento ya existe');
}
}
Object.assign(tipoEvento, updateTipoEventoDto);
return this.tipoEventoRepository.save(tipoEvento);
}
async remove(id: number): Promise<{ message: string }> {
const tipoEvento = await this.findOne(id);
await this.tipoEventoRepository.remove(tipoEvento);
return { message: 'Tipo de evento eliminado exitosamente' };
}
}
@@ -2,6 +2,11 @@ import { Administrador } from 'src/administrador/entities/administrador.entity';
import { Participante } from 'src/participante/entities/participante.entity';
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
export enum TipoUsuarioEnum {
ADMINISTRADOR = 'administrador',
STAFF = 'staff',
}
@Entity()
export class TipoUser {
@PrimaryGeneratedColumn()
+20 -1
View File
@@ -5,7 +5,7 @@ import {
Injectable,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { TipoUser } from './entities/tipo_user.entity';
import { TipoUser, TipoUsuarioEnum } from './entities/tipo_user.entity';
import { Repository } from 'typeorm';
import { CreateTipoUserDto } from './dto/create-tipo-user.dto';
import { UpdateTipoUserDto } from './dto/update.tipo_user.dto';
@@ -23,6 +23,25 @@ export class TipoUserService {
private administradorRepository: Repository<Administrador>,
) {}
async onModuleInit() {
await this.seedTipoEventos();
}
private async seedTipoEventos() {
const enumValues = Object.values(TipoUsuarioEnum);
for (const tipoUsuario of enumValues) {
const existingTipo = await this.tipoUserRepository.findOne({
where: { tipo: tipoUsuario },
});
if (!existingTipo) {
await this.tipoUserRepository.save({
tipo: tipoUsuario,
});
}
}
}
/*
//Funcion para registrar usuario
async registrarUsuario(dto: CreateTipoUserDto) {