develop #36
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
}
|
||||
+24
-35
@@ -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',
|
||||
// },
|
||||
};
|
||||
|
||||
@@ -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');
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
+6
-3
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
|
||||
export const ROLES_KEY = 'roles';
|
||||
export const Roles = (...roles: ('administrador' | 'staff')[]) =>
|
||||
SetMetadata(ROLES_KEY, roles);
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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 {}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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[];
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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()
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user