6 Commits

239 changed files with 1843 additions and 7002 deletions
-20
View File
@@ -1,20 +0,0 @@
# Etapa de desarrollo para ejecutar en watch mode
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
# Instalar explícitamente las dependencias de autenticación
RUN npm install bcryptjs @types/bcryptjs @nestjs/jwt @nestjs/passport passport passport-jwt @types/passport-jwt
COPY . .
# Asegurarse de compilar el proyecto
RUN npm run build
# Puerto expuesto
EXPOSE 4204
# Comando para iniciar en modo desarrollo
CMD ["npm", "run", "start:dev"]
-22
View File
@@ -97,25 +97,3 @@ Nest is an MIT-licensed open source project. It can grow thanks to the sponsors
## License
Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE).
# usar validadores en el front end
import { validarRespuesta, FabricaValidadores } from
'./validaciones/validador';
// To validate a single response
const respuesta = 'user@example.com';
const tipoValidacion = 'correo';
const resultado = validarRespuesta(respuesta, tipoValidacion);
if (resultado.valido) {
// proceed
} else {
// show error message: resultado.mensaje
}
// Or directly use the validators
const validadorCorreo = new ValidadorCorreo();
const resultadoCorreo = validadorCorreo.validar('user@example.com');
-48
View File
@@ -1,48 +0,0 @@
version: '3.8'
services:
mariadb:
image: mariadb:latest
container_name: formularios_db
ports:
- "4201:3306"
environment:
MARIADB_ROOT_PASSWORD: mypass
MARIADB_DATABASE: formularios_test
volumes:
- mariadb_data:/var/lib/mysql
- ./init-scripts:/docker-entrypoint-initdb.d
restart: unless-stopped
api:
build:
context: .
dockerfile: Dockerfile
container_name: formularios_api
env_file:
- .env
volumes:
- .:/app
- /app/node_modules
ports:
- "4204:4204"
depends_on:
- mariadb
restart: unless-stopped
front:
build: ../formularios_front
container_name: formularios_front
restart: always
depends_on:
- api
env_file:
- ../formularios_front/.env
ports:
- "4202:3000"
volumes:
mariadb_data:
-14
View File
@@ -1,14 +0,0 @@
PORT=4200
db_type=mysql
db_host=mariadb # <-- uso del nombre del servicio, NO IP
db_port=3306
db_username=root # <-- te conectarás con root
db_password=mypass # <-- la misma que MARIADB_ROOT_PASSWORD
db_database=formularios_test
url_api_correos=http://host.docker.internal:4100 #http://localhost:4100
-22
View File
@@ -1,22 +0,0 @@
-- Crear la base de datos para el registro de alumnos
CREATE DATABASE IF NOT EXISTS datos_feria_registro;
USE datos_feria_registro;
-- Crear la tabla registro_alumno
CREATE TABLE IF NOT EXISTS registro_alumno (
id_ncuenta int(9) unsigned zerofill NOT NULL,
nombre varchar(100) NOT NULL,
apellidos varchar(120) NOT NULL,
carrera varchar(100) NOT NULL,
genero char(1) NOT NULL,
PRIMARY KEY (id_ncuenta)
);
-- Insertar algunos datos de ejemplo
INSERT INTO registro_alumno (id_ncuenta, nombre, apellidos, carrera, genero) VALUES
(000123456, 'Juan', 'Pérez López', 'Ingeniería en Computación', 'M'),
(000789012, 'María', 'González Ramírez', 'Licenciatura en Administración', 'F'),
(000456789, 'Carlos', 'Rodríguez Martínez', 'Ingeniería Civil', 'M'),
(000345678, 'Ana', 'Sánchez Jiménez', 'Medicina', 'F'),
(000901234, 'Pedro', 'Fernández Gómez', 'Arquitectura', 'M'),
(316092940, 'Eithan', 'Fernández Gómez', 'Arquitectura', 'M');
+184 -760
View File
File diff suppressed because it is too large Load Diff
+4 -24
View File
@@ -8,7 +8,7 @@
"scripts": {
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "source ~/.nvm/nvm.sh && nvm use 22.10.0 && npm install && nest start",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
@@ -20,32 +20,19 @@
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@nestjs/common": "^11.0.12",
"@nestjs/common": "^11.0.1",
"@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.0.1",
"@nestjs/jwt": "^11.0.0",
"@nestjs/mapped-types": "*",
"@nestjs/passport": "^11.0.5",
"@nestjs/platform-express": "^11.0.1",
"@nestjs/swagger": "^11.1.0",
"@nestjs/typeorm": "^11.0.0",
"@types/bcrypt": "^5.0.2",
"@types/bcryptjs": "^2.4.6",
"@types/passport-jwt": "^4.0.1",
"axios": "^1.8.4",
"bcryptjs": "^3.0.2",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"dotenv": "^16.4.7",
"express": "^5.1.0",
"multer": "^1.4.5-lts.2",
"mysql2": "^3.14.0",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"qrcode": "^1.5.4",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"typeorm": "^0.3.22"
"typeorm": "^0.3.21"
},
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",
@@ -57,9 +44,7 @@
"@swc/core": "^1.10.7",
"@types/express": "^5.0.0",
"@types/jest": "^29.5.14",
"@types/multer": "^1.4.12",
"@types/node": "^22.10.7",
"@types/qrcode": "^1.5.5",
"@types/supertest": "^6.0.2",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
@@ -73,7 +58,7 @@
"ts-loader": "^9.5.2",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.8.2",
"typescript": "^5.7.3",
"typescript-eslint": "^8.20.0"
},
"jest": {
@@ -92,10 +77,5 @@
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
},
"compilerOptions": {
"outDir": "./dist"
}
}
+26 -41
View File
@@ -1,72 +1,57 @@
import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post, UseGuards } from '@nestjs/common';
import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post } from '@nestjs/common';
import { AdministradorService } from './administrador.service';
import { Administrador } from './administrador.entity';
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 } from '@nestjs/swagger';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
import { AdministradorApiDocumentation } from './administrador.documentation';
import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiBody } from '@nestjs/swagger';
@ApiTags('Administradores') // Agrupa los endpoints en Swagger
@Controller('administrador')
@ApiBearerAuth()
@AdministradorApiDocumentation.ApiController
export class AdministradorController {
constructor(private administradorService: AdministradorService) {}
@Get()
@UseGuards(JwtAuthGuard)
@AdministradorApiDocumentation.ApiGetAll
@ApiOperation({ summary: 'Obtener todos los administradores' })
@ApiResponse({ status: 200, description: 'Lista de administradores obtenida correctamente.' })
getAdministradores(): Promise<Administrador[]> {
return this.administradorService.getAdministradores();
}
@Get(':id')
@UseGuards(JwtAuthGuard)
@AdministradorApiDocumentation.ApiGetOne
@ApiOperation({ summary: 'Obtener un administrador por ID' })
@ApiParam({ name: 'id', description: 'ID del administrador', example: 1 })
@ApiResponse({ status: 200, description: 'Administrador obtenido correctamente.' })
@ApiResponse({ status: 404, description: 'Administrador no encontrado.' })
getAdministrador(@Param('id', ParseIntPipe) id: number) {
return this.administradorService.getAdministrador(id);
return this.administradorService.getAdministrador(id)
}
@Post()
@AdministradorApiDocumentation.ApiCreate
@ApiOperation({ summary: 'Registrar un nuevo administrador' })
@ApiBody({
description: 'Datos del administrador a registrar',
schema: {
type: 'object',
properties: {
id_tipo_user: { type: 'integer', example: 1 }
}
}
})
@ApiResponse({ status: 201, description: 'Administrador registrado exitosamente.' })
@ApiResponse({ status: 400, description: 'Datos inválidos.' })
createAdministrador(@Body() newAdministrador: CreateAdministradorDto) {
return this.administradorService.createAdministrador(newAdministrador);
}
@Post('login')
@AdministradorApiDocumentation.ApiLogin
login(@Body() loginData: LoginAdministradorDto) {
return this.administradorService.login(loginData.correo, loginData.password);
}
@Patch(':id/change-password')
@UseGuards(JwtAuthGuard)
@AdministradorApiDocumentation.ApiChangePassword
changePassword(
@Param('id', ParseIntPipe) id: number,
@Body() changePasswordDto: ChangePasswordDto
) {
return this.administradorService.changePassword(
id,
changePasswordDto.currentPassword,
changePasswordDto.newPassword
);
return this.administradorService.createAdministrador(newAdministrador)
}
@Delete(':id')
@UseGuards(JwtAuthGuard)
@AdministradorApiDocumentation.ApiRemove
deleteAdministrador(@Param('id', ParseIntPipe) id: number) {
return this.administradorService.deleteAdministrador(id);
return this.administradorService.deleteAdministrador(id)
}
@Patch(':id')
@UseGuards(JwtAuthGuard)
@AdministradorApiDocumentation.ApiUpdate
updateAdministrador(@Param('id', ParseIntPipe) id: number, @Body() administrador: UpdateAdministradorDto) {
return this.administradorService.updateAdministrador(id, administrador);
return this.administradorService.updateAdministrador(id, administrador)
}
}
@@ -1,314 +0,0 @@
import { ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
import { applyDecorators } from '@nestjs/common';
export class AdministradorApiDocumentation {
// Decorador para toda la clase del controlador
static ApiController = ApiTags('Administradores');
// Documentación para crear un administrador
static ApiCreate = applyDecorators(
ApiOperation({
summary: 'Registrar un nuevo administrador',
description: 'Crea un nuevo administrador con correo, contraseña y tipo de administrador'
}),
ApiBody({
description: 'Datos del administrador a registrar',
schema: {
type: 'object',
required: ['correo', 'password', 'id_tipo_user'],
properties: {
correo: {
type: 'string',
format: 'email',
example: 'admin@ejemplo.com',
description: 'Correo electrónico del administrador'
},
password: {
type: 'string',
format: 'password',
example: 'Password123',
description: 'Contraseña del administrador (mínimo 6 caracteres)'
},
id_tipo_user: {
type: 'integer',
example: 1,
description: 'ID del tipo de usuario administrador'
}
}
}
}),
ApiResponse({
status: 201,
description: 'Administrador registrado exitosamente',
schema: {
type: 'object',
properties: {
id_admnistrador: { type: 'number', example: 1 },
correo: { type: 'string', example: 'admin@ejemplo.com' },
id_tipo_user: { type: 'number', example: 1 }
}
}
}),
ApiResponse({ status: 400, description: 'Datos del administrador inválidos' }),
ApiResponse({ status: 409, description: 'El correo ya está registrado' })
);
// Documentación para iniciar sesión
static ApiLogin = applyDecorators(
ApiOperation({
summary: 'Iniciar sesión como administrador',
description: 'Autentica un administrador con correo y contraseña, y devuelve un token JWT'
}),
ApiBody({
description: 'Credenciales de inicio de sesión',
schema: {
type: 'object',
required: ['correo', 'password'],
properties: {
correo: {
type: 'string',
format: 'email',
example: 'admin@ejemplo.com',
description: 'Correo electrónico del administrador'
},
password: {
type: 'string',
format: 'password',
example: 'Password123',
description: 'Contraseña del administrador'
}
}
}
}),
ApiResponse({
status: 200,
description: 'Inicio de sesión exitoso',
schema: {
type: 'object',
properties: {
access_token: {
type: 'string',
example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
description: 'Token JWT para autenticación'
},
id_administrador: {
type: 'number',
example: 1,
description: 'ID del administrador autenticado'
},
correo: {
type: 'string',
example: 'admin@ejemplo.com',
description: 'Correo del administrador autenticado'
},
tipo_user: {
type: 'number',
example: 1,
description: 'Tipo de usuario del administrador'
}
}
}
}),
ApiResponse({ status: 401, description: 'Credenciales inválidas' })
);
// Documentación para obtener todos los administradores
static ApiGetAll = applyDecorators(
ApiOperation({
summary: 'Obtener todos los administradores',
description: 'Retorna una lista de todos los administradores registrados (requiere autenticación)'
}),
ApiResponse({
status: 200,
description: 'Lista de administradores obtenida correctamente',
schema: {
type: 'array',
items: {
type: 'object',
properties: {
id_admnistrador: { type: 'number', example: 1 },
correo: { type: 'string', example: 'admin@ejemplo.com' },
id_tipo_user: { type: 'number', example: 1 },
tipoUser: {
type: 'array',
items: {
type: 'object',
properties: {
id_tipo_user: { type: 'number', example: 1 },
tipo_user: { type: 'string', example: 'Administrador General' }
}
}
}
}
}
}
}),
ApiResponse({ status: 401, description: 'No autorizado' })
);
// Documentación para obtener un administrador por ID
static ApiGetOne = applyDecorators(
ApiOperation({
summary: 'Obtener un administrador por ID',
description: 'Retorna los datos de un administrador específico según su ID (requiere autenticación)'
}),
ApiParam({
name: 'id',
description: 'ID del administrador',
type: 'number',
example: 1
}),
ApiResponse({
status: 200,
description: 'Administrador obtenido correctamente',
schema: {
type: 'object',
properties: {
id_admnistrador: { type: 'number', example: 1 },
correo: { type: 'string', example: 'admin@ejemplo.com' },
id_tipo_user: { type: 'number', example: 1 },
tipoUser: {
type: 'array',
items: {
type: 'object',
properties: {
id_tipo_user: { type: 'number', example: 1 },
tipo_user: { type: 'string', example: 'Administrador General' }
}
}
}
}
}
}),
ApiResponse({ status: 404, description: 'Administrador no encontrado' }),
ApiResponse({ status: 401, description: 'No autorizado' })
);
// Documentación para actualizar un administrador
static ApiUpdate = applyDecorators(
ApiOperation({
summary: 'Actualizar datos de un administrador',
description: 'Actualiza la información de un administrador existente (requiere autenticación)'
}),
ApiParam({
name: 'id',
description: 'ID del administrador a actualizar',
type: 'number',
example: 1
}),
ApiBody({
description: 'Datos a actualizar del administrador',
schema: {
type: 'object',
properties: {
correo: {
type: 'string',
format: 'email',
example: 'nuevo_admin@ejemplo.com',
description: 'Nuevo correo electrónico del administrador'
},
password: {
type: 'string',
format: 'password',
example: 'NuevaPassword123',
description: 'Nueva contraseña del administrador (mínimo 6 caracteres)'
},
id_tipo_user: {
type: 'integer',
example: 2,
description: 'Nuevo tipo de usuario administrador'
}
}
}
}),
ApiResponse({
status: 200,
description: 'Administrador actualizado correctamente',
schema: {
type: 'object',
properties: {
id_admnistrador: { type: 'number', example: 1 },
correo: { type: 'string', example: 'nuevo_admin@ejemplo.com' },
id_tipo_user: { type: 'number', example: 2 }
}
}
}),
ApiResponse({ status: 400, description: 'Datos de actualización inválidos' }),
ApiResponse({ status: 404, description: 'Administrador no encontrado' }),
ApiResponse({ status: 401, description: 'No autorizado' })
);
// Documentación para cambiar contraseña
static ApiChangePassword = applyDecorators(
ApiOperation({
summary: 'Cambiar contraseña de un administrador',
description: 'Permite a un administrador cambiar su contraseña verificando primero la contraseña actual (requiere autenticación)'
}),
ApiParam({
name: 'id',
description: 'ID del administrador',
type: 'number',
example: 1
}),
ApiBody({
description: 'Datos para cambio de contraseña',
schema: {
type: 'object',
required: ['currentPassword', 'newPassword'],
properties: {
currentPassword: {
type: 'string',
format: 'password',
example: 'Password123',
description: 'Contraseña actual del administrador'
},
newPassword: {
type: 'string',
format: 'password',
example: 'NuevaPassword123',
description: 'Nueva contraseña del administrador (mínimo 6 caracteres)'
}
}
}
}),
ApiResponse({
status: 200,
description: 'Contraseña actualizada correctamente',
schema: {
type: 'object',
properties: {
message: { type: 'string', example: 'Contraseña actualizada correctamente' }
}
}
}),
ApiResponse({ status: 400, description: 'Contraseña actual incorrecta' }),
ApiResponse({ status: 404, description: 'Administrador no encontrado' }),
ApiResponse({ status: 401, description: 'No autorizado' })
);
// Documentación para eliminar un administrador
static ApiRemove = applyDecorators(
ApiOperation({
summary: 'Eliminar un administrador',
description: 'Elimina permanentemente un administrador por su ID (requiere autenticación)'
}),
ApiParam({
name: 'id',
description: 'ID del administrador a eliminar',
type: 'number',
example: 1
}),
ApiResponse({
status: 200,
description: 'Administrador eliminado correctamente',
schema: {
type: 'object',
properties: {
affected: { type: 'number', example: 1 }
}
}
}),
ApiResponse({ status: 404, description: 'Administrador no encontrado' }),
ApiResponse({ status: 401, description: 'No autorizado' })
);
}
@@ -6,12 +6,6 @@ export class Administrador {
@PrimaryGeneratedColumn()
id_admnistrador: number
@Column({ unique: true })
correo: string
@Column()
password: string
/*
@ManyToOne(() => TipoUser, (tipoUser) => tipoUser.administradores)
@JoinColumn({ name: "id_tipo_user" })
+1 -5
View File
@@ -3,13 +3,9 @@ import { AdministradorService } from './administrador.service';
import { AdministradorController } from './administrador.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Administrador } from './administrador.entity';
import { AuthModule } from '../auth/auth.module';
@Module({
imports: [
TypeOrmModule.forFeature([Administrador]),
AuthModule
],
imports: [TypeOrmModule.forFeature([Administrador])],
controllers: [AdministradorController],
providers: [AdministradorService],
exports: [AdministradorService],
+23 -115
View File
@@ -4,115 +4,36 @@ import { Administrador } from './administrador.entity';
import { Repository } from 'typeorm';
import { CreateAdministradorDto } from './dto/create-administrador.dto';
import { UpdateAdministradorDto } from './dto/update.administrador.dto';
import * as bcrypt from 'bcryptjs';
import { JwtService } from '@nestjs/jwt';
@Injectable()
export class AdministradorService {
constructor(
@InjectRepository(Administrador) private administradorRepository: Repository<Administrador>,
private jwtService: JwtService
@InjectRepository(Administrador) private administradorRepository: Repository<Administrador>
) {}
async createAdministrador(administrador: CreateAdministradorDto) {
// Verificar si ya existe un administrador con el mismo correo
//revisar el where
const administradorFound = await this.administradorRepository.findOne({
where: {
correo: administrador.correo
id_admnistrador: administrador.id_tipo_user
}
});
})
if (administradorFound) {
throw new HttpException('El correo ya está registrado', HttpStatus.CONFLICT);
return new HttpException('Administrador already exists', HttpStatus.CONFLICT)
}
// Encriptar la contraseña
const hashedPassword = await this.hashPassword(administrador.password);
// Crear y guardar el nuevo administrador
const newAdministrador = this.administradorRepository.create({
correo: administrador.correo,
password: hashedPassword,
id_tipo_user: administrador.id_tipo_user
});
const savedAdministrador = await this.administradorRepository.save(newAdministrador);
// Excluir la contraseña de la respuesta
const { password, ...result } = savedAdministrador;
return result;
}
async login(correo: string, password: string) {
// Buscar administrador por correo
const administrador = await this.administradorRepository.findOne({
where: { correo },
relations: ['tipoUser']
});
if (!administrador) {
throw new HttpException('Credenciales inválidas', HttpStatus.UNAUTHORIZED);
}
// Verificar contraseña
const isPasswordValid = await bcrypt.compare(password, administrador.password);
if (!isPasswordValid) {
throw new HttpException('Credenciales inválidas', HttpStatus.UNAUTHORIZED);
}
// Generar token JWT
const payload = {
sub: administrador.id_admnistrador,
correo: administrador.correo,
id_tipo_user: administrador.id_tipo_user
};
return {
access_token: this.jwtService.sign(payload),
id_administrador: administrador.id_admnistrador,
correo: administrador.correo,
tipo_user: administrador.id_tipo_user
};
}
async changePassword(id_admnistrador: number, currentPassword: string, newPassword: string) {
// Buscar administrador
const administrador = await this.administradorRepository.findOne({
where: { id_admnistrador }
});
if (!administrador) {
throw new HttpException('Administrador no encontrado', HttpStatus.NOT_FOUND);
}
// Verificar contraseña actual
const isPasswordValid = await bcrypt.compare(currentPassword, administrador.password);
if (!isPasswordValid) {
throw new HttpException('Contraseña actual incorrecta', HttpStatus.BAD_REQUEST);
}
// Hashear y actualizar nueva contraseña
administrador.password = await this.hashPassword(newPassword);
await this.administradorRepository.save(administrador);
return { message: 'Contraseña actualizada correctamente' };
}
private async hashPassword(password: string): Promise<string> {
const salt = await bcrypt.genSalt();
return bcrypt.hash(password, salt);
//Falta regresar el return
//return this.administradorRepository.save(administradorFound)
}
getAdministradores() {
return this.administradorRepository.find({
relations: ['tipoUser'],
select: {
id_admnistrador: true,
correo: true,
id_tipo_user: true
}
});
relations: ['tipoUser']
})
}
async getAdministrador(id_admnistrador: number) {
@@ -120,29 +41,24 @@ export class AdministradorService {
where: {
id_admnistrador
},
relations: ['tipoUser'],
select: {
id_admnistrador: true,
correo: true,
id_tipo_user: true
}
});
relations: ['tipoUser']
})
if (!administradorFound) {
throw new HttpException('Administrador no encontrado', HttpStatus.NOT_FOUND);
if (!Administrador) {
return new HttpException('User not found', HttpStatus.NOT_FOUND);
}
return administradorFound;
return administradorFound
}
async deleteAdministrador(id_admnistrador: number) {
const result = await this.administradorRepository.delete({ id_admnistrador });
const result = await this.administradorRepository.delete({ id_admnistrador })
if (result.affected === 0) {
throw new HttpException('Administrador no encontrado', HttpStatus.NOT_FOUND);
return new HttpException('User not found', HttpStatus.NOT_FOUND);
}
return result;
return result
}
async updateAdministrador(id_admnistrador: number, administrador: UpdateAdministradorDto) {
@@ -150,22 +66,14 @@ export class AdministradorService {
where: {
id_admnistrador
}
});
})
if (!administradorFound) {
throw new HttpException('Administrador no encontrado', HttpStatus.NOT_FOUND);
return new HttpException('User not found', HttpStatus.NOT_FOUND)
}
// Si se incluye una nueva contraseña, hashearla
if (administrador.password) {
administrador.password = await this.hashPassword(administrador.password);
}
const updateAdministrador = Object.assign(administradorFound, administrador);
const savedAdministrador = await this.administradorRepository.save(updateAdministrador);
// Excluir la contraseña de la respuesta
const { password, ...result } = savedAdministrador;
return result;
const updateAdministrador = Object.assign(administradorFound, administrador)
return this.administradorRepository.save(updateAdministrador)
}
}
@@ -1,13 +0,0 @@
import { IsNotEmpty, MinLength } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class ChangePasswordDto {
@ApiProperty({ description: 'Contraseña actual del administrador' })
@IsNotEmpty({ message: 'La contraseña actual es requerida' })
currentPassword: string;
@ApiProperty({ description: 'Nueva contraseña del administrador' })
@MinLength(6, { message: 'La nueva contraseña debe tener al menos 6 caracteres' })
@IsNotEmpty({ message: 'La nueva contraseña es requerida' })
newPassword: string;
}
@@ -1,19 +1,3 @@
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: '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;
id_tipo_user: number
}
@@ -1,13 +0,0 @@
import { IsEmail, IsNotEmpty, MinLength } 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: 'Password123', description: 'Contraseña del administrador' })
@IsNotEmpty({ message: 'La contraseña es requerida' })
password: string;
}
@@ -1,19 +1,3 @@
import { IsEmail, IsNumber, IsOptional, MinLength } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class UpdateAdministradorDto {
@ApiProperty({ example: 'admin@example.com', description: 'Correo del administrador', required: false })
@IsEmail({}, { message: 'El correo no es válido' })
@IsOptional()
correo?: string;
@ApiProperty({ example: 'Password123', description: 'Contraseña del administrador', required: false })
@MinLength(6, { message: 'La contraseña debe tener al menos 6 caracteres' })
@IsOptional()
password?: string;
@ApiProperty({ example: 1, description: 'ID del tipo de usuario', required: false })
@IsNumber({}, { message: 'El tipo de usuario debe ser un número' })
@IsOptional()
id_tipo_user?: number;
id_tipo_user?: number
}
-24
View File
@@ -1,24 +0,0 @@
import { Controller, Get, Param, NotFoundException } from '@nestjs/common';
import { AlumnosService } from './alumnos.service';
import { ApiTags, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger';
import { AlumnoDto, ALUMNO_RESPONSES } from './alumnos.documentation';
@ApiTags('alumnos')
@Controller('alumnos')
export class AlumnosController {
constructor(private readonly alumnosService: AlumnosService) {}
@Get(':cuenta')
@ApiOperation({ summary: 'Obtener datos de un alumno por su número de cuenta' })
@ApiParam({ name: 'cuenta', description: 'Número de cuenta del alumno' })
@ApiResponse(ALUMNO_RESPONSES[200])
@ApiResponse(ALUMNO_RESPONSES[404])
@ApiResponse(ALUMNO_RESPONSES[500])
async getAlumnoByCuenta(@Param('cuenta') cuenta: string): Promise<AlumnoDto | null> {
const alumno = await this.alumnosService.findByCuenta(cuenta);
if (!alumno) {
throw new NotFoundException(`No se encontró alumno con número de cuenta ${cuenta}`);
}
return alumno;
}
}
-47
View File
@@ -1,47 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
export class AlumnoDto {
@ApiProperty({
example: 123456789,
description: 'Número de cuenta del alumno (9 dígitos)',
})
id_ncuenta: number;
@ApiProperty({
example: 'Juan',
description: 'Nombre del alumno',
})
nombre: string;
@ApiProperty({
example: 'Pérez López',
description: 'Apellidos del alumno',
})
apellidos: string;
@ApiProperty({
example: 'Ingeniería en Computación',
description: 'Carrera que cursa el alumno',
})
carrera: string;
@ApiProperty({
example: 'M',
description: 'Género del alumno (M: Masculino, F: Femenino)',
enum: ['M', 'F'],
})
genero: string;
}
export const ALUMNO_RESPONSES = {
200: {
description: 'Datos del alumno obtenidos correctamente',
type: AlumnoDto,
},
404: {
description: 'Alumno no encontrado',
},
500: {
description: 'Error interno del servidor',
},
};
-58
View File
@@ -1,58 +0,0 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AlumnosController } from './alumnos.controller';
import { AlumnosService } from './alumnos.service';
import { RegistroAlumno } from './entities/registro-alumno.entity';
import { ConfigModule, ConfigService } from '@nestjs/config';
@Module({
imports: [
TypeOrmModule.forFeature([RegistroAlumno], 'alumnosConnection'),
ConfigModule.forRoot({
isGlobal: true, // importante si querés que esté disponible en todos lados
envFilePath: '.env', // opcional si el archivo ya es '.env'
}),
TypeOrmModule.forRootAsync({
name: 'alumnosConnection',
useFactory: (configService: ConfigService) => ({
type: 'mysql',
host: configService.get<string>('ALUMNOS_DB_HOST'),
port: Number(configService.get<number>('ALUMNOS_DB_PORT')),
username: configService.get<string>('ALUMNOS_DB_USERNAME'),
password: configService.get<string>('ALUMNOS_DB_PASSWORD'),
database: configService.get<string>('ALUMNOS_DB_DATABASE'),
entities: [RegistroAlumno],
synchronize: false,
retryAttempts: 10,
retryDelay: 3000,
connectTimeout: 30000,
}),
inject: [ConfigService],
}),
/* TypeOrmModule.forRoot({
name: 'alumnosConnection',
type: 'mysql',
host: process.env.ALUMNOS_DB_HOST ,
port: Number(process.env.ALUMNOS_DB_PORT),
username: process.env.ALUMNOS_DB_USERNAME ,
password: process.env.ALUMNOS_DB_PASSWORD ,
database: process.env.ALUMNOS_DB_DATABASE ,
entities: [RegistroAlumno],
synchronize: false,
retryAttempts: 10, // Intentos para reconectar
retryDelay: 3000, // Tiempo entre reintentos
connectTimeout: 30000, // Timeout de conexión (30 segundos) │ │
}), */
],
controllers: [AlumnosController],
providers: [AlumnosService],
exports: [AlumnosService],
})
export class AlumnosModule {}
-18
View File
@@ -1,18 +0,0 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { RegistroAlumno } from './entities/registro-alumno.entity';
@Injectable()
export class AlumnosService {
constructor(
@InjectRepository(RegistroAlumno, 'alumnosConnection')
private registroAlumnoRepository: Repository<RegistroAlumno>,
) {}
async findByCuenta(cuenta: string): Promise<RegistroAlumno | null> {
return this.registroAlumnoRepository.findOne({
where: { id_ncuenta: parseInt(cuenta, 10) },
});
}
}
@@ -1,19 +0,0 @@
import { Entity, Column, PrimaryColumn } from 'typeorm';
@Entity('registro_alumno')
export class RegistroAlumno {
@PrimaryColumn({ type: 'int', unsigned: true, zerofill: true, width: 9 })
id_ncuenta: number;
@Column({ type: 'varchar', length: 100 })
nombre: string;
@Column({ type: 'varchar', length: 120 })
apellidos: string;
@Column({ type: 'varchar', length: 100 })
carrera: string;
@Column({ type: 'char', length: 1 })
genero: string;
}
+17 -77
View File
@@ -1,5 +1,4 @@
//import { Module } from '@nestjs/common;
import { Module } from '@nestjs/common';
import { forwardRef, Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { CuestionarioModule } from './cuestionario/cuestionario.module';
@@ -15,100 +14,41 @@ import { RespuestaParticipanteCerradaModule } from './respuesta_participante_cer
import { CuestionarioRespondidoModule } from './cuestionario_respondido/cuestionario_respondido.module';
import { RespuestaParticipanteAbiertaModule } from './respuesta_participante_abierta/respuesta_participante_abierta.module';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { EventoModule } from './evento/evento.module';
import { TipoUserModule } from './tipo_user/tipo_user.module';
import { ConfigModule } from '@nestjs/config';
import { ParticipanteModule } from './participante/participante.module';
import { QrModule } from './qr/qr.module';
import { AdministradorModule } from './administrador/administrador.module';
import { AsistenciaModule } from './asistencia/asistencia.module';
import { ParticipanteEventoModule } from './participante_evento/participante_evento.module';
import { ValidacionesModule } from './validaciones/validaciones.module';
import { AlumnosModule } from './alumnos/alumnos.module';
import { AuthModule } from './auth/auth.module';
import { RegistroAlumno } from './alumnos/entities/registro-alumno.entity';
import { ImagenController } from './imagen/imagen.controller';
import { ImagenModule } from './imagen/imagen.module';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
}),
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
type: config.get<'mysql'>('DB_TYPE'),
host: config.get<string>('DB_HOST'),
port: +(config.get<number>('DB_PORT') || 3306),
username: config.get<string>('DB_USERNAME'),
password: config.get<string>('DB_PASSWORD'),
database: config.get<string>('DB_DATABASE'),
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: true,
//dropSchema: true, // elimina la base de datos
}),
/*
ConfigModule.forRoot(),
TypeOrmModule.forRoot({
type: 'mysql',
host: process.env.db_host, //process.env.db_host
host: process.env.db_host,
username: process.env.db_username,
database: process.env.db_database,
password: process.env.db_password,
port: Number(process.env.db_port),
*/
//synchronize: true,
//dropSchema: true, // elimina la base de datos
synchronize: true,
dropSchema: true, // elimina la base de datos
// logging: true, // Habilita los logs para depuración
//autoLoadEntities: true, // Carga automáticamente las entidades
autoLoadEntities: true, // Carga automáticamente las entidades
//esta es mi base de datos local
/*
type: 'mysql',
host: 'localhost',
port: 3306, //3306
username: 'root',
password: 'admin', //admin
database: 'cidwa', //nestdb
*/
// entities: [__dirname + '/**/*.entity{.ts,.js}'],
// synchronize: true,
//extra
//retryAttempts: 10, // Intentos para reconectar
//retryDelay: 3000, // Tiempo entre reintentos
}),
TypeOrmModule.forFeature([RegistroAlumno], 'alumnosConnection'),
AuthModule,
CuestionarioModule,
forwardRef(() => CuestionarioModule),
forwardRef(() => SeccionModule),
forwardRef(() => PreguntaModule),
forwardRef(() => OpcionModule),
ParticipanteModule,
TipoCuestionarioModule,
CuestionarioSeccionModule,
SeccionModule,
SeccionPreguntaModule,
PreguntaModule,
TipoPreguntaModule,
OpcionModule, PreguntaOpcionModule,
PreguntaOpcionModule,
RespuestaParticipanteCerradaModule,
CuestionarioRespondidoModule,
RespuestaParticipanteAbiertaModule,
EventoModule,
TipoUserModule,
ParticipanteModule,
QrModule,
AdministradorModule,
AsistenciaModule,
ParticipanteEventoModule,
ValidacionesModule,
AlumnosModule,
ImagenModule,
],
RespuestaParticipanteAbiertaModule],
controllers: [AppController],
providers: [AppService],
-22
View File
@@ -1,22 +0,0 @@
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Administrador } from '../administrador/administrador.entity';
import { JwtStrategy } from './jwt.strategy';
@Module({
imports: [
PassportModule.register({ defaultStrategy: 'jwt' }),
JwtModule.register({
secret: process.env.JWT_SECRET || 'tu_clave_secreta',
signOptions: {
expiresIn: '24h', // Tokens expiran en 24 horas
},
}),
TypeOrmModule.forFeature([Administrador]),
],
providers: [JwtStrategy],
exports: [PassportModule, JwtModule],
})
export class AuthModule {}
-5
View File
@@ -1,5 +0,0 @@
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}
-39
View File
@@ -1,39 +0,0 @@
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { Repository } from 'typeorm';
import { InjectRepository } from '@nestjs/typeorm';
import { Administrador } from '../administrador/administrador.entity';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(
@InjectRepository(Administrador)
private administradorRepository: Repository<Administrador>,
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: process.env.JWT_SECRET || 'tu_clave_secreta',
});
}
async validate(payload: any) {
const { sub: id_admnistrador } = payload;
const administrador = await this.administradorRepository.findOne({
where: { id_admnistrador },
select: {
id_admnistrador: true,
correo: true,
id_tipo_user: true
}
});
if (!administrador) {
throw new UnauthorizedException('Token inválido');
}
return administrador;
}
}
+195 -27
View File
@@ -1,48 +1,216 @@
import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common';
import {
Controller,
Post,
Body,
Get,
Param,
Put,
Delete,
HttpStatus,
HttpException,
Query,
} from '@nestjs/common';
import { CuestionarioService } from './cuestionario.service';
import { CreateCuestionarioDto } from './dto/create-cuestionario.dto';
import { UpdateCuestionarioDto } from './dto/update-cuestionario.dto';
import { CuestionarioApiDocumentation } from './cuestionario.documentation';
import { ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger';
import {
ApiTags,
ApiOperation,
ApiResponse,
ApiParam,
ApiBody,
ApiQuery,
} from '@nestjs/swagger';
import { SeccionExistenteCuestionarioDto } from './dto/seccion-existente-cuestionario.dto';
@Controller('cuestionario')
@CuestionarioApiDocumentation.ApiController
@ApiTags('Cuestionarios')
@Controller('cuestionarios')
export class CuestionarioController {
constructor(private readonly cuestionarioService: CuestionarioService) {}
@Post()
@CuestionarioApiDocumentation.ApiCreate
create(@Body() createCuestionarioDto: CreateCuestionarioDto) {
return this.cuestionarioService.create(createCuestionarioDto);
@ApiOperation({ summary: 'Crear un nuevo cuestionario' })
@ApiBody({ type: CreateCuestionarioDto })
@ApiResponse({
status: HttpStatus.CREATED,
description: 'Cuestionario creado exitosamente con sus secciones y preguntas',
})
@ApiResponse({
status: HttpStatus.BAD_REQUEST,
description: 'Datos de entrada inválidos',
})
async create(@Body() createCuestionarioDto: CreateCuestionarioDto) {
try {
return await this.cuestionarioService.create(createCuestionarioDto);
} catch (error) {
throw new HttpException(
{
status: HttpStatus.BAD_REQUEST,
error: error.message,
},
HttpStatus.BAD_REQUEST,
);
}
}
@Get()
@CuestionarioApiDocumentation.ApiGetAll
findAll() {
@ApiOperation({ summary: 'Obtener todos los cuestionarios' })
@ApiQuery({
name: 'tipo',
required: false,
description: 'Filtrar por tipo de cuestionario',
})
@ApiResponse({
status: HttpStatus.OK,
description: 'Lista de cuestionarios obtenida exitosamente',
})
async findAll(@Query('tipo') tipoCuestionarioId?: number) {
if (tipoCuestionarioId) {
return this.cuestionarioService.findAllByTipo(tipoCuestionarioId);
}
return this.cuestionarioService.findAll();
}
@Get(':id')
@CuestionarioApiDocumentation.ApiGetOne
findOne(@Param('id') id: string) {
return this.cuestionarioService.findOne(+id);
}
@Get(':id/formulario')
@CuestionarioApiDocumentation.ApiGetFormulario
findFormulario(@Param('id') id: string) {
return this.cuestionarioService.findCompleto(+id);
@ApiOperation({ summary: 'Obtener un cuestionario específico con todas sus relaciones' })
@ApiParam({ name: 'id', description: 'ID del cuestionario' })
@ApiResponse({
status: HttpStatus.OK,
description: 'Cuestionario encontrado con todas sus secciones y preguntas',
})
@ApiResponse({
status: HttpStatus.NOT_FOUND,
description: 'Cuestionario no encontrado',
})
async findOne(@Param('id') id: string) {
const cuestionario = await this.cuestionarioService.findOneWithRelations(+id);
if (!cuestionario) {
throw new HttpException(
'Cuestionario no encontrado',
HttpStatus.NOT_FOUND,
);
}
return cuestionario;
}
@Patch(':id')
@CuestionarioApiDocumentation.ApiUpdate
update(@Param('id') id: string, @Body() updateCuestionarioDto: UpdateCuestionarioDto) {
return this.cuestionarioService.update(+id, updateCuestionarioDto);
@Put(':id')
@ApiOperation({ summary: 'Actualizar los metadatos de un cuestionario' })
@ApiParam({ name: 'id', description: 'ID del cuestionario a actualizar' })
@ApiBody({ type: UpdateCuestionarioDto })
@ApiResponse({
status: HttpStatus.OK,
description: 'Cuestionario actualizado exitosamente',
})
@ApiResponse({
status: HttpStatus.NOT_FOUND,
description: 'Cuestionario no encontrado',
})
async update(
@Param('id') id: string,
@Body() updateCuestionarioDto: UpdateCuestionarioDto,
) {
try {
return await this.cuestionarioService.update(+id, updateCuestionarioDto);
} catch (error) {
throw new HttpException(
{
status: HttpStatus.BAD_REQUEST,
error: error.message,
},
HttpStatus.BAD_REQUEST,
);
}
}
@Delete(':id')
@CuestionarioApiDocumentation.ApiRemove
remove(@Param('id') id: string) {
return this.cuestionarioService.remove(+id);
@ApiOperation({ summary: 'Eliminar un cuestionario y todas sus relaciones' })
@ApiParam({ name: 'id', description: 'ID del cuestionario a eliminar' })
@ApiResponse({
status: HttpStatus.OK,
description: 'Cuestionario eliminado exitosamente',
})
@ApiResponse({
status: HttpStatus.NOT_FOUND,
description: 'Cuestionario no encontrado',
})
async remove(@Param('id') id: string) {
try {
return await this.cuestionarioService.remove(+id);
} catch (error) {
throw new HttpException(
{
status: HttpStatus.BAD_REQUEST,
error: error.message,
},
HttpStatus.BAD_REQUEST,
);
}
}
}
@Post(':id/secciones')
@ApiOperation({ summary: 'Agregar una sección existente al cuestionario' })
@ApiParam({ name: 'id', description: 'ID del cuestionario' })
@ApiBody({ type: SeccionExistenteCuestionarioDto })
@ApiResponse({
status: HttpStatus.CREATED,
description: 'Sección agregada al cuestionario exitosamente',
})
@ApiResponse({
status: HttpStatus.NOT_FOUND,
description: 'Cuestionario o sección no encontrada',
})
async agregarSeccion(
@Param('id') cuestionarioId: string,
@Body() seccionDto: SeccionExistenteCuestionarioDto,
) {
try {
return await this.cuestionarioService.agregarSeccionExistente(
+cuestionarioId,
seccionDto.id_seccion,
seccionDto.posicion,
);
} catch (error) {
throw new HttpException(
{
status: error.status || HttpStatus.BAD_REQUEST,
error: error.message,
},
error.status || HttpStatus.BAD_REQUEST,
);
}
}
@Delete(':cuestionarioId/secciones/:seccionId')
@ApiOperation({ summary: 'Remover una sección del cuestionario' })
@ApiParam({ name: 'cuestionarioId', description: 'ID del cuestionario' })
@ApiParam({ name: 'seccionId', description: 'ID de la sección a remover' })
@ApiResponse({
status: HttpStatus.OK,
description: 'Sección removida del cuestionario exitosamente',
})
@ApiResponse({
status: HttpStatus.NOT_FOUND,
description: 'Relación no encontrada',
})
async removerSeccion(
@Param('cuestionarioId') cuestionarioId: string,
@Param('seccionId') seccionId: string,
) {
try {
return await this.cuestionarioService.eliminarSeccionDeCuestionario(
+cuestionarioId,
+seccionId,
);
} catch (error) {
throw new HttpException(
{
status: error.status || HttpStatus.BAD_REQUEST,
error: error.message,
},
error.status || HttpStatus.BAD_REQUEST,
);
}
}
}
@@ -1,210 +0,0 @@
import { ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags, getSchemaPath } from '@nestjs/swagger';
import { feriaSexualidad } from '../../utils/crear_formulario_feria';
import { applyDecorators } from '@nestjs/common';
import { FormularioDto, FormularioFeriaSchema } from './dto/formulario.schema';
export class CuestionarioApiDocumentation {
// Decoradores para toda la clase del controlador
static ApiController = ApiTags('Cuestionarios');
// Documentación para obtener el formulario completo
static ApiGetFormulario = applyDecorators(
ApiOperation({
summary: 'Obtener formulario completo',
description: 'Devuelve el cuestionario completo con la misma estructura que se usa para crearlo, incluyendo el atributo validacion para las preguntas que tengan reglas de validación específicas'
}),
ApiParam({
name: 'id',
description: 'ID del cuestionario',
required: true,
type: 'number',
example: 1
}),
ApiResponse({
status: 200,
description: 'Formulario completo con todas sus secciones, preguntas y opciones. Las preguntas incluyen el atributo "validacion" si tienen reglas de validación específicas (como "correo", "cuenta_alumno", "telefono", "nombre", etc.)',
type: FormularioDto,
content: {
'application/json': {
schema: { $ref: getSchemaPath(FormularioDto) },
example: feriaSexualidad
}
}
}),
ApiResponse({ status: 404, description: 'Cuestionario no encontrado' })
);
// Documentación para crear un cuestionario
static ApiCreate = applyDecorators(
ApiOperation({
summary: 'Crear un nuevo cuestionario',
description: 'Crea un nuevo cuestionario con secciones, preguntas y opciones, incluyendo validaciones personalizadas'
}),
ApiBody({
description: 'Datos del cuestionario a crear. Para las preguntas de tipo "Abierta", se puede especificar un tipo de validación usando la propiedad "validacion" con valores como: "correo", "cuenta_alumno", "telefono", "nombre", "entero", "decimal", etc.',
type: FormularioDto,
examples: {
feriaSexualidad: {
summary: 'Ejemplo de cuestionario para feria de sexualidad',
description: 'Cuestionario con secciones, preguntas y opciones para feria de sexualidad, ahora con validaciones específicas para cada tipo de campo (correo, nombre, cuenta_alumno, etc.)',
value: feriaSexualidad
}
}
}),
ApiResponse({
status: 201,
description: 'Cuestionario creado correctamente. Los campos con la propiedad "validacion" se validarán según su tipo: "correo", "cuenta_alumno", "telefono", "nombre", "entero", "decimal", etc.',
schema: {
properties: {
success: { type: 'boolean', example: true },
cuestionario: {
type: 'object',
properties: {
id_cuestionario: { type: 'number', example: 1 },
nombre_form: { type: 'string', example: 'Registro para la Feria de la Sexualidad - FES Acatlán' },
descripcion: { type: 'string' },
contador_secciones: { type: 'number', example: 1 },
editable: { type: 'boolean', example: true },
fecha_fin: { type: 'string', format: 'date-time' },
fecha_inicio: { type: 'string', format: 'date-time' },
id_tipo_cuestionario: { type: 'number', example: 1 }
}
}
}
}
}),
ApiResponse({ status: 400, description: 'Datos del cuestionario inválidos' }),
ApiResponse({ status: 500, description: 'Error interno del servidor' })
);
// Documentación para obtener todos los cuestionarios
static ApiGetAll = applyDecorators(
ApiOperation({
summary: 'Obtener todos los cuestionarios',
description: 'Retorna una lista de todos los cuestionarios registrados'
}),
ApiResponse({
status: 200,
description: 'Lista de cuestionarios obtenida correctamente',
schema: {
type: 'array',
items: {
type: 'object',
properties: {
id_cuestionario: { type: 'number', example: 1 },
nombre_form: { type: 'string', example: 'Registro para la Feria de la Sexualidad - FES Acatlán' },
descripcion: { type: 'string' },
contador_secciones: { type: 'number', example: 1 },
editable: { type: 'boolean', example: true },
fecha_fin: { type: 'string', format: 'date-time' },
fecha_inicio: { type: 'string', format: 'date-time' },
id_tipo_cuestionario: { type: 'number', example: 1 }
}
}
}
}),
ApiResponse({ status: 500, description: 'Error interno del servidor' })
);
// Documentación para obtener un cuestionario por ID
static ApiGetOne = applyDecorators(
ApiOperation({
summary: 'Obtener un cuestionario por ID',
description: 'Retorna un cuestionario específico por su ID'
}),
ApiParam({
name: 'id',
description: 'ID del cuestionario',
required: true,
type: 'number',
example: 1
}),
ApiResponse({
status: 200,
description: 'Cuestionario obtenido correctamente',
schema: {
type: 'object',
properties: {
id_cuestionario: { type: 'number', example: 1 },
nombre_form: { type: 'string', example: 'Registro para la Feria de la Sexualidad - FES Acatlán' },
descripcion: { type: 'string' },
contador_secciones: { type: 'number', example: 1 },
editable: { type: 'boolean', example: true },
fecha_fin: { type: 'string', format: 'date-time' },
fecha_inicio: { type: 'string', format: 'date-time' },
id_tipo_cuestionario: { type: 'number', example: 1 }
}
}
}),
ApiResponse({ status: 404, description: 'Cuestionario no encontrado' }),
ApiResponse({ status: 500, description: 'Error interno del servidor' })
);
// Documentación para actualizar un cuestionario
static ApiUpdate = applyDecorators(
ApiOperation({
summary: 'Actualizar un cuestionario',
description: 'Actualiza los datos de un cuestionario existente'
}),
ApiParam({
name: 'id',
description: 'ID del cuestionario a actualizar',
required: true,
type: 'number',
example: 1
}),
ApiBody({
description: 'Datos a actualizar del cuestionario',
schema: {
type: 'object',
properties: {
nombre_form: { type: 'string', example: 'Nombre actualizado del formulario' },
descripcion: { type: 'string', example: 'Descripción actualizada' },
fecha_inicio: { type: 'string', format: 'date-time' },
fecha_fin: { type: 'string', format: 'date-time' },
editable: { type: 'boolean', example: true }
}
}
}),
ApiResponse({
status: 200,
description: 'Cuestionario actualizado correctamente',
schema: {
type: 'object',
properties: {
affected: { type: 'number', example: 1 }
}
}
}),
ApiResponse({ status: 400, description: 'Datos de actualización inválidos' }),
ApiResponse({ status: 404, description: 'Cuestionario no encontrado' }),
ApiResponse({ status: 500, description: 'Error interno del servidor' })
);
// Documentación para eliminar un cuestionario
static ApiRemove = applyDecorators(
ApiOperation({
summary: 'Eliminar un cuestionario',
description: 'Elimina permanentemente un cuestionario por su ID'
}),
ApiParam({
name: 'id',
description: 'ID del cuestionario a eliminar',
required: true,
type: 'number',
example: 1
}),
ApiResponse({
status: 200,
description: 'Cuestionario eliminado correctamente',
schema: {
type: 'object',
properties: {
affected: { type: 'number', example: 1 }
}
}
}),
ApiResponse({ status: 404, description: 'Cuestionario no encontrado' }),
ApiResponse({ status: 500, description: 'Error interno del servidor' })
);
}
+11 -27
View File
@@ -1,36 +1,20 @@
import { Module } from '@nestjs/common';
import { CuestionarioService } from './cuestionario.service';
import { CuestionarioController } from './cuestionario.controller';
import { Module, forwardRef } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CuestionarioController } from './cuestionario.controller';
import { CuestionarioService } from './cuestionario.service';
import { Cuestionario } from './entities/cuestionario.entity';
import { Seccion } from '../seccion/entities/seccion.entity';
import { CuestionarioSeccion } from '../cuestionario_seccion/entities/cuestionario_seccion.entity';
import { Pregunta } from '../pregunta/entities/pregunta.entity';
import { SeccionPregunta } from '../seccion_pregunta/entities/seccion_pregunta.entity';
import { Opcion } from '../opcion/entities/opcion.entity';
import { PreguntaOpcion } from '../pregunta_opcion/entities/pregunta_opcion.entity';
import { TipoPregunta } from '../tipo_pregunta/entities/tipo_pregunta.entity';
import { TipoCuestionario } from '../tipo_cuestionario/entities/tipo_cuestionario.entity';
import { Evento } from '../evento/evento.entity';
import { EventoService } from '../evento/evento.service';
import { SeccionModule } from '../seccion/seccion.module';
import { TipoCuestionarioModule } from '../tipo_cuestionario/tipo_cuestionario.module';
@Module({
imports: [
TypeOrmModule.forFeature([
Cuestionario,
Seccion,
CuestionarioSeccion,
Pregunta,
SeccionPregunta,
Opcion,
PreguntaOpcion,
TipoPregunta,
TipoCuestionario,
Evento
])
TypeOrmModule.forFeature([Cuestionario, CuestionarioSeccion]),
forwardRef(() => SeccionModule),
TipoCuestionarioModule,
],
controllers: [CuestionarioController],
providers: [CuestionarioService, EventoService],
exports: [TypeOrmModule, CuestionarioService]
providers: [CuestionarioService],
exports: [CuestionarioService],
})
export class CuestionarioModule {}
export class CuestionarioModule {}
+195 -370
View File
@@ -1,396 +1,221 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, DataSource, In } from 'typeorm';
import { CreateCuestionarioDto } from './dto/create-cuestionario.dto';
import { UpdateCuestionarioDto } from './dto/update-cuestionario.dto';
import { Repository } from 'typeorm';
import { Cuestionario } from './entities/cuestionario.entity';
import { SeccionService } from '../seccion/seccion.service';
import { CreateCuestionarioDto, SeccionEnCuestionarioDto } from './dto/create-cuestionario.dto';
import { UpdateCuestionarioDto } from './dto/update-cuestionario.dto';
import { Seccion } from '../seccion/entities/seccion.entity';
import { CuestionarioSeccion } from '../cuestionario_seccion/entities/cuestionario_seccion.entity';
import { Pregunta } from '../pregunta/entities/pregunta.entity';
import { SeccionPregunta } from '../seccion_pregunta/entities/seccion_pregunta.entity';
import { Opcion } from '../opcion/entities/opcion.entity';
import { PreguntaOpcion } from '../pregunta_opcion/entities/pregunta_opcion.entity';
import { TipoPregunta } from '../tipo_pregunta/entities/tipo_pregunta.entity';
import { TipoCuestionario } from '../tipo_cuestionario/entities/tipo_cuestionario.entity';
import { Evento } from '../evento/evento.entity';
import { EventoService } from '../evento/evento.service';
import { CuestionarioSeccion } from 'src/cuestionario_seccion/entities/cuestionario_seccion.entity';
import { TipoCuestionario } from 'src/tipo_cuestionario/entities/tipo_cuestionario.entity';
@Injectable()
export class CuestionarioService {
async findAllByTipo(tipoCuestionarioId: number): Promise<Cuestionario[]> {
return this.cuestionarioRepository.find({
where: { tipo_cuestionario: { id_tipo_cuestionario: tipoCuestionarioId } },
relations: ['tipo_cuestionario', 'cuestionarioSecciones'],
});
}
obtenerResumen(arg0: number) {
throw new Error('Method not implemented.');
}
constructor(
@InjectRepository(Cuestionario)
private cuestionarioRepository: Repository<Cuestionario>,
@InjectRepository(Seccion)
private seccionRepository: Repository<Seccion>,
private readonly cuestionarioRepository: Repository<Cuestionario>,
@InjectRepository(TipoCuestionario)
private readonly tipoCuestionarioRepository: Repository<TipoCuestionario>,
@InjectRepository(CuestionarioSeccion)
private cuestionarioSeccionRepository: Repository<CuestionarioSeccion>,
@InjectRepository(Pregunta)
private preguntaRepository: Repository<Pregunta>,
@InjectRepository(SeccionPregunta)
private seccionPreguntaRepository: Repository<SeccionPregunta>,
@InjectRepository(Opcion)
private opcionRepository: Repository<Opcion>,
@InjectRepository(PreguntaOpcion)
private preguntaOpcionRepository: Repository<PreguntaOpcion>,
@InjectRepository(TipoPregunta)
private tipoPreguntaRepository: Repository<TipoPregunta>,
@InjectRepository(Evento)
private eventoRepository: Repository<Evento>,
private dataSource: DataSource,
private eventoService: EventoService
private readonly cuestionarioSeccionRepository: Repository<CuestionarioSeccion>,
private readonly seccionService: SeccionService,
) {}
async create(createCuestionarioDto: CreateCuestionarioDto) {
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
// Verificar si se especificó un evento y búscarlo/crearlo
let idEvento: number | undefined = undefined;
if (createCuestionarioDto.evento) {
// Buscar si el evento ya existe
let evento = await this.eventoRepository.findOne({
where: { nombre_evento: createCuestionarioDto.evento }
});
// Si no existe, crearlo
if (!evento) {
const nuevoEvento = {
nombre_evento: createCuestionarioDto.evento,
tipo_evento: 'Predeterminado',
fecha_inicio: createCuestionarioDto.fecha_inicio || new Date(),
fecha_fin: createCuestionarioDto.fecha_fin || new Date(Date.now() + 86400000) // 1 día después si no hay fecha
};
evento = await queryRunner.manager.save(Evento, nuevoEvento);
console.log(`Evento creado: ${evento.nombre_evento}, ID: ${evento.id_evento}`);
} else {
console.log(`Evento encontrado: ${evento.nombre_evento}, ID: ${evento.id_evento}`);
}
idEvento = evento.id_evento;
}
// 1. Crear cuestionario
const cuestionario = new Cuestionario();
cuestionario.nombre_form = createCuestionarioDto.nombre_form;
cuestionario.descripcion = createCuestionarioDto.descripcion;
cuestionario.fecha_inicio = createCuestionarioDto.fecha_inicio ? new Date(createCuestionarioDto.fecha_inicio) : undefined;
cuestionario.fecha_fin = createCuestionarioDto.fecha_fin ? new Date(createCuestionarioDto.fecha_fin) : undefined;
cuestionario.id_tipo_cuestionario = createCuestionarioDto.id_tipo_cuestionario;
cuestionario.contador_secciones = createCuestionarioDto.secciones?.length || 0;
cuestionario.editable = true;
// Asociar el cuestionario con el evento si existe
if (idEvento !== undefined) {
cuestionario.id_evento = idEvento;
}
const savedCuestionario = await queryRunner.manager.save(cuestionario);
// 2. Crear secciones y vincularlas al cuestionario
if (createCuestionarioDto.secciones && createCuestionarioDto.secciones.length > 0) {
for (let i = 0; i < createCuestionarioDto.secciones.length; i++) {
const seccionDto = createCuestionarioDto.secciones[i];
// Crear sección
const seccion = new Seccion();
seccion.titulo = seccionDto.titulo;
seccion.descripcion = seccionDto.descripcion;
seccion.contador_pregunta = seccionDto.preguntas?.length || 0;
const savedSeccion = await queryRunner.manager.save(seccion);
// Vincular sección con cuestionario
const cuestionarioSeccion = new CuestionarioSeccion();
cuestionarioSeccion.id_cuestionario = savedCuestionario.id_cuestionario;
cuestionarioSeccion.id_seccion = savedSeccion.id_seccion;
cuestionarioSeccion.posicion = i + 1;
await queryRunner.manager.save(cuestionarioSeccion);
// 3. Crear preguntas y vincularlas a la sección
if (seccionDto.preguntas && seccionDto.preguntas.length > 0) {
for (let j = 0; j < seccionDto.preguntas.length; j++) {
const preguntaDto = seccionDto.preguntas[j];
// Mapear directamente el tipo de pregunta a su ID correspondiente
let idTipoPregunta = 7; // Valor por defecto (desconocido)
// Mapeamos el tipo de pregunta a su ID correspondiente
if (preguntaDto.tipo === 'Cerrada') {
idTipoPregunta = 1;
} else if (preguntaDto.tipo === 'Abierta') {
idTipoPregunta = 2;
} else if (preguntaDto.tipo === 'Multiple') {
idTipoPregunta = 3;
} else {
console.warn(`Tipo de pregunta no reconocido: ${preguntaDto.tipo}. Se asignará como desconocido.`);
}
const tipoPregunta = { id_tipo: idTipoPregunta, tipo_pregunta: preguntaDto.tipo };
// Crear pregunta
const pregunta = new Pregunta();
pregunta.pregunta = preguntaDto.titulo;
pregunta.obligatoria = preguntaDto.obligatoria || false;
pregunta.id_tipo_pregunta = tipoPregunta.id_tipo;
pregunta.id_opcion_dependiente = preguntaDto.id_opcion_dependiente || undefined;
pregunta.contador_opcion = preguntaDto.opciones?.length || 0;
pregunta.validacion = preguntaDto.validacion || undefined;
const savedPregunta = await queryRunner.manager.save(pregunta);
// Vincular pregunta con sección
const seccionPregunta = new SeccionPregunta();
seccionPregunta.id_seccion = savedSeccion.id_seccion;
seccionPregunta.id_pregunta = savedPregunta.id_pregunta;
seccionPregunta.posicion = j + 1;
await queryRunner.manager.save(seccionPregunta);
// 4. Crear opciones para preguntas de tipo multiple/radio
if (preguntaDto.opciones && preguntaDto.opciones.length > 0) {
for (let k = 0; k < preguntaDto.opciones.length; k++) {
const opcionDto = preguntaDto.opciones[k];
// Crear opción
const opcion = new Opcion();
opcion.opcion = opcionDto.valor;
const savedOpcion = await queryRunner.manager.save(opcion);
// Vincular opción con pregunta
const preguntaOpcion = new PreguntaOpcion();
preguntaOpcion.id_pregunta = savedPregunta.id_pregunta;
// Utilizamos la opción como objeto, no como ID
preguntaOpcion.opcion = savedOpcion;
preguntaOpcion.posicion = k + 1;
await queryRunner.manager.save(preguntaOpcion);
}
}
}
}
}
}
await queryRunner.commitTransaction();
return {
success: true,
cuestionario: savedCuestionario,
};
} catch (error) {
await queryRunner.rollbackTransaction();
throw error;
} finally {
await queryRunner.release();
}
}
findAll() {
return this.cuestionarioRepository.find();
}
async findOne(id: number) {
const cuestionario = await this.cuestionarioRepository.findOne({
where: { id_cuestionario: id }
// Verificar tipo de cuestionario
const tipoCuestionario = await this.tipoCuestionarioRepository.findOne({
where: { id_tipo_cuestionario: createCuestionarioDto.id_tipo_cuestionario },
});
if (!cuestionario) {
throw new NotFoundException(`Cuestionario con ID ${id} no encontrado`);
if (!tipoCuestionario) {
throw new NotFoundException('Tipo de cuestionario no encontrado');
}
return cuestionario;
// Crear cuestionario base
const cuestionario = this.cuestionarioRepository.create({
nombre_form: createCuestionarioDto.nombre_form,
descripcion: createCuestionarioDto.descripcion,
editable: createCuestionarioDto.editable ?? true,
fecha_inicio: createCuestionarioDto.fecha_inicio,
fecha_fin: createCuestionarioDto.fecha_fin,
tipo_cuestionario: tipoCuestionario,
contador_secciones: createCuestionarioDto.secciones?.length || 0,
});
const cuestionarioGuardado = await this.cuestionarioRepository.save(cuestionario);
// Procesar secciones si existen
if (createCuestionarioDto.secciones && createCuestionarioDto.secciones.length > 0) {
await this.agregarSeccionesACuestionario(
cuestionarioGuardado.id_cuestionario,
createCuestionarioDto.secciones,
);
}
return this.findOneWithRelations(cuestionarioGuardado.id_cuestionario);
}
private async agregarSeccionesACuestionario(
cuestionarioId: number,
secciones: SeccionEnCuestionarioDto[],
) {
await Promise.all(
secciones.map(async (seccionDto) => {
// Crear la sección usando el servicio existente
const seccionCreada = await this.seccionService.create(seccionDto.seccion);
async findCompleto(id: number) {
// Obtener el cuestionario básico
const cuestionario = await this.cuestionarioRepository.findOne({
where: { id_cuestionario: id },
relations: ['evento'] // Incluir la relación con el evento
});
if (!cuestionario) {
throw new NotFoundException(`Cuestionario con ID ${id} no encontrado`);
}
// Para fines de demostración, usar valores hardcodeados
// para el tipo de cuestionario según get_formulario_feria.ts
const tipoCuestionario = {
id_tipo_cuestionario: 1,
tipo_cuestionario: 'Encuesta'
};
// Mapeador de ID de tipo de pregunta a nombres conocidos
const tiposPreguntaMap = {
1: { id_tipo: 1, tipo_pregunta: 'Cerrada' },
2: { id_tipo: 2, tipo_pregunta: 'Abierta' },
3: { id_tipo: 3, tipo_pregunta: 'Multiple' }
};
// Obtener las relaciones cuestionario-seccion
const cuestionarioSecciones = await this.cuestionarioSeccionRepository.find({
where: { id_cuestionario: id },
order: { posicion: 'ASC' }
});
// Obtener todas las secciones
const seccionIds = cuestionarioSecciones.map(cs => cs.id_seccion);
const secciones = await this.seccionRepository.find({
where: { id_seccion: In(seccionIds) }
});
const seccionesFormateadas = await Promise.all(
cuestionarioSecciones.map(async (cs) => {
const seccion = secciones.find(s => s.id_seccion === cs.id_seccion);
if (!seccion) return null;
// Obtener relaciones sección-pregunta
const seccionPreguntas = await this.seccionPreguntaRepository.find({
where: { id_seccion: seccion.id_seccion },
order: { posicion: 'ASC' }
// Crear la relación cuestionario-sección
const cuestionarioSeccion = this.cuestionarioSeccionRepository.create({
posicion: seccionDto.posicion,
cuestionario: { id_cuestionario: cuestionarioId },
seccion: { id_seccion: seccionCreada?.id_seccion },
});
// Obtener preguntas
const preguntaIds = seccionPreguntas.map(sp => sp.id_pregunta);
const preguntas = await this.preguntaRepository.find({
where: { id_pregunta: In(preguntaIds) }
});
// Formatear cada pregunta según el formato requerido
const preguntasFormateadas = await Promise.all(
seccionPreguntas.map(async (sp) => {
const pregunta = preguntas.find(p => p.id_pregunta === sp.id_pregunta);
if (!pregunta) return null;
// Usar el mapeo de tipos de pregunta definido arriba
const tipoPregunta = tiposPreguntaMap[pregunta.id_tipo_pregunta] ||
{ id_tipo: pregunta.id_tipo_pregunta, tipo_pregunta: 'Desconocido' };
// Obtener opciones para preguntas de tipo multiple/radio
const preguntaOpciones = await this.preguntaOpcionRepository.find({
where: { id_pregunta: pregunta.id_pregunta },
relations: ['opcion'],
order: { posicion: 'ASC' }
});
// Formatear las opciones según el formato requerido
const opcionesFormateadas = preguntaOpciones.map(po => ({
id_pregunta_opcion: po.id_pregunta_opcion,
posicion: po.posicion,
id_opcion: po.opcion.id_opcion,
opcion: {
id_opcion: po.opcion.id_opcion,
opcion: po.opcion.opcion
}
}));
// Construir objeto de pregunta según el formato requerido
return {
id_seccion_pregunta: sp.id_seccion_pregunta,
posicion: sp.posicion,
pregunta: {
id_pregunta: pregunta.id_pregunta,
pregunta: pregunta.pregunta,
contador_opcion: pregunta.contador_opcion,
obligatoria: pregunta.obligatoria,
id_tipo_pregunta: pregunta.id_tipo_pregunta,
id_opcion_dependiente: pregunta.id_opcion_dependiente,
validacion: pregunta.validacion,
tipo_pregunta: {
id_tipo: tipoPregunta.id_tipo,
tipo_pregunta: tipoPregunta.tipo_pregunta
},
opciones: opcionesFormateadas
}
};
})
).then(results => results.filter(p => p !== null));
// Construir objeto de sección según el formato requerido
return {
id_cuestionario_seccion: cs.id_cuestionario_seccion,
posicion: cs.posicion,
seccion: {
id_seccion: seccion.id_seccion,
contador_pregunta: seccion.contador_pregunta,
descripcion: seccion.descripcion,
titulo: seccion.titulo
},
preguntas: preguntasFormateadas
};
await this.cuestionarioSeccionRepository.save(cuestionarioSeccion);
})
).then(results => results.filter(s => s !== null));
// Construir objeto final respetando el formato de get_formulario_feria.ts
return {
tipo_cuestionario: {
id_tipo_cuestionario: tipoCuestionario.id_tipo_cuestionario,
tipo_cuestionario: tipoCuestionario.tipo_cuestionario
},
evento: cuestionario.evento ? {
id_evento: cuestionario.evento.id_evento,
nombre_evento: cuestionario.evento.nombre_evento,
tipo_evento: cuestionario.evento.tipo_evento,
fecha_inicio: cuestionario.evento.fecha_inicio ? cuestionario.evento.fecha_inicio.toISOString() : null,
fecha_fin: cuestionario.evento.fecha_fin ? cuestionario.evento.fecha_fin.toISOString() : null
} : null,
cuestionario: {
id_cuestionario: cuestionario.id_cuestionario,
nombre_form: cuestionario.nombre_form,
contador_secciones: cuestionario.contador_secciones,
descripcion: cuestionario.descripcion,
editable: cuestionario.editable,
fecha_inicio: cuestionario.fecha_inicio ? cuestionario.fecha_inicio.toISOString() : null,
fecha_fin: cuestionario.fecha_fin ? cuestionario.fecha_fin.toISOString() : null,
id_cuestionario_original: cuestionario.id_cuestionario_original,
id_tipo_cuestionario: cuestionario.id_tipo_cuestionario,
id_evento: cuestionario.id_evento || null,
secciones: seccionesFormateadas
}
};
);
}
async findOneWithRelations(id: number) {
return this.cuestionarioRepository.findOne({
where: { id_cuestionario: id },
relations: [
'tipo_cuestionario',
'cuestionarioSecciones',
'cuestionarioSecciones.seccion',
'cuestionarioSecciones.seccion.seccionPreguntas',
'cuestionarioSecciones.seccion.seccionPreguntas.pregunta',
'cuestionarioSecciones.seccion.seccionPreguntas.pregunta.tipo_pregunta',
'cuestionarioSecciones.seccion.seccionPreguntas.pregunta.opciones',
'cuestionarioSecciones.seccion.seccionPreguntas.pregunta.opciones.opcion',
],
});
}
async findAll() {
return this.cuestionarioRepository.find({
relations: ['tipo_cuestionario'],
});
}
async update(id: number, updateCuestionarioDto: UpdateCuestionarioDto) {
// Si el DTO incluye el campo "evento" (string), debemos procesarlo aparte
if (updateCuestionarioDto.evento) {
// Buscar o crear evento
let evento = await this.eventoRepository.findOne({
where: { nombre_evento: updateCuestionarioDto.evento }
});
if (!evento) {
// Crear el evento
const nuevoEvento = {
nombre_evento: updateCuestionarioDto.evento,
tipo_evento: 'Predeterminado',
fecha_inicio: new Date(),
fecha_fin: new Date(Date.now() + 86400000)
};
evento = await this.eventoRepository.save(nuevoEvento);
}
// Asignar el ID del evento al DTO de actualización
updateCuestionarioDto.id_evento = evento.id_evento;
// Eliminar la propiedad evento para evitar conflictos
delete updateCuestionarioDto.evento;
const cuestionario = await this.cuestionarioRepository.findOne({
where: { id_cuestionario: id },
});
if (!cuestionario) {
throw new NotFoundException(`Cuestionario con ID ${id} no encontrado`);
}
// Preparar los datos para la actualización
const updateData: any = { ...updateCuestionarioDto };
return this.cuestionarioRepository.update(id, updateData);
// Actualizar campos básicos
if (updateCuestionarioDto.nombre_form !== undefined) {
cuestionario.nombre_form = updateCuestionarioDto.nombre_form;
}
if (updateCuestionarioDto.descripcion !== undefined) {
cuestionario.descripcion = updateCuestionarioDto.descripcion;
}
if (updateCuestionarioDto.editable !== undefined) {
cuestionario.editable = updateCuestionarioDto.editable;
}
if (updateCuestionarioDto.fecha_inicio !== undefined) {
cuestionario.fecha_inicio = updateCuestionarioDto.fecha_inicio;
}
if (updateCuestionarioDto.fecha_fin !== undefined) {
cuestionario.fecha_fin = updateCuestionarioDto.fecha_fin;
}
return this.cuestionarioRepository.save(cuestionario);
}
remove(id: number) {
return this.cuestionarioRepository.delete(id);
async remove(id: number) {
// Eliminar relaciones cuestionario-sección primero
await this.cuestionarioSeccionRepository.delete({
cuestionario: { id_cuestionario: id },
});
// Luego eliminar el cuestionario
const result = await this.cuestionarioRepository.delete(id);
if (result.affected === 0) {
throw new NotFoundException(`Cuestionario con ID ${id} no encontrado`);
}
return { affected: result.affected };
}
}
async agregarSeccionExistente(
cuestionarioId: number,
seccionId: number,
posicion: number,
) {
// Verificar que existan ambos elementos
const cuestionario = await this.cuestionarioRepository.findOne({
where: { id_cuestionario: cuestionarioId },
});
if (!cuestionario) {
throw new NotFoundException(`Cuestionario con ID ${cuestionarioId} no encontrado`);
}
const seccion = await this.seccionService.findOneWithRelations(seccionId);
if (!seccion) {
throw new NotFoundException(`Sección con ID ${seccionId} no encontrada`);
}
// Crear la relación
const cuestionarioSeccion = this.cuestionarioSeccionRepository.create({
posicion,
cuestionario: { id_cuestionario: cuestionarioId },
seccion: { id_seccion: seccionId },
});
await this.cuestionarioSeccionRepository.save(cuestionarioSeccion);
// Actualizar contador de secciones
await this.cuestionarioRepository.increment(
{ id_cuestionario: cuestionarioId },
'contador_secciones',
1,
);
return this.findOneWithRelations(cuestionarioId);
}
async eliminarSeccionDeCuestionario(cuestionarioId: number, seccionId: number) {
const relacion = await this.cuestionarioSeccionRepository.findOne({
where: {
cuestionario: { id_cuestionario: cuestionarioId },
seccion: { id_seccion: seccionId },
},
});
if (!relacion) {
throw new NotFoundException('La sección no está asociada a este cuestionario');
}
await this.cuestionarioSeccionRepository.remove(relacion);
// Actualizar contador de secciones
await this.cuestionarioRepository.decrement(
{ id_cuestionario: cuestionarioId },
'contador_secciones',
1,
);
return { success: true };
}
}
+23 -46
View File
@@ -1,69 +1,46 @@
import { CreateSeccionDto } from '../../seccion/dto/create-seccion.dto';
import { IsArray, IsDateString, IsNotEmpty, IsNumber, IsOptional, IsString, ValidateNested } from 'class-validator';
import { IsNotEmpty, IsString, IsBoolean, IsDateString, IsNumber, IsOptional, IsArray, ValidateNested } from 'class-validator';
import { Type } from 'class-transformer';
import { ApiProperty } from '@nestjs/swagger';
import { CreateSeccionDto } from 'src/seccion/dto/create-seccion.dto';
export class CreateCuestionarioDto {
@ApiProperty({
description: 'Nombre del formulario',
example: 'Registro para la Feria de la Sexualidad - FES Acatlán'
})
@IsNotEmpty()
@IsString()
nombre_form: string;
@ApiProperty({
description: 'Descripción del formulario',
example: 'Formulario de registro para la feria...',
required: false
})
@IsOptional()
@IsString()
descripcion?: string;
@ApiProperty({
description: 'Fecha de inicio',
example: '2025-03-26T00:00:00',
required: false
})
@IsOptional()
@IsDateString()
fecha_inicio?: Date;
@IsBoolean()
editable?: boolean;
@ApiProperty({
description: 'Fecha de fin',
example: '2025-03-31T23:59:59',
required: false
})
@IsOptional()
@IsNotEmpty()
@IsDateString()
fecha_fin?: Date;
fecha_inicio: Date;
@IsNotEmpty()
@IsDateString()
fecha_fin: Date;
@ApiProperty({
description: 'ID del tipo de cuestionario',
example: 1
})
@IsNotEmpty()
@IsNumber()
id_tipo_cuestionario: number;
@ApiProperty({
description: 'Nombre del evento asociado',
example: 'Feria de la Sexualidad',
required: false
})
@IsOptional()
@IsString()
evento?: string;
@ApiProperty({
description: 'Secciones del cuestionario',
type: [CreateSeccionDto],
required: false
})
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => CreateSeccionDto)
secciones?: CreateSeccionDto[];
@Type(() => SeccionEnCuestionarioDto)
secciones?: SeccionEnCuestionarioDto[];
}
export class SeccionEnCuestionarioDto {
@IsNotEmpty()
@IsNumber()
posicion: number;
@IsNotEmpty()
@ValidateNested()
@Type(() => CreateSeccionDto)
seccion: CreateSeccionDto;
}
-220
View File
@@ -1,220 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
import { TipoPreguntaEnum } from '../../pregunta/dto/create-pregunta.dto';
// Ejemplo de respuesta de cuestionario completo
export const FormularioFeriaSchema = {
tipo_cuestionario: {
id_tipo_cuestionario: 1,
tipo_cuestionario: 'Encuesta'
},
cuestionario: {
id_cuestionario: 10,
nombre_form: 'Registro para la Feria de la Sexualidad - Comunidad FES Acatlán',
contador_secciones: 2,
descripcion: 'La Feria de la Sexualidad es un espacio seguro e informativo dirigido a toda la comunidad universitaria. Este registro nos ayudará a organizar mejor las actividades, talleres y charlas, así como conocer tus intereses. Por favor, completa el siguiente formulario para confirmar tu participación.',
editable: true,
fecha_inicio: '2025-03-26T00:00:00',
fecha_fin: '2025-03-31T23:59:59',
id_cuestionario_original: null,
id_tipo_cuestionario: 1,
secciones: [
{
id_cuestionario_seccion: 1,
posicion: 1,
seccion: {
id_seccion: 100,
contador_pregunta: 3,
descripcion: 'Información básica',
titulo: 'Datos personales'
},
preguntas: [
{
id_seccion_pregunta: 1000,
posicion: 1,
pregunta: {
id_pregunta: 101,
pregunta: '¿Eres parte de la comunidad de la FES Acatlán?',
contador_opcion: 2,
obligatoria: true,
id_tipo_pregunta: 1,
id_opcion_dependiente: null,
tipo_pregunta: {
id_tipo: 1,
tipo_pregunta: 'Cerrada'
},
opciones: [
{
id_pregunta_opcion: 50001,
posicion: 1,
id_opcion: 90001,
opcion: {
id_opcion: 90001,
opcion: 'Sí'
}
},
{
id_pregunta_opcion: 50002,
posicion: 2,
id_opcion: 90002,
opcion: {
id_opcion: 90002,
opcion: 'No'
}
}
]
}
},
{
id_seccion_pregunta: 1005,
posicion: 2,
pregunta: {
id_pregunta: 109,
pregunta: 'Correo electrónico',
contador_opcion: 0,
obligatoria: true,
id_tipo_pregunta: 2,
id_opcion_dependiente: null,
limite: 250, // Límite de caracteres para respuestas de texto
validacion: 'correo', // Validación de formato de correo electrónico
tipo_pregunta: {
id_tipo: 2,
tipo_pregunta: 'Abierta'
},
opciones: []
}
},
{
id_seccion_pregunta: 1001,
posicion: 3,
pregunta: {
id_pregunta: 102,
pregunta: 'Número de cuenta (solo para estudiantes de la FES Acatlán)',
contador_opcion: 0,
obligatoria: false,
id_tipo_pregunta: 2,
id_opcion_dependiente: 90001, // Dependiente de la respuesta "Sí" en la pregunta anterior
limite: 250, // Límite de caracteres para respuestas de texto
validacion: 'cuenta_alumno', // Validación de formato de número de cuenta
tipo_pregunta: {
id_tipo: 2,
tipo_pregunta: 'Abierta'
},
opciones: []
}
}
]
},
{
id_cuestionario_seccion: 2,
posicion: 2,
seccion: {
id_seccion: 101,
contador_pregunta: 2,
descripcion: 'Información sobre tus intereses',
titulo: 'Intereses'
},
preguntas: [
{
id_seccion_pregunta: 1003,
posicion: 1,
pregunta: {
id_pregunta: 106,
pregunta: '¿Qué temas te interesan para la Feria de la Sexualidad?',
contador_opcion: 5,
obligatoria: true,
id_tipo_pregunta: 3,
id_opcion_dependiente: null,
tipo_pregunta: {
id_tipo: 3,
tipo_pregunta: 'Multiple'
},
opciones: [
{
id_pregunta_opcion: 50010,
posicion: 1,
id_opcion: 90010,
opcion: {
id_opcion: 90010,
opcion: 'Educación sexual'
}
},
{
id_pregunta_opcion: 50011,
posicion: 2,
id_opcion: 90011,
opcion: {
id_opcion: 90011,
opcion: 'Prevención de ITS'
}
},
{
id_pregunta_opcion: 50012,
posicion: 3,
id_opcion: 90012,
opcion: {
id_opcion: 90012,
opcion: 'Salud reproductiva'
}
},
{
id_pregunta_opcion: 50013,
posicion: 4,
id_opcion: 90013,
opcion: {
id_opcion: 90013,
opcion: 'Diversidad sexual y de género'
}
},
{
id_pregunta_opcion: 50014,
posicion: 5,
id_opcion: 90014,
opcion: {
id_opcion: 90014,
opcion: 'Relaciones saludables'
}
}
]
}
},
{
id_seccion_pregunta: 1004,
posicion: 2,
pregunta: {
id_pregunta: 107,
pregunta: '¿Algún otro tema que te gustaría que se abordara?',
contador_opcion: 0,
obligatoria: false,
id_tipo_pregunta: 2,
id_opcion_dependiente: null,
limite: 250, // Límite de caracteres para respuestas de texto
tipo_pregunta: {
id_tipo: 2,
tipo_pregunta: 'Abierta'
},
opciones: []
}
}
]
}
]
}
};
// Ejemplo de tipo FormularioDto para usar en la API
export class FormularioDto {
@ApiProperty({
description: 'Tipo de cuestionario',
example: {
id_tipo_cuestionario: 1,
tipo_cuestionario: 'Encuesta'
}
})
tipo_cuestionario: any;
@ApiProperty({
description: 'Información completa del cuestionario',
example: FormularioFeriaSchema.cuestionario
})
cuestionario: any;
}
@@ -0,0 +1,22 @@
import { IsNotEmpty, IsNumber, Min } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class SeccionExistenteCuestionarioDto {
@ApiProperty({
example: 1,
description: 'Posición de la sección dentro del cuestionario',
})
@IsNotEmpty()
@IsNumber()
@Min(1)
posicion: number;
@ApiProperty({
example: 5,
description: 'ID de la sección existente a agregar',
})
@IsNotEmpty()
@IsNumber()
@Min(1)
id_seccion: number;
}
+21 -13
View File
@@ -1,15 +1,23 @@
import { PartialType } from '@nestjs/mapped-types';
import { CreateCuestionarioDto } from './create-cuestionario.dto';
import { IsNumber, IsOptional } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { IsOptional, IsString, IsBoolean, IsDateString } from 'class-validator';
export class UpdateCuestionarioDto extends PartialType(CreateCuestionarioDto) {
@ApiProperty({
description: 'ID del evento asociado',
example: 1,
required: false
})
export class UpdateCuestionarioDto {
@IsOptional()
@IsNumber()
id_evento?: number;
}
@IsString()
nombre_form?: string;
@IsOptional()
@IsString()
descripcion?: string;
@IsOptional()
@IsBoolean()
editable?: boolean;
@IsOptional()
@IsDateString()
fecha_inicio?: Date;
@IsOptional()
@IsDateString()
fecha_fin?: Date;
}
@@ -1,5 +1,5 @@
import { CuestionarioRespondido } from 'src/cuestionario_respondido/entities/cuestionario_respondido.entity';
import { CuestionarioSeccion } from 'src/cuestionario_seccion/entities/cuestionario_seccion.entity';
import { Evento } from 'src/evento/evento.entity';
import { TipoCuestionario } from 'src/tipo_cuestionario/entities/tipo_cuestionario.entity';
import { Entity, PrimaryGeneratedColumn, Column, OneToMany, ManyToMany, ManyToOne, JoinColumn } from 'typeorm';
@@ -17,34 +17,33 @@ export class Cuestionario {
contador_secciones: number;
@Column({ type: 'text', nullable: true })
descripcion?: string;
descripcion: string;
@Column({ type: 'boolean', default: true })
editable: boolean;
@Column({ type: 'datetime', nullable: true })
fecha_fin?: Date;
fecha_fin: Date;
@Column({ type: 'datetime', nullable: true })
fecha_inicio?: Date;
fecha_inicio: Date;
@OneToMany(() => CuestionarioRespondido, cuestionarioRespondido => cuestionarioRespondido.cuestionario)
cuestionariosRespondidos: CuestionarioRespondido[];
@OneToMany(() => CuestionarioSeccion,(cuestionario_seccion) => cuestionario_seccion.id_cuestionario)
@OneToMany(() => CuestionarioSeccion,(cuestionario_seccion) => cuestionario_seccion.seccion)
@JoinColumn()
cuestionarioSeccion:CuestionarioSeccion[]
@Column({ type: 'int', nullable: true })
id_cuestionario_original: number;
@OneToMany(()=> Cuestionario, (cuestionario) => cuestionario.cuestionario_original)
cuestionarioOriginal: Cuestionario[];
@Column({ type: 'int' })
id_tipo_cuestionario: number;
@ManyToOne(() => TipoCuestionario, (tipo) => tipo.cuestionarios)
@JoinColumn({ name: 'id_tipo_cuestionario' }) // Clave foránea en la base de datos
tipo_cuestionario: TipoCuestionario;
@ManyToOne(()=> Cuestionario)
cuestionarioOriginal: Cuestionario;
@Column({ type: 'int', nullable: true })
id_evento: number;
@ManyToOne(() => Evento)
@JoinColumn({ name: 'id_evento' })
evento: Evento;
@ManyToOne(()=> Cuestionario, (cuestionario) => cuestionario.cuestionarioOriginal)
@JoinColumn({ name: 'id_cuestionario_original' })
cuestionario_original: Cuestionario;
}
@@ -1,13 +1,8 @@
import { Controller, Get, Post, Body, Patch, Param, Delete, Res } from '@nestjs/common';
import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common';
import { CuestionarioRespondidoService } from './cuestionario_respondido.service';
import { CreateCuestionarioRespondidoDto } from './dto/create-cuestionario_respondido.dto';
import { UpdateCuestionarioRespondidoDto } from './dto/update-cuestionario_respondido.dto';
import { SubmitRespuestasDto } from './dto/submit-respuestas.dto';
import { ApiBody, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { CuestionarioRespondidoApiDocumentation } from './cuestionario_respondido.documentation';
import { Response } from 'express';
@ApiTags('Cuestionario Respondido')
@Controller('cuestionario-respondido')
export class CuestionarioRespondidoController {
constructor(private readonly cuestionarioRespondidoService: CuestionarioRespondidoService) {}
@@ -17,36 +12,11 @@ export class CuestionarioRespondidoController {
return this.cuestionarioRespondidoService.create(createCuestionarioRespondidoDto);
}
@Post('submit')
@CuestionarioRespondidoApiDocumentation.ApiSubmitRespuestas
@ApiBody({
type: SubmitRespuestasDto,
examples: {
comunidad: CuestionarioRespondidoApiDocumentation.ApiSubmitRespuestasExamples.communityExample,
externos: CuestionarioRespondidoApiDocumentation.ApiSubmitRespuestasExamples.externalExample
}
})
@CuestionarioRespondidoApiDocumentation.ApiSubmitResponse201
@CuestionarioRespondidoApiDocumentation.ApiSubmitResponse400
@CuestionarioRespondidoApiDocumentation.ApiSubmitResponse404
submitRespuestas(@Body() submitRespuestasDto: SubmitRespuestasDto) {
return this.cuestionarioRespondidoService.submitRespuestas(submitRespuestasDto);
}
@Get()
findAll() {
return this.cuestionarioRespondidoService.findAll();
}
@Get('/reporte-respuestas/:id')
@CuestionarioRespondidoApiDocumentation.ApiReporteRespuestas
async descargarReporteRespuestas(@Param('id') id: string, @Res() res: Response) {
const reporte = await this.cuestionarioRespondidoService.generarReporteRespuestas(+id);
res.header('Content-Type', 'text/csv');
res.header('Content-Disposition', `attachment; filename="reporte-cuestionario-${id}.csv"`);
return res.send(reporte);
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.cuestionarioRespondidoService.findOne(+id);
@@ -1,125 +0,0 @@
import { ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
import { applyDecorators } from '@nestjs/common';
export class CuestionarioRespondidoApiDocumentation {
static ApiController = ApiTags('Cuestionario Respondido');
static ApiSubmitRespuestas = ApiOperation({
summary: 'Enviar respuestas a un cuestionario',
description: `Registra las respuestas de un participante a un cuestionario. Crea un registro de participante si no existe uno con ese correo.
## Tipos de respuestas:
- **Preguntas abiertas**: Enviar el texto de la respuesta como string (máximo 250 caracteres)
- **Preguntas cerradas**: Enviar el ID de la opción como número
- **Preguntas de selección múltiple**: Enviar un array de IDs de opciones como [número, número, ...]
### Importante
Para preguntas cerradas y múltiples, se deben enviar los IDs de las opciones, NO los textos.
Esto evita conflictos cuando existen opciones con el mismo texto pero diferentes IDs.
Todas las respuestas de texto y el correo electrónico están limitados a 250 caracteres.`
});
static ApiSubmitResponse201 = ApiResponse({
status: 201,
description: 'Respuestas guardadas correctamente',
schema: {
properties: {
success: { type: 'boolean', example: true },
message: { type: 'string', example: 'Respuestas guardadas correctamente' },
cuestionarioRespondidoId: { type: 'number', example: 1 }
}
}
});
static ApiSubmitResponse400 = ApiResponse({
status: 400,
description: 'Datos inválidos en la solicitud',
schema: {
properties: {
statusCode: { type: 'number', example: 400 },
message: { type: 'string', example: 'La opción con ID 999 no está asociada a la pregunta 101' },
error: { type: 'string', example: 'Bad Request' }
}
}
});
static ApiSubmitResponse404 = ApiResponse({
status: 404,
description: 'Cuestionario o pregunta no encontrada',
schema: {
properties: {
statusCode: { type: 'number', example: 404 },
message: { type: 'string', example: 'Cuestionario con ID 999 no encontrado' },
error: { type: 'string', example: 'Not Found' }
}
}
});
static get ApiReporteRespuestas() {
return applyDecorators(
ApiOperation({
summary: 'Descargar reporte de respuestas de un cuestionario',
description: `
Este endpoint permite descargar un archivo CSV con todas las respuestas de un cuestionario.
El reporte incluye información del participante, evento asociado, y el contenido de las respuestas.
Se generará un archivo CSV con los siguientes campos:
- ID Respuesta
- Fecha Respuesta
- ID Participante
- Correo Participante
- Evento
- Cuestionario
- Pregunta
- Respuesta
`
}),
ApiResponse({
status: 200,
description: 'Reporte de respuestas descargado exitosamente como archivo CSV'
}),
ApiResponse({
status: 404,
description: 'Cuestionario no encontrado'
})
);
}
static ApiSubmitRespuestasExamples = {
communityExample: {
summary: 'Ejemplo de respuesta para un miembro de la comunidad',
value: {
id_formulario: 1,
correo: 'estudiante@acatlan.unam.mx',
respuestas: [
{ id_pregunta: 101, valor: 3 }, // Opción "Si" (usando el ID de la opción)
{ id_pregunta: 102, valor: 'estudiante@acatlan.unam.mx' }, // Correo electrónico
{ id_pregunta: 103, valor: '123456789' }, // Numero de cuenta
{ id_pregunta: 106, valor: 7 } // Opción "Prefiero no decirlo" (usando el ID de la opción)
],
fecha_envio: '2025-04-02T13:45:00'
},
description: 'Datos de un estudiante de la FES Acatlán con respuestas abiertas y opciones por ID.'
},
externalExample: {
summary: 'Ejemplo de respuesta para un participante externo con selección múltiple',
value: {
id_formulario: 1,
correo: 'externo@ejemplo.com',
respuestas: [
{ id_pregunta: 101, valor: 4 }, // Opción "No" (usando el ID de la opción)
{ id_pregunta: 102, valor: 'externo@ejemplo.com' }, // Correo electrónico
{ id_pregunta: 103, valor: 'Juan Carlos' }, // Nombre(s)
{ id_pregunta: 104, valor: 'Ramírez López' }, // Apellidos
{ id_pregunta: 105, valor: [2, 5, 8] }, // Selección múltiple (array de IDs de opciones)
{ id_pregunta: 106, valor: 7 }, // Opción "Prefiero no decirlo" (usando el ID de la opción)
{ id_pregunta: 107, valor: 'UAM Azcapotzalco' } // Institución de procedencia
],
fecha_envio: '2025-04-02T13:45:00'
},
description: 'Datos de un participante externo con respuestas abiertas, opciones por ID y selección múltiple.'
}
};
}
@@ -1,37 +1,14 @@
import { Module } from '@nestjs/common';
import { CuestionarioRespondidoService } from './cuestionario_respondido.service';
import { CuestionarioRespondidoController } from './cuestionario_respondido.controller';
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 { Participante } from '../participante/participante.entity';
import { Cuestionario } from '../cuestionario/entities/cuestionario.entity';
import { Pregunta } from '../pregunta/entities/pregunta.entity';
import { RespuestaParticipanteAbierta } from '../respuesta_participante_abierta/entities/respuesta_participante_abierta.entity';
import { RespuestaParticipanteCerrada } from '../respuesta_participante_cerrada/entities/respuesta_participante_cerrada.entity';
import { PreguntaOpcion } from '../pregunta_opcion/entities/pregunta_opcion.entity';
import { Opcion } from '../opcion/entities/opcion.entity';
import { ValidacionesModule } from '../validaciones/validaciones.module';
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [
TypeOrmModule.forFeature([
CuestionarioRespondido,
Participante,
Cuestionario,
Pregunta,
RespuestaParticipanteAbierta,
RespuestaParticipanteCerrada,
PreguntaOpcion,
Opcion
]),
CuestionarioModule,
ParticipanteModule,
ValidacionesModule
],
controllers: [CuestionarioRespondidoController],
providers: [CuestionarioRespondidoService],
exports: [TypeOrmModule]
imports: [TypeOrmModule.forFeature([CuestionarioRespondido])],
controllers: [CuestionarioRespondidoController],
providers: [CuestionarioRespondidoService],
exports:[CuestionarioRespondidoService, TypeOrmModule]
})
export class CuestionarioRespondidoModule {}
@@ -1,384 +1,19 @@
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, DataSource } from 'typeorm';
import { Injectable } from '@nestjs/common';
import { CreateCuestionarioRespondidoDto } from './dto/create-cuestionario_respondido.dto';
import { UpdateCuestionarioRespondidoDto } from './dto/update-cuestionario_respondido.dto';
import { SubmitRespuestasDto } from './dto/submit-respuestas.dto';
import { CuestionarioRespondido } from './entities/cuestionario_respondido.entity';
import { Participante } from '../participante/participante.entity';
import { Cuestionario } from '../cuestionario/entities/cuestionario.entity';
import { Pregunta } from '../pregunta/entities/pregunta.entity';
import { RespuestaParticipanteAbierta } from '../respuesta_participante_abierta/entities/respuesta_participante_abierta.entity';
import { RespuestaParticipanteCerrada } from '../respuesta_participante_cerrada/entities/respuesta_participante_cerrada.entity';
import { PreguntaOpcion } from '../pregunta_opcion/entities/pregunta_opcion.entity';
import { Opcion } from '../opcion/entities/opcion.entity';
import { ValidadorRespuestasService } from '../validaciones/validador-respuestas.service';
import axios from 'axios';
import * as QRCode from 'qrcode';
@Injectable()
export class CuestionarioRespondidoService {
constructor(
@InjectRepository(CuestionarioRespondido)
private cuestionarioRespondidoRepository: Repository<CuestionarioRespondido>,
@InjectRepository(Participante)
private participanteRepository: Repository<Participante>,
@InjectRepository(Cuestionario)
private cuestionarioRepository: Repository<Cuestionario>,
@InjectRepository(Pregunta)
private preguntaRepository: Repository<Pregunta>,
@InjectRepository(RespuestaParticipanteAbierta)
private respuestaAbiertaRepository: Repository<RespuestaParticipanteAbierta>,
@InjectRepository(RespuestaParticipanteCerrada)
private respuestaCerradaRepository: Repository<RespuestaParticipanteCerrada>,
@InjectRepository(PreguntaOpcion)
private preguntaOpcionRepository: Repository<PreguntaOpcion>,
@InjectRepository(Opcion)
private opcionRepository: Repository<Opcion>,
private dataSource: DataSource,
private validadorRespuestasService: ValidadorRespuestasService,
) {}
create(createCuestionarioRespondidoDto: CreateCuestionarioRespondidoDto) {
return 'This action adds a new cuestionarioRespondido';
}
async submitRespuestas(submitDto: SubmitRespuestasDto) {
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
// 1. Verificar que el cuestionario existe
const cuestionario = await this.cuestionarioRepository.findOne({
where: { id_cuestionario: submitDto.id_formulario },
relations: ['evento']
});
if (!cuestionario) {
throw new NotFoundException(`Cuestionario con ID ${submitDto.id_formulario} no encontrado`);
}
// 2. Buscar o crear participante
let participante = await this.participanteRepository.findOne({
where: { correo: submitDto.correo }
});
if (!participante) {
participante = new Participante();
participante.correo = submitDto.correo;
participante.id_tipo_user = 1; // Usar un tipo de usuario por defecto
participante = await queryRunner.manager.save(participante);
}
// 3. Crear un nuevo registro de cuestionario respondido
const cuestionarioRespondido = new CuestionarioRespondido();
cuestionarioRespondido.cuestionario = cuestionario;
cuestionarioRespondido.participante = participante;
cuestionarioRespondido.fechaRespuesta = submitDto.fecha_envio ?
new Date(submitDto.fecha_envio) : new Date();
const savedCuestionarioRespondido = await queryRunner.manager.save(cuestionarioRespondido);
// 4. Procesar cada respuesta
for (const respuestaDto of submitDto.respuestas) {
// Buscar la pregunta
const pregunta = await this.preguntaRepository.findOne({
where: { id_pregunta: respuestaDto.id_pregunta },
relations: ['tipoPregunta']
});
if (!pregunta) {
throw new NotFoundException(`Pregunta con ID ${respuestaDto.id_pregunta} no encontrada`);
}
// Si la pregunta tiene una validación configurada y es una respuesta abierta, validarla
if (pregunta.validacion && typeof respuestaDto.valor === 'string') {
const resultadoValidacion = this.validadorRespuestasService.validarRespuesta(
respuestaDto.valor,
pregunta.validacion
);
if (!resultadoValidacion.valido) {
throw new BadRequestException(`Validación fallida en pregunta ${pregunta.id_pregunta}: ${resultadoValidacion.mensaje}`);
}
}
// Determinar tipo de pregunta por ID
const esPreguntaAbierta = pregunta.id_tipo_pregunta === 2;
// O determinar tipo de pregunta por su nombre si existe
const esPreguntaTipoAbierta = pregunta.tipoPregunta &&
pregunta.tipoPregunta.tipo_pregunta === 'Abierta';
// Determinar tipo de pregunta y guardar respuesta adecuadamente
if (esPreguntaAbierta || esPreguntaTipoAbierta) {
// Respuesta abierta (texto)
// Convertir a string y truncar a 250 caracteres si es necesario
let respuestaTexto = String(respuestaDto.valor);
if (respuestaTexto.length > 250) {
respuestaTexto = respuestaTexto.substring(0, 250);
}
const respuestaAbierta = new RespuestaParticipanteAbierta();
respuestaAbierta.pregunta = pregunta;
respuestaAbierta.respuesta = respuestaTexto;
respuestaAbierta.cuestionarioRespondido = savedCuestionarioRespondido;
await queryRunner.manager.save(respuestaAbierta);
} else {
// Respuesta cerrada (opción)
// Verificar si es una respuesta múltiple (array de IDs)
const opcionIds = Array.isArray(respuestaDto.valor)
? respuestaDto.valor
: [respuestaDto.valor];
// Procesar cada ID de opción
for (const opcionId of opcionIds) {
// Buscar la relación entre pregunta y opción usando el ID de opción
const preguntaOpciones = await this.preguntaOpcionRepository
.createQueryBuilder('po')
.innerJoinAndSelect('po.opcion', 'opcion')
.where('po.id_pregunta = :preguntaId', { preguntaId: pregunta.id_pregunta })
.andWhere('opcion.id_opcion = :opcionId', { opcionId: opcionId })
.getMany();
if (preguntaOpciones.length === 0) {
throw new BadRequestException(`La opción con ID ${opcionId} no está asociada a la pregunta ${respuestaDto.id_pregunta}`);
}
// Guardar respuesta cerrada
const respuestaCerrada = new RespuestaParticipanteCerrada();
respuestaCerrada.preguntaOpcion = preguntaOpciones[0];
respuestaCerrada.cuestionarioRespondido = savedCuestionarioRespondido;
await queryRunner.manager.save(respuestaCerrada);
}
}
}
// Variables para el QR y correo electrónico
let datosQR: {
id_participante: number;
id_evento: number;
} | null = null;
// Capturamos los datos para el QR
if (cuestionario.evento && cuestionario.evento.id_evento) {
datosQR = {
id_participante: participante.id_participante,
id_evento: cuestionario.evento.id_evento
};
}
// Primero completamos la transacción principal
await queryRunner.commitTransaction();
// Después de la transacción principal, intentamos registrar la relación participante-evento
// y enviar el correo como operaciones independientes
if (cuestionario.evento && cuestionario.evento.id_evento) {
try {
// Registrar participante en evento
try {
// Verificar si ya existe una relación participante-evento
const participanteEventoExistente = await this.dataSource.query(
`SELECT * FROM participante_evento WHERE id_participante = ? AND id_evento = ?`,
[participante.id_participante, cuestionario.evento.id_evento]
);
// Si no existe, crear la relación
if (!participanteEventoExistente || participanteEventoExistente.length === 0) {
await this.dataSource.query(
`INSERT INTO participante_evento (id_participante, id_evento, fecha_inscripcion, estatus) VALUES (?, ?, ?, ?)`,
[participante.id_participante, cuestionario.evento.id_evento, new Date(), true]
);
}
} catch (dbError) {
console.error('Error al registrar participante en evento:', dbError.message);
}
// Generar código QR
const qrJsonData = JSON.stringify(datosQR);
const qrBuffer = await QRCode.toBuffer(qrJsonData);
const qrDataUrl = await QRCode.toDataURL(qrJsonData);
console.log('QR Data URL:', qrDataUrl);
console.log("antes de enviar correo");
// Enviar correo con el QR
const urlApiCorreos = process.env.url_api_correos || 'http://localhost:3000';
await axios.post(`${urlApiCorreos}/mail/send`, {
to: participante.correo,
subject: 'Asistencia QR',
fecha_recibido: "2025-03-10T12:00:00Z",
html: `
<h1>¡Gracias por registrarte!</h1>
<p>Has completado exitosamente el formulario: ${cuestionario.nombre_form}</p>
<p>Deberás presentar este QR (celular o impreso) en cualquier stand para que valide tu asistencia y
recibas tu constancia.</p>
<p>Este es tu QR para registrar asistencia:</p>
<img src="cid:qrCode" alt="Código QR" />
<p>Tus datos de registro:</p>
<ul>
<li>Evento: ${cuestionario.evento.nombre_evento || 'Evento'}</li>
<li>Correo: ${participante.correo}</li>
<li>Fecha de registro: ${new Date().toLocaleString()}</li>
</ul>
`,
adjuntos: [
{
filename: 'qr.png',
content: qrBuffer.toString('base64'),
encoding: 'base64',
cid: 'qrCode'
}
]
});
console.log(`Correo enviado exitosamente a ${participante.correo}`);
} catch (error) {
console.error('Error después de la transacción principal:', error);
// No lanzamos el error para que no afecte la respuesta al usuario
}
}
// Preparar respuesta
const response: any = {
success: true,
message: 'Respuestas guardadas correctamente',
cuestionarioRespondidoId: savedCuestionarioRespondido.idCuestionarioRespondido
};
// Incluir IDs para generar QR si hay evento asociado
if (datosQR) {
response.datos_qr = datosQR;
}
return response;
} catch (error) {
// Solo hacemos rollback si no hemos hecho commit todavía
try {
await queryRunner.rollbackTransaction();
} catch (rollbackError) {
console.error('Error durante rollback:', rollbackError.message);
}
throw error;
} finally {
await queryRunner.release();
}
findAll() {
return `This action returns all cuestionarioRespondido`;
}
async findAll() {
// Obtener todos los cuestionarios respondidos con sus relaciones básicas
const cuestionariosRespondidos = await this.cuestionarioRespondidoRepository.find({
relations: ['participante', 'cuestionario', 'respuestasAbiertas', 'respuestasAbiertas.pregunta', 'respuestasCerradas']
});
// Para cada cuestionario respondido, obtener y formatear las respuestas cerradas
const resultados = await Promise.all(
cuestionariosRespondidos.map(async (cuestionarioRespondido) => {
// Obtener datos de respuestas cerradas con consulta SQL
const respuestasCerradas = await this.dataSource.query(`
SELECT
rpc.idRespuestaParticipanteCerrada,
rpc.id_pregunta_opcion,
po.id_pregunta,
p.pregunta,
p.id_tipo_pregunta,
o.id_opcion,
o.opcion
FROM respuesta_participante_cerrada rpc
LEFT JOIN pregunta_opcion po ON rpc.id_pregunta_opcion = po.id_pregunta_opcion
LEFT JOIN pregunta p ON po.id_pregunta = p.id_pregunta
LEFT JOIN opcion o ON po.id_opcion = o.id_opcion
WHERE rpc.id_cuestionario_respondido = ?
`, [cuestionarioRespondido.idCuestionarioRespondido]);
// Formatear respuestas cerradas
const respuestasCerradasFormateadas = respuestasCerradas.map(respuesta => ({
idRespuestaParticipanteCerrada: respuesta.idRespuestaParticipanteCerrada,
id_pregunta_opcion: respuesta.id_pregunta_opcion,
pregunta: {
id_pregunta: respuesta.id_pregunta,
texto_pregunta: respuesta.pregunta,
id_tipo_pregunta: respuesta.id_tipo_pregunta
},
opcion: {
id_opcion: respuesta.id_opcion,
opcion: respuesta.opcion
}
}));
// Devolver cuestionario con respuestas formateadas
return {
...cuestionarioRespondido,
respuestasCerradas: respuestasCerradasFormateadas
};
})
);
return resultados;
}
async findOne(id: number) {
// Obtener cuestionario respondido con sus relaciones básicas
const cuestionarioRespondido = await this.cuestionarioRespondidoRepository.findOne({
where: { idCuestionarioRespondido: id },
relations: [
'participante',
'cuestionario',
'respuestasAbiertas',
'respuestasAbiertas.pregunta',
'respuestasCerradas'
]
});
if (!cuestionarioRespondido) {
throw new NotFoundException(`Cuestionario respondido con ID ${id} no encontrado`);
}
// Obtener directamente los datos de respuestas cerradas desde la base de datos
// usando una consulta SQL directa
const respuestasCerradas = await this.dataSource.query(`
SELECT
rpc.idRespuestaParticipanteCerrada,
rpc.id_pregunta_opcion,
po.id_pregunta,
p.pregunta,
p.id_tipo_pregunta,
o.id_opcion,
o.opcion
FROM respuesta_participante_cerrada rpc
LEFT JOIN pregunta_opcion po ON rpc.id_pregunta_opcion = po.id_pregunta_opcion
LEFT JOIN pregunta p ON po.id_pregunta = p.id_pregunta
LEFT JOIN opcion o ON po.id_opcion = o.id_opcion
WHERE rpc.id_cuestionario_respondido = ?
`, [id]);
console.log('Respuestas cerradas SQL:', JSON.stringify(respuestasCerradas, null, 2));
// Transformar las respuestas cerradas al formato deseado
const respuestasCerradasFormateadas = respuestasCerradas.map(respuesta => ({
idRespuestaParticipanteCerrada: respuesta.idRespuestaParticipanteCerrada,
id_pregunta_opcion: respuesta.id_pregunta_opcion,
pregunta: {
id_pregunta: respuesta.id_pregunta,
texto_pregunta: respuesta.pregunta,
id_tipo_pregunta: respuesta.id_tipo_pregunta
},
opcion: {
id_opcion: respuesta.id_opcion,
opcion: respuesta.opcion
}
}));
// Crear el objeto de respuesta con el formato deseado
const resultado = {
...cuestionarioRespondido,
respuestasCerradas: respuestasCerradasFormateadas
};
return resultado;
findOne(id: number) {
return `This action returns a #${id} cuestionarioRespondido`;
}
update(id: number, updateCuestionarioRespondidoDto: UpdateCuestionarioRespondidoDto) {
@@ -388,98 +23,4 @@ recibas tu constancia.</p>
remove(id: number) {
return `This action removes a #${id} cuestionarioRespondido`;
}
async generarReporteRespuestas(idCuestionario: number): Promise<string> {
// Buscar el cuestionario para validar que existe
const cuestionario = await this.cuestionarioRepository.findOne({
where: { id_cuestionario: idCuestionario },
relations: ['evento'],
});
if (!cuestionario) {
throw new NotFoundException(`Cuestionario con ID ${idCuestionario} no encontrado`);
}
// Obtener todas las respuestas para este cuestionario con sus relaciones
const respuestas = await this.dataSource.query(`
SELECT
cr.id_cuestionario_respondido,
cr.fecha_respuesta,
p.id_participante,
p.correo AS correo_participante,
c.id_cuestionario,
c.nombre_form AS nombre_cuestionario,
e.id_evento,
e.nombre_evento,
preg.id_pregunta,
preg.pregunta,
-- Para respuestas abiertas
rpa.respuesta AS respuesta_abierta,
-- Para respuestas cerradas
o.id_opcion,
o.opcion AS respuesta_cerrada
FROM cuestionario_respondido cr
JOIN participante p ON cr.id_participante = p.id_participante
JOIN cuestionario c ON cr.id_cuestionario = c.id_cuestionario
LEFT JOIN evento e ON c.id_evento = e.id_evento
-- Respuestas abiertas
LEFT JOIN respuesta_participante_abierta rpa ON rpa.id_cuestionario_respondido = cr.id_cuestionario_respondido
LEFT JOIN pregunta preg_abierta ON rpa.id_pregunta = preg_abierta.id_pregunta
-- Respuestas cerradas
LEFT JOIN respuesta_participante_cerrada rpc ON rpc.id_cuestionario_respondido = cr.id_cuestionario_respondido
LEFT JOIN pregunta_opcion po ON rpc.id_pregunta_opcion = po.id_pregunta_opcion
LEFT JOIN pregunta preg ON po.id_pregunta = preg.id_pregunta
LEFT JOIN opcion o ON po.id_opcion = o.id_opcion
WHERE c.id_cuestionario = ?
ORDER BY cr.id_cuestionario_respondido, preg.id_pregunta
`, [idCuestionario]);
// Preparar los datos para el CSV
const datosReporte: string[][] = [];
// Agregar encabezados
const encabezados: string[] = [
'ID Respuesta',
'Fecha Respuesta',
'ID Participante',
'Correo Participante',
'Evento',
'Cuestionario',
'Pregunta',
'Respuesta'
];
datosReporte.push(encabezados);
// Procesar los resultados para el CSV
for (const fila of respuestas) {
const respuesta = fila.respuesta_abierta || fila.respuesta_cerrada || '';
datosReporte.push([
fila.id_cuestionario_respondido?.toString() || '',
new Date(fila.fecha_respuesta).toLocaleString(),
fila.id_participante?.toString() || '',
fila.correo_participante || '',
fila.nombre_evento || 'Sin evento',
fila.nombre_cuestionario || '',
fila.pregunta || '',
respuesta
]);
}
// Generar CSV de forma manual con valores separados por comas y líneas con saltos de línea
// Escapar valores que contengan comas para evitar problemas con el formato CSV
const csvContent = datosReporte.map(row =>
row.map(value => {
// Si el valor contiene comas, comillas o saltos de línea, encerrarlo en comillas dobles
// y escapar cualquier comilla doble dentro del valor
if (value.includes(',') || value.includes('"') || value.includes('\n')) {
return `"${value.replace(/"/g, '""')}"`;
}
return value;
}).join(',')
).join('\n');
return csvContent;
}
}
@@ -1,92 +0,0 @@
import { ApiProperty } from '@nestjs/swagger';
import {
IsArray,
IsDateString,
IsNotEmpty,
IsNumber,
IsOptional,
IsString,
ValidateNested,
ValidateIf,
MaxLength
} from 'class-validator';
import { Type, Transform } from 'class-transformer';
export class RespuestaDto {
@ApiProperty({
description: 'ID de la pregunta respondida',
example: 101
})
@IsNumber()
id_pregunta: number;
@ApiProperty({
description: 'Valor de la respuesta según tipo de pregunta',
examples: [
'Texto para pregunta abierta', // String para preguntas abiertas
5, // Número para preguntas cerradas (ID de opción)
[2, 7, 9] // Array para preguntas múltiples (IDs de opciones)
],
oneOf: [
{ type: 'string', description: 'Texto para pregunta abierta (máximo 250 caracteres)' },
{ type: 'number', description: 'ID de opción para pregunta cerrada' },
{
type: 'array',
items: { type: 'number' },
description: 'Array de IDs de opciones para pregunta multiple'
}
]
})
@ValidateIf(o => typeof o.valor === 'string')
@MaxLength(250, { message: 'Las respuestas de texto no pueden exceder 250 caracteres' })
valor: string | number | number[];
}
export class SubmitRespuestasDto {
@ApiProperty({
description: 'ID del formulario a responder',
example: 1
})
@IsNumber()
id_formulario: number;
@ApiProperty({
description: 'Correo electrónico del participante',
example: 'usuario@ejemplo.com'
})
@IsString()
@IsNotEmpty()
@MaxLength(250, { message: 'El correo electrónico no puede exceder 250 caracteres' })
correo: string;
@ApiProperty({
description: 'Array de respuestas a las preguntas',
type: [RespuestaDto],
examples: [
{
id_pregunta: 101,
valor: 'Texto para pregunta abierta'
},
{
id_pregunta: 102,
valor: 5 // ID de la opción para pregunta cerrada simple
},
{
id_pregunta: 103,
valor: [2, 7, 9] // Array de IDs de opciones para pregunta de selección múltiple
}
]
})
@IsArray()
@ValidateNested({ each: true })
@Type(() => RespuestaDto)
respuestas: RespuestaDto[];
@ApiProperty({
description: 'Fecha y hora del envío',
example: '2025-04-02T13:45:00'
})
@IsDateString()
@IsOptional()
fecha_envio?: string;
}
@@ -4,7 +4,6 @@ import { Cuestionario } from 'src/cuestionario/entities/cuestionario.entity';
import { RespuestaParticipanteCerrada } from 'src/respuesta_participante_cerrada/entities/respuesta_participante_cerrada.entity';
import { RespuestaParticipanteAbierta } from 'src/respuesta_participante_abierta/entities/respuesta_participante_abierta.entity';
import { Participante } from 'src/participante/participante.entity';
import { Opcion } from 'src/opcion/entities/opcion.entity';
@Entity('cuestionario_respondido')
export class CuestionarioRespondido {
@@ -15,12 +14,12 @@ export class CuestionarioRespondido {
fechaRespuesta: Date;
// Relación con Participante
@ManyToOne(() => Participante, participante => participante.cuestionariosRespondidos)
@ManyToOne(() => Participante, participante => participante.cuestionarioRespondidos)
@JoinColumn({ name: 'id_participante' })
participante: Participante;
// Relación con Cuestionario
@ManyToOne(() => Cuestionario)
@ManyToOne(() => Cuestionario, cuestionario => cuestionario.cuestionariosRespondidos)
@JoinColumn({ name: 'id_cuestionario' })
cuestionario: Cuestionario;
@@ -3,17 +3,11 @@ import { CuestionarioSeccionService } from './cuestionario_seccion.service';
import { CuestionarioSeccionController } from './cuestionario_seccion.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CuestionarioSeccion } from './entities/cuestionario_seccion.entity';
import { CuestionarioModule } from '../cuestionario/cuestionario.module';
import { SeccionModule } from '../seccion/seccion.module';
@Module({
imports: [
TypeOrmModule.forFeature([CuestionarioSeccion]),
CuestionarioModule,
SeccionModule
],
controllers: [CuestionarioSeccionController],
providers: [CuestionarioSeccionService],
exports: [TypeOrmModule]
imports: [TypeOrmModule.forFeature([CuestionarioSeccion])],
controllers: [CuestionarioSeccionController],
providers: [CuestionarioSeccionService],
exports:[CuestionarioSeccionService, TypeOrmModule]
})
export class CuestionarioSeccionModule {}
@@ -1,4 +1,4 @@
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from 'typeorm';
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
import { Cuestionario } from '../../cuestionario/entities/cuestionario.entity'; // Asegúrate de importar la entidad Cuestionario
import { Seccion } from 'src/seccion/entities/seccion.entity';
@@ -12,14 +12,10 @@ export class CuestionarioSeccion {
@ManyToOne(() => Cuestionario, (cuestionario) => cuestionario.cuestionarioSeccion)
@JoinColumn({name:'id_cuestionario'})
cuestionario: Cuestionario;
@Column({ type: 'int' })
id_cuestionario: number;
@ManyToOne(() => Seccion, (seccion) => seccion.seccion)
seccion: Seccion;
@Column({type:'int'})
id_seccion:number;
@ManyToOne(() => Seccion, (seccion) => seccion.cuestionarioSeccion)
@JoinColumn({name:'id_seccion'})
seccion:Seccion;
}
+2 -2
View File
@@ -6,7 +6,7 @@ import { INestApplication } from '@nestjs/common';
@Module({})
export class DocsModule {
static setupSwagger(app: INestApplication) {
const document = SwaggerModule.createDocument(app, swaggerConfig);
SwaggerModule.setup('api-docs', app, document);
const document = SwaggerModule.createDocument(app, swaggerConfig);
SwaggerModule.setup('api-docs', app, document);
}
}
+3 -27
View File
@@ -1,35 +1,11 @@
import { DocumentBuilder } from '@nestjs/swagger';
export const swaggerConfig = new DocumentBuilder()
.setTitle('Sistema de Formularios y Eventos CIDWA')
.setDescription(`
API para la gestión de formularios, cuestionarios, eventos y asistencias.
## Funcionalidades principales
- **Cuestionarios**: Creación y gestión de formularios con secciones y preguntas
- **Eventos**: Registro y control de eventos
- **Participantes**: Gestión de usuarios y participantes
- **Asistencia**: Control de asistencia a eventos
- **QR**: Generación y validación de códigos QR
## Ejemplos de uso
La API permite crear cuestionarios complejos con secciones y diferentes tipos de preguntas.
Consulte la documentación de cada endpoint para ver ejemplos específicos.
`)
.addServer(process.env.SWAGGER_SERVER_URL || 'http://localhost:3000')
.addServer( 'http://localhost:3000')
.setTitle('Sistema de Registro de Usuarios y Eventos')
.setDescription('API para gestionar usuarios, eventos, asistencia y códigos QR')
.setVersion('1.0')
.addTag('Cuestionarios')
.addTag('Secciones')
.addTag('Preguntas')
.addTag('Opciones')
.addTag('Usuarios')
.addTag('Eventos')
.addTag('Asistencia')
.addTag('QR')
.addBearerAuth()
.addTag('QR')
.build();
-6
View File
@@ -1,16 +1,10 @@
import { IsString } from "class-validator";
export class CreateEventoDto {
@IsString()
tipo_evento: string;
@IsString()
nombre_evento: string;
@IsString()
fecha_inicio: Date;
@IsString()
fecha_fin: Date;
//agregar el id del administrador
+5 -11
View File
@@ -1,7 +1,5 @@
import { Imagen } from "src/imagen/imagen.entity";
import { Participante } from "src/participante/participante.entity";
import { ParticipanteEvento } from "src/participante_evento/participante_evento.entity";
import { Column, Entity, ManyToMany, OneToMany, PrimaryGeneratedColumn } from "typeorm";
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm";
@Entity()
export class Evento {
@@ -28,17 +26,13 @@ export class Evento {
@JoinColumn({ name: "id_administrador" })
administrador: Administrador;
@OneToMany(() => ParticipanteEvento, (pe) => pe.evento)
participantes: ParticipanteEvento[];
@OneToMany(() => Asistencia, (asistencia) => asistencia.evento)
asistencias: Asistencia[];
*/
@OneToMany(() => ParticipanteEvento, participanteEvento => participanteEvento.evento)
participanteEventos: ParticipanteEvento[];
@OneToMany(() => Imagen, (imagen) => imagen.evento)
imagenes: Imagen[]
@OneToMany(() => ParticipanteEvento, (participanteEvento) => participanteEvento.evento)
participantes: ParticipanteEvento[];
}
+3 -5
View File
@@ -1,14 +1,12 @@
import { forwardRef, Module } from '@nestjs/common';
import { Module } from '@nestjs/common';
import { EventoService } from './evento.service';
import { EventoController } from './evento.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Evento } from './evento.entity';
import { ImagenModule } from 'src/imagen/imagen.module';
@Module({
imports: [TypeOrmModule.forFeature([Evento]), forwardRef(() => ImagenModule)],
imports: [TypeOrmModule.forFeature([Evento])],
controllers: [EventoController],
providers: [EventoService],
exports: [EventoService, TypeOrmModule]
providers: [EventoService]
})
export class EventoModule {}
+2 -2
View File
@@ -29,7 +29,7 @@ export class EventoService {
getEventos() {
return this.eventoRepository.find({
relations: ['participanteEventos', 'imagenes'] //participante
relations: ['participantes']
})
}
@@ -38,7 +38,7 @@ export class EventoService {
where: {
id_evento
},
relations: ['participanteEventos', 'imagenes'],
relations: ['participantes']
})
if (!eventoFound)
-47
View File
@@ -1,47 +0,0 @@
import { ApiProperty, ApiSchema } from "@nestjs/swagger";
import { Transform, Type } from "class-transformer";
import { IsBoolean, IsInt, IsOptional, IsString } from "class-validator";
@ApiSchema({ name: 'CreateImagenRequest' })
class CreateCatDto {}
export class CreateImagenDto {
/*
url: string
orden: number
tipo: string
evento: {
id_evento: number
} // si usas relaciones
evento: number
*/
@IsOptional()
@IsString()
url: string;
@Type(() => Number)
@IsInt()
orden: number;
@IsString()
tipo: string;
@Type(() => Number)
@IsInt() // tiene que ser el id del evento
evento: number;
// Campo extra para controlar lógica, no se guarda
@IsOptional()
@IsBoolean()
@ApiProperty({
type: Boolean
})
@Transform(({ value }) => value === 'true' || value === true)
force_orden?: boolean;
}
-17
View File
@@ -1,17 +0,0 @@
import { Type } from "class-transformer"
import { IsInt, IsOptional, IsString } from "class-validator"
export class UpdateImagenDto {
@IsString()
@IsOptional()
url?: string
@Type(() => Number)
@IsOptional()
@IsInt()
orden?: number
@IsOptional()
@IsString()
tipo?: string
}
-108
View File
@@ -1,108 +0,0 @@
import { BadRequestException, Body, Controller, Delete, Get, HttpException, HttpStatus, Param, ParseIntPipe, Patch, Post, Res, UploadedFile, UseInterceptors } from '@nestjs/common';
import { ImagenService, imageStorage } from './imagen.service';
import { UpdateImagenDto } from './dto/update.imagen.dto';
import { FileInterceptor } from '@nestjs/platform-express';
import { extname, join } from 'path';
import { diskStorage } from 'multer';
import { CreateImagenDto } from './dto/create-imagen.dto';
import { existsSync } from 'fs';
import { Response } from 'express';
import { ImagenApiDocumentation } from './imagen.documentation'; // Importar la documentación
@ImagenApiDocumentation.ApiController
@Controller('imagen')
export class ImagenController {
constructor(private imagenService: ImagenService) {}
@Get()
getImagenes() {
return this.imagenService.getImagenes()
}
@Get('evento/:id_evento')
@ImagenApiDocumentation.ApiGetImagenEvento // Aplicando la documentación al método
getImagenEvento(@Param('id_evento', ParseIntPipe) id_evento: number) {
return this.imagenService.getImagenEvento(id_evento)
}
@Get(':id')
@ImagenApiDocumentation.ApiGetOne
getImagen(@Param('id', ParseIntPipe) id_imagen: number) {
return this.imagenService.getImagen(id_imagen)
}
@Get('visual/:id')
@ImagenApiDocumentation.ApiGetVisual // Aplicando la documentación al método
async getVisualImagen(
@Param('id') id_imagen: number,
@Res() res: Response
) {
const url = await this.imagenService.getVisualImagen(id_imagen);
const rutaImagen = join(process.cwd(), url);
if (!existsSync(rutaImagen)) {
throw new HttpException('Imagen no encontrada', HttpStatus.NOT_FOUND);
}
return res.sendFile(rutaImagen);
}
@Post()
@ImagenApiDocumentation.ApiCreate
@UseInterceptors(FileInterceptor('imagen', {
storage: diskStorage({
destination: './uploads',
filename: (req, file, cb) => {
const uniqueName = Date.now() + '-' + Math.round(Math.random() * 1e9);
const extension = extname(file.originalname);
cb(null, `${uniqueName}${extension}`);
},
}),
}))
async subirImagen(@UploadedFile() file: Express.Multer.File, @Body() imagen: CreateImagenDto,) {
if (!file) {
throw new BadRequestException('No se ha subido ningún archivo');
}
if (!imagen.evento) {
throw new BadRequestException('Falta el id_evento en los datos');
}
const url = `/uploads/${file.filename}`;
return this.imagenService.crearImagen({
...imagen,
url,
evento: imagen.evento,
});
}
@Delete(':id')
@ImagenApiDocumentation.ApiRemove
deleteImagen(@Param('id', ParseIntPipe) id_imagen: number) {
return this.imagenService.deleteImagen(id_imagen)
}
@Patch(':id')
@ImagenApiDocumentation.ApiUpdate
@UseInterceptors(FileInterceptor('imagen', { storage: imageStorage })) // Interceptor para manejar la subida de archivo
async updateImagen(
@Param('id') id: number,
@UploadedFile() file: Express.Multer.File, // Recibe el archivo cargado
@Body() imagen: UpdateImagenDto,
) {
if (file) {
// Si se sube un nuevo archivo, actualiza la URL
imagen.url = `/uploads/${file.filename}`;
}
const newImagen = await this.imagenService.updateImagen(id, imagen);
return newImagen;
}
}
-210
View File
@@ -1,210 +0,0 @@
import { ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
import { applyDecorators, Get, HttpException, HttpStatus, Param, ParseIntPipe, Res } from '@nestjs/common';
import { join } from 'path';
import { existsSync } from 'fs';
export class ImagenApiDocumentation {
// Decoradores para toda la clase del controlador
static ApiController = ApiTags('Imágenes');
// Documentación para obtener una imagen visual por ID
static ApiGetVisual = applyDecorators(
ApiOperation({
summary: 'Obtener imagen visual por ID',
description: 'Retorna una imagen visual específica dada su ID.'
}),
ApiParam({
name: 'id',
description: 'ID de la imagen visual',
required: true,
type: 'number',
example: 1
}),
ApiResponse({
status: 200,
description: 'Imagen visual encontrada correctamente.',
content: {
'image/png': {
schema: { type: 'string', format: 'binary' }
}
}
}),
ApiResponse({ status: 404, description: 'Imagen no encontrada.' }),
ApiResponse({ status: 500, description: 'Error interno del servidor.' })
);
// Documentación para obtener las imágenes asociadas a un evento
static ApiGetImagenEvento = applyDecorators(
ApiOperation({
summary: 'Obtener imágenes por evento',
description: 'Retorna una lista de imágenes asociadas a un evento específico.'
}),
ApiParam({
name: 'id_evento',
description: 'ID del evento',
required: true,
type: 'number',
example: 10
}),
ApiResponse({
status: 200,
description: 'Imágenes del evento obtenidas correctamente.',
schema: {
type: 'array',
items: {
type: 'object',
properties: {
id_imagen: { type: 'number', example: 1 },
url: { type: 'string', example: '/uploads/imagen1.png' },
evento_id: { type: 'number', example: 10 }
}
}
}
}),
ApiResponse({ status: 404, description: 'Evento no encontrado.' }),
ApiResponse({ status: 500, description: 'Error interno del servidor.' })
);
// Documentación para crear una nueva imagen
static ApiCreate = applyDecorators(
ApiOperation({
summary: 'Crear una nueva imagen',
description: 'Crea una nueva imagen asociada a un evento específico.'
}),
ApiBody({
description: 'Datos para crear una nueva imagen',
schema: {
type: 'object',
required: ['orden', 'tipo', 'evento'],
properties: {
url: { type: 'string', example: '/uploads/imagen1.png' },
orden: { type: 'number', example: 1 },
tipo: { type: 'string', example: 'cabezera' },
evento: { type: 'number', example: 5 }
}
}
}),
ApiResponse({
status: 201,
description: 'Imagen creada correctamente.',
schema: {
type: 'object',
properties: {
id_imagen: { type: 'number', example: 1 },
url: { type: 'string', example: '/uploads/imagen1.png' },
orden: { type: 'number', example: 1 },
tipo: { type: 'string', example: 'cabezera' },
evento: {
type: 'object',
properties: {
id_evento: { type: 'number', example: 1 }
}
}
}
}
}),
ApiResponse({ status: 400, description: 'Datos inválidos para crear la imagen.' }),
ApiResponse({ status: 500, description: 'Error interno del servidor.' })
);
// Documentación para actualizar una imagen
static ApiUpdate = applyDecorators(
ApiOperation({
summary: 'Actualizar una imagen',
description: 'Permite actualizar los datos de una imagen existente'
}),
ApiParam({
name: 'id',
description: 'ID de la imagen a actualizar',
required: true,
type: 'number',
example: 1
}),
ApiBody({
description: 'Datos a actualizar de la imagen',
schema: {
type: 'object',
properties: {
url: { type: 'string', example: '/uploads/1745549896986-789608318_updated.png' },
descripcion: { type: 'string', example: 'Nueva imagen de ejemplo para actualización' }
}
}
}),
ApiResponse({
status: 200,
description: 'Imagen actualizada correctamente',
schema: {
type: 'object',
properties: {
affected: { type: 'number', example: 1 }
}
}
}),
ApiResponse({ status: 400, description: 'Datos inválidos para la actualización de la imagen.' }),
ApiResponse({ status: 404, description: 'Imagen no encontrada.' }),
ApiResponse({ status: 500, description: 'Error interno del servidor' })
);
// Documentación para obtener una imagen por ID
static ApiGetOne = applyDecorators(
ApiOperation({
summary: 'Obtener una imagen por ID',
description: 'Permite obtener los detalles de una imagen a partir de su ID.'
}),
ApiParam({
name: 'id',
description: 'ID de la imagen',
required: true,
type: 'number',
example: 1
}),
ApiResponse({
status: 200,
description: 'Imagen encontrada correctamente',
schema: {
type: 'object',
properties: {
id_imagen: { type: 'number', example: 1 },
url: { type: 'string', example: '/uploads/1745549896986-789608318.png' },
orden: { type: 'number', example: '1'},
tipo: { type: 'string', example: 'Cabezera'},
//descripcion: { type: 'string', example: 'Imagen de muestra para la documentación' }
}
}
}),
ApiResponse({ status: 404, description: 'Imagen no encontrada' }),
ApiResponse({ status: 500, description: 'Error interno del servidor' })
);
// Documentación para eliminar una imagen
static ApiRemove = applyDecorators(
ApiOperation({
summary: 'Eliminar una imagen',
description: 'Permite eliminar una imagen a partir de su ID.'
}),
ApiParam({
name: 'id',
description: 'ID de la imagen a eliminar',
required: true,
type: 'number',
example: 1
}),
ApiResponse({
status: 200,
description: 'Imagen eliminada correctamente',
schema: {
type: 'object',
properties: {
affected: { type: 'number', example: 1 }
}
}
}),
ApiResponse({ status: 404, description: 'Imagen no encontrada' }),
ApiResponse({ status: 500, description: 'Error interno del servidor' })
);
}
-25
View File
@@ -1,25 +0,0 @@
import { Type } from "class-transformer";
import { IsInt } from "class-validator";
import { Evento } from "src/evento/evento.entity";
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm";
@Entity()
export class Imagen {
@PrimaryGeneratedColumn()
id_imagen: number
@Column()
url?: string
@Column()
@Type(() => Number)
orden: number
@Column()
tipo: string
@ManyToOne(() => Evento, (evento) => evento.imagenes, { onDelete: 'CASCADE' })
@IsInt()
@Type(() => Number)
evento: Evento;
}
-14
View File
@@ -1,14 +0,0 @@
import { forwardRef, Module } from '@nestjs/common';
import { ImagenService } from './imagen.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Imagen } from './imagen.entity';
import { EventoModule } from 'src/evento/evento.module';
import { ImagenController } from './imagen.controller';
@Module({
imports: [TypeOrmModule.forFeature([Imagen]), forwardRef(() => EventoModule)], //forwardRef(() => EventoModule
controllers: [ImagenController],
providers: [ImagenService],
exports: [ImagenService]
})
export class ImagenModule {}
-169
View File
@@ -1,169 +0,0 @@
import { HttpException, HttpStatus, Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Imagen } from './imagen.entity';
import { Repository } from 'typeorm';
import { Evento } from 'src/evento/evento.entity';
import { CreateImagenDto } from './dto/create-imagen.dto';
import { UpdateImagenDto } from './dto/update.imagen.dto';
import { extname } from 'path';
import { diskStorage } from 'multer';
@Injectable()
export class ImagenService {
constructor(
@InjectRepository(Imagen) private imagenRepository: Repository<Imagen>,
@InjectRepository(Evento) private eventoRepository: Repository<Evento>
) {}
async getUltimoOrden(id_evento: number): Promise<number> {
const [ultimo] = await this.imagenRepository.find({
where: {
evento: { id_evento }, // Filtrar por evento
},
order: {
orden: 'DESC',
},
take: 1,
});
return ultimo?.orden ?? 0;
}
async crearImagen(imagen: CreateImagenDto): Promise<Imagen> {
const evento = await this.eventoRepository.findOneBy({ id_evento: imagen.evento });
if (!evento) {
throw new NotFoundException('Evento no encontrado');
}
const ordenExiste = await this.imagenRepository.findOneBy({
orden: imagen.orden,
evento: { id_evento: imagen.evento },
});
if (ordenExiste) {
if (imagen.force_orden === false || imagen.force_orden === undefined) {
throw new HttpException('Este orden ya existe', HttpStatus.CONFLICT)
}
}
if (imagen.force_orden === true) {
const imagenExistente = await this.imagenRepository.findOne({
where: {
orden: imagen.orden,
evento: { id_evento: imagen.evento },
},
relations: ['evento'],
});
if (imagenExistente) {
const ultimoOrden = await this.getUltimoOrden(imagen.evento);
imagenExistente.orden = ultimoOrden + 1;
await this.imagenRepository.save(imagenExistente); // Guardar imagen existente con nuevo orden
}
}
// Crear y guardar nueva imagen
const nuevaImagen = this.imagenRepository.create({
...imagen,
evento: { id_evento: imagen.evento }, // para la relación ManyToOne
});
return this.imagenRepository.save(nuevaImagen);
}
getImagenes(): Promise<Imagen[]> {
return this.imagenRepository.find()
}
async getImagen(id_imagen: number) {
const imagenFound = await this.imagenRepository.findOne({
where: {
id_imagen
}
})
if (!imagenFound) {
return new HttpException('Imagen not found', HttpStatus.NOT_FOUND);
}
return imagenFound
}
async getImagenEvento(id_evento: number) {
const imagenEventoFound = await this.eventoRepository.findOne({
where: {
id_evento
}
})
if (!imagenEventoFound) {
return new HttpException('Evento not found', HttpStatus.NOT_FOUND);
}
return this.imagenRepository.find({
where: {
evento: {
id_evento
}
}
})
}
async getVisualImagen(id_imagen: number): Promise<string> {
const imagenFound = await this.imagenRepository.findOne({
where: {
id_imagen
},
});
if (!imagenFound || !imagenFound.url) {
throw new HttpException('Imagen not found', HttpStatus.NOT_FOUND);
}
return imagenFound.url;
}
async deleteImagen(id_imagen: number) {
const result = await this.imagenRepository.delete({ id_imagen })
if (result.affected === 0) {
return new HttpException('Imagen not found', HttpStatus.NOT_FOUND);
}
return result
}
async updateImagen(id_imagen: number, imagen: UpdateImagenDto) {
const imagenFound = await this.imagenRepository.findOne({
where: {
id_imagen
}
});
if (!imagenFound) {
throw new HttpException('Imagen not found', HttpStatus.NOT_FOUND);
}
if (imagen.url) {
imagenFound.url = imagen.url;
}
const updatedImagen = Object.assign(imagenFound, imagen);
return this.imagenRepository.save(updatedImagen);
}
}
export const imageStorage = diskStorage({
destination: './uploads', // Carpeta donde se almacenarán las imágenes
filename: (req, file, callback) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9); // Genera un nombre único
callback(null, uniqueSuffix + extname(file.originalname)); // Usa la extensión original del archivo
},
});
+2 -70
View File
@@ -1,76 +1,8 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import 'dotenv/config';
import { DocsModule } from './docs/docs.module';
import { DataSource } from 'typeorm';
import { TipoPregunta } from './tipo_pregunta/entities/tipo_pregunta.entity';
import { ValidationPipe } from '@nestjs/common';
import { NestExpressApplication } from '@nestjs/platform-express';
import { join, resolve } from 'path';
async function bootstrap() {
//const app = await NestFactory.create(AppModule);
const app = await NestFactory.create<NestExpressApplication>(AppModule);
// Configuración de validación global
app.useGlobalPipes(
new ValidationPipe({
whitelist: true, // Elimina propiedades no decoradas
forbidNonWhitelisted: true, // Arroja error si hay propiedades no decoradas
transform: true, // Transforma los datos recibidos al tipo definido en el DTO
})
);
// CORS configuration
app.enableCors({
origin: '*',
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
allowedHeaders: ['Content-Type', 'Authorization'],
});
// Verifica que la carpeta uploads esté correctamente ubicada
const uploadsPath = resolve(__dirname, '..', 'uploads');
// Configurar los archivos estáticos para que se sirvan desde /uploads
app.useStaticAssets(uploadsPath, {
prefix: '/uploads', // Esto asegura que los archivos sean accesibles en http://localhost:4200/uploads
});
// Swagger setup
DocsModule.setupSwagger(app);
// Seed TipoPregunta data
const dataSource = app.get(DataSource);
const tiposPreguntaRepository = dataSource.getRepository(TipoPregunta);
// Check if data exists
const count = await tiposPreguntaRepository.count();
if (count === 0) {
// Create default tipos de pregunta
const tiposPregunta = [
{ tipo_pregunta: 'Texto' },
{ tipo_pregunta: 'Numero' },
{ tipo_pregunta: 'Radio' },
{ tipo_pregunta: 'Multiple' },
{ tipo_pregunta: 'Abierto' },
{ tipo_pregunta: 'Fecha' },
];
await tiposPreguntaRepository.save(tiposPregunta);
console.log('Tipos de pregunta creados');
}
// Start the server
await app.listen(process.env.PORT ?? 4200);
console.log('API en ejecución en http://localhost:4200');
console.log('Swagger disponible en http://localhost:4200/api-docs');
const app = await NestFactory.create(AppModule);
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
+8 -6
View File
@@ -1,9 +1,11 @@
import { IsString, MaxLength } from "class-validator";
import { IsString, IsNumber, IsNotEmpty, IsOptional } from 'class-validator';
export class CreateOpcionDto {
@IsString()
@MaxLength(50)
opcion: string;
@IsString()
@IsNotEmpty()
opcion: string;
}
@IsNumber()
@IsOptional()
posicion?: number;
}
+10 -3
View File
@@ -1,4 +1,11 @@
import { PartialType } from '@nestjs/mapped-types';
import { CreateOpcionDto } from './create-opcion.dto';
import { IsString, IsNumber, IsOptional } from 'class-validator';
export class UpdateOpcionDto extends PartialType(CreateOpcionDto) {}
export class UpdateOpcionDto {
@IsString()
@IsOptional()
opcion?: string;
@IsNumber()
@IsOptional()
posicion?: number;
}
+1 -1
View File
@@ -10,7 +10,7 @@ export class Opcion {
opcion: string;
@OneToMany(() => PreguntaOpcion, (preguntaOpcion) => preguntaOpcion.opcion)
preguntasOpciones: PreguntaOpcion[];
Preguntas: PreguntaOpcion[];
}
+19 -18
View File
@@ -2,40 +2,41 @@ import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/commo
import { OpcionService } from './opcion.service';
import { CreateOpcionDto } from './dto/create-opcion.dto';
import { UpdateOpcionDto } from './dto/update-opcion.dto';
import { OpcionApiDocumentation } from './opcion.documentation';
import { Pregunta } from 'src/pregunta/entities/pregunta.entity';
@Controller('opcion')
@OpcionApiDocumentation.ApiController
export class OpcionController {
constructor(private readonly opcionService: OpcionService) {}
@Post()
@OpcionApiDocumentation.ApiCreate
create(@Body() createOpcionDto: CreateOpcionDto) {
return this.opcionService.create(createOpcionDto);
@Post()
async create(
@Param('PreguntaId')PreguntaId:number,
@Body() createOpcionDto: CreateOpcionDto) {
return this.opcionService.create(PreguntaId,createOpcionDto);
}
@Get()
@OpcionApiDocumentation.ApiGetAll
findAll() {
return this.opcionService.findAll();
async findAll(
@Param('PreguntaId')PreguntaId:number
) {
return this.opcionService.findAllByPregunta(PreguntaId);
}
@Get(':id')
@OpcionApiDocumentation.ApiGetOne
findOne(@Param('id') id: string) {
return this.opcionService.findOne(+id);
async findOne(
@Param('preguntaId') preguntaId: number,
@Param('opcionId') OpcionId: number) {
return this.opcionService.findOne(preguntaId, OpcionId);
}
@Patch(':id')
@OpcionApiDocumentation.ApiUpdate
update(@Param('id') id: string, @Body() updateOpcionDto: UpdateOpcionDto) {
return this.opcionService.update(+id, updateOpcionDto);
async pdate(@Param('preguntaId') preguntaId:number ,@Param('opcionId') id: number, @Body() updateOpcionDto: UpdateOpcionDto) {
return this.opcionService.update(preguntaId, id,updateOpcionDto);
}
@Delete(':id')
@OpcionApiDocumentation.ApiRemove
remove(@Param('id') id: string) {
return this.opcionService.remove(+id);
async remove(@Param('preguntaId') preguntaId:number ,@Param('opcionId') id: number) {
return this.opcionService.remove(preguntaId, id);
}
}
-153
View File
@@ -1,153 +0,0 @@
import { ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
import { applyDecorators } from '@nestjs/common';
export class OpcionApiDocumentation {
// Decoradores para toda la clase del controlador
static ApiController = ApiTags('Opciones');
// Documentación para crear una opción
static ApiCreate = applyDecorators(
ApiOperation({
summary: 'Crear una nueva opción',
description: 'Crea una nueva opción para una pregunta'
}),
ApiBody({
description: 'Datos de la opción a crear',
schema: {
type: 'object',
required: ['opcion'],
properties: {
opcion: { type: 'string', example: 'Si' }
}
}
}),
ApiResponse({
status: 201,
description: 'Opción creada correctamente',
schema: {
type: 'object',
properties: {
id_opcion: { type: 'number', example: 1 },
opcion: { type: 'string', example: 'Si' }
}
}
}),
ApiResponse({ status: 400, description: 'Datos de la opción inválidos' }),
ApiResponse({ status: 500, description: 'Error interno del servidor' })
);
// Documentación para obtener todas las opciones
static ApiGetAll = applyDecorators(
ApiOperation({
summary: 'Obtener todas las opciones',
description: 'Retorna una lista de todas las opciones registradas'
}),
ApiResponse({
status: 200,
description: 'Lista de opciones obtenida correctamente',
schema: {
type: 'array',
items: {
type: 'object',
properties: {
id_opcion: { type: 'number', example: 1 },
opcion: { type: 'string', example: 'Si' }
}
}
}
}),
ApiResponse({ status: 500, description: 'Error interno del servidor' })
);
// Documentación para obtener una opción por ID
static ApiGetOne = applyDecorators(
ApiOperation({
summary: 'Obtener una opción por ID',
description: 'Retorna una opción específica por su ID'
}),
ApiParam({
name: 'id',
description: 'ID de la opción',
required: true,
type: 'number',
example: 1
}),
ApiResponse({
status: 200,
description: 'Opción obtenida correctamente',
schema: {
type: 'object',
properties: {
id_opcion: { type: 'number', example: 1 },
opcion: { type: 'string', example: 'Si' }
}
}
}),
ApiResponse({ status: 404, description: 'Opción no encontrada' }),
ApiResponse({ status: 500, description: 'Error interno del servidor' })
);
// Documentación para actualizar una opción
static ApiUpdate = applyDecorators(
ApiOperation({
summary: 'Actualizar una opción',
description: 'Actualiza los datos de una opción existente'
}),
ApiParam({
name: 'id',
description: 'ID de la opción a actualizar',
required: true,
type: 'number',
example: 1
}),
ApiBody({
description: 'Datos a actualizar de la opción',
schema: {
type: 'object',
properties: {
opcion: { type: 'string', example: 'Opción actualizada' }
}
}
}),
ApiResponse({
status: 200,
description: 'Opción actualizada correctamente',
schema: {
type: 'object',
properties: {
affected: { type: 'number', example: 1 }
}
}
}),
ApiResponse({ status: 400, description: 'Datos de actualización inválidos' }),
ApiResponse({ status: 404, description: 'Opción no encontrada' }),
ApiResponse({ status: 500, description: 'Error interno del servidor' })
);
// Documentación para eliminar una opción
static ApiRemove = applyDecorators(
ApiOperation({
summary: 'Eliminar una opción',
description: 'Elimina permanentemente una opción por su ID'
}),
ApiParam({
name: 'id',
description: 'ID de la opción a eliminar',
required: true,
type: 'number',
example: 1
}),
ApiResponse({
status: 200,
description: 'Opción eliminada correctamente',
schema: {
type: 'object',
properties: {
affected: { type: 'number', example: 1 }
}
}
}),
ApiResponse({ status: 404, description: 'Opción no encontrada' }),
ApiResponse({ status: 500, description: 'Error interno del servidor' })
);
}
+11 -5
View File
@@ -1,13 +1,19 @@
import { Module } from '@nestjs/common';
import { Module, forwardRef } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { OpcionService } from './opcion.service';
import { OpcionController } from './opcion.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Opcion } from './entities/opcion.entity';
import { PreguntaOpcion } from '../pregunta_opcion/entities/pregunta_opcion.entity';
import { PreguntaModule } from '../pregunta/pregunta.module';
import { Pregunta } from '../pregunta/entities/pregunta.entity';
@Module({
imports: [TypeOrmModule.forFeature([Opcion])], // Importa el repositorio de Opcion
imports: [
TypeOrmModule.forFeature([Opcion, Pregunta, PreguntaOpcion]), // Importar todas las entidades necesarias
forwardRef(() => PreguntaModule), // Usar forwardRef para dependencia circular
],
controllers: [OpcionController],
providers: [OpcionService],
exports: [OpcionService, TypeOrmModule]
exports: [OpcionService, TypeOrmModule], // Exportar TypeOrmModule si otros módulos necesitan los repositorios
})
export class OpcionModule {}
export class OpcionModule {}
+142 -24
View File
@@ -1,40 +1,158 @@
import { Injectable } from '@nestjs/common';
import { CreateOpcionDto } from './dto/create-opcion.dto';
import { UpdateOpcionDto } from './dto/update-opcion.dto';
import { Opcion } from './entities/opcion.entity';
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Opcion } from './entities/opcion.entity';
import { Pregunta } from '../pregunta/entities/pregunta.entity';
import { CreateOpcionDto } from './dto/create-opcion.dto';
import { PreguntaOpcion } from 'src/pregunta_opcion/entities/pregunta_opcion.entity';
import { UpdateOpcionDto } from './dto/update-opcion.dto';
@Injectable()
export class OpcionService {
constructor(
@InjectRepository(Opcion)
private repository: Repository<Opcion>,
){}
private readonly opcionRepository: Repository<Opcion>,
@InjectRepository(Pregunta)
private readonly preguntaRepository: Repository<Pregunta>,
@InjectRepository(PreguntaOpcion)
private readonly preguntaOpcionRepository: Repository<PreguntaOpcion>,
) {}
create(createOpcionDto: CreateOpcionDto) {
return this.repository.save(createOpcionDto);
}
async create(preguntaId: number, createOpcionDto: CreateOpcionDto) {
// Verificar que la pregunta exista y sea cerrada
const pregunta = await this.preguntaRepository.findOne({
where: { id_pregunta: preguntaId },
relations: ['tipo_pregunta'],
});
/*
create():Promise<Opcion> {
return this.repository.save(this.repository.create());
}
*/
findAll() {
return `This action returns all opcion`;
if (!pregunta) {
throw new NotFoundException('Pregunta no encontrada');
}
if (pregunta.tipo_pregunta.tipo_pregunta !== 'Cerrada') {
throw new Error('Solo se pueden agregar opciones a preguntas cerradas');
}
// Crear la opción
const opcion = this.opcionRepository.create({
opcion: createOpcionDto.opcion,
});
const savedOpcion = await this.opcionRepository.save(opcion);
// Crear la relación entre pregunta y opción
const preguntaOpcion = this.preguntaOpcionRepository.create({
pregunta: { id_pregunta: preguntaId },
opcion: savedOpcion,
posicion: createOpcionDto.posicion || 0, // Asignar posición
});
await this.preguntaOpcionRepository.save(preguntaOpcion);
// Actualizar el contador de opciones en la pregunta
await this.preguntaRepository.increment(
{ id_pregunta: preguntaId },
'contador_opcion',
1
);
return savedOpcion;
}
findOne(id: number) {
return `This action returns a #${id} opcion`;
async findAllByPregunta(preguntaId: number) {
return this.preguntaOpcionRepository.find({
where: { pregunta: { id_pregunta: preguntaId } },
relations: ['opcion'],
order: { posicion: 'ASC' },
});
}
update(id: number, updateOpcionDto: UpdateOpcionDto) {
return `This action updates a #${id} opcion`;
async findOne(preguntaId: number, id: number) {
const preguntaOpcion = await this.preguntaOpcionRepository.findOne({
where: {
pregunta: { id_pregunta: preguntaId },
opcion: { id_opcion: id },
},
relations: ['opcion'],
});
if (!preguntaOpcion) {
throw new NotFoundException('Opción no encontrada para esta pregunta');
}
return preguntaOpcion.opcion;
}
remove(id: number) {
return `This action removes a #${id} opcion`;
async update(
preguntaId: number,
id: number,
updateOpcionDto: UpdateOpcionDto
) {
// Verificar que la relación exista
const preguntaOpcion = await this.preguntaOpcionRepository.findOne({
where: {
pregunta: { id_pregunta: preguntaId },
opcion: { id_opcion: id },
},
relations: ['opcion'],
});
if (!preguntaOpcion) {
throw new NotFoundException('Opción no encontrada para esta pregunta');
}
// Actualizar posición si se proporciona
if (updateOpcionDto.posicion !== undefined) {
await this.preguntaOpcionRepository.update(
{ id_pregunta_opcion: preguntaOpcion.id_pregunta_opcion },
{ posicion: updateOpcionDto.posicion }
);
}
// Actualizar texto de la opción
if (updateOpcionDto.opcion) {
await this.opcionRepository.update(
{ id_opcion: id },
{ opcion: updateOpcionDto.opcion }
);
}
return this.findOne(preguntaId, id);
}
}
async remove(preguntaId: number, id: number) {
// Verificar que la relación exista
const preguntaOpcion = await this.preguntaOpcionRepository.findOne({
where: {
pregunta: { id_pregunta: preguntaId },
opcion: { id_opcion: id },
},
});
if (!preguntaOpcion) {
throw new NotFoundException('Opción no encontrada para esta pregunta');
}
// Eliminar la relación primero
await this.preguntaOpcionRepository.remove(preguntaOpcion);
// Eliminar la opción (si no está siendo usada en otras preguntas)
const count = await this.preguntaOpcionRepository.count({
where: { opcion: { id_opcion: id } },
});
if (count === 0) {
await this.opcionRepository.delete(id);
}
// Actualizar el contador de opciones en la pregunta
await this.preguntaRepository.decrement(
{ id_pregunta: preguntaId },
'contador_opcion',
1
);
return { message: 'Opción eliminada correctamente' };
}
}
@@ -1,22 +1,7 @@
import { IsEmail, IsNotEmpty, IsNumber } from "class-validator";
import { ApiProperty } from "@nestjs/swagger";
import { IsEmail } from "class-validator";
export class CreateParticipanteDto {
@ApiProperty({
description: 'Correo electrónico del participante',
example: 'usuario@ejemplo.com',
required: true
})
@IsEmail({}, { message: 'El correo electrónico debe tener un formato válido' })
@IsNotEmpty({ message: 'El correo electrónico es requerido' })
correo: string;
@ApiProperty({
description: 'ID del tipo de usuario',
example: 1,
required: true
})
@IsNumber({}, { message: 'El ID del tipo de usuario debe ser un número' })
@IsNotEmpty({ message: 'El ID del tipo de usuario es requerido' })
id_tipo_user: number;
@IsEmail()
correo: string
id_tipo_user: number
}
@@ -1,22 +1,3 @@
import { IsEmail, IsOptional, IsNumber } from "class-validator";
import { ApiProperty } from "@nestjs/swagger";
export class UpdateParticipanteDto {
@ApiProperty({
description: 'Nuevo correo electrónico del participante',
example: 'nuevo_correo@ejemplo.com',
required: false
})
@IsEmail({}, { message: 'El correo electrónico debe tener un formato válido' })
@IsOptional()
correo?: string;
@ApiProperty({
description: 'Nuevo ID del tipo de usuario',
example: 2,
required: false
})
@IsNumber({}, { message: 'El ID del tipo de usuario debe ser un número' })
@IsOptional()
id_tipo_user?: number;
correo: string
}
+28 -10
View File
@@ -3,40 +3,58 @@ import { Participante } from './participante.entity';
import { ParticipanteService } from './participante.service';
import { CreateParticipanteDto } from './dto/create-participante.dto';
import { UpdateParticipanteDto } from './dto/update.participante.dto';
import { ParticipanteApiDocumentation } from './participante.documentation';
import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiBody } from '@nestjs/swagger';
@ParticipanteApiDocumentation.ApiController
@ApiTags('Participantes') // Agrupa los endpoints en Swagger
@Controller('participante')
export class ParticipanteController {
constructor(private participanteService: ParticipanteService) {}
@ParticipanteApiDocumentation.ApiGetAll
@ApiOperation({ summary: 'Obtener todos los participantes' })
@ApiResponse({ status: 200, description: 'Lista de participantes obtenida correctamente.' })
@Get()
getParticipantes(): Promise<Participante[]> {
return this.participanteService.getParticipantes();
return this.participanteService.getParticipantes()
}
@ParticipanteApiDocumentation.ApiGetOne
@Get(':id')
@ApiOperation({ summary: 'Obtener un participante por ID' })
@ApiParam({ name: 'id', description: 'ID del participante', example: 1 })
@ApiResponse({ status: 200, description: 'Participante obtenido correctamente.' })
@ApiResponse({ status: 404, description: 'Participante no encontrado.' })
getParticipante(@Param('id', ParseIntPipe) id: number) {
return this.participanteService.getParticipante(id);
}
@ParticipanteApiDocumentation.ApiCreate
@Post()
@ApiOperation({ summary: 'Registrar un nuevo participante' })
@ApiBody({
description: 'Datos del participante a registrar',
schema: {
type: 'object',
properties: {
correo: { type: 'string', example: 'user@example.com' },
id_tipo_user: { type: 'integer', example: 2 }
}
}
})
@ApiResponse({ status: 201, description: 'Participante registrado exitosamente.' })
@ApiResponse({ status: 400, description: 'Datos inválidos.' })
createParticipante(@Body() newParticipante: CreateParticipanteDto) {
return this.participanteService.createParticipante(newParticipante);
}
@ParticipanteApiDocumentation.ApiRemove
@Delete(':id')
deleteParticipante(@Param('id', ParseIntPipe) id: number) {
return this.participanteService.deleteParticipante(id);
return this.participanteService.deleteParticipante(id)
}
@ParticipanteApiDocumentation.ApiUpdate
@Patch(':id')
updateParticipante(@Param('id', ParseIntPipe) id: number, @Body() participante: UpdateParticipanteDto) {
updateParticipante(@Param('correo') id: number, @Body() participante: UpdateParticipanteDto) {
return this.participanteService.updateParticipante(id, participante);
}
}
@@ -1,211 +0,0 @@
import { ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
import { applyDecorators } from '@nestjs/common';
export class ParticipanteApiDocumentation {
// Decorador para toda la clase del controlador
static ApiController = ApiTags('Participantes');
// Documentación para crear un participante
static ApiCreate = applyDecorators(
ApiOperation({
summary: 'Registrar un nuevo participante',
description: 'Crea un nuevo registro de participante en el sistema'
}),
ApiBody({
description: 'Datos del participante a registrar',
schema: {
type: 'object',
required: ['correo', 'id_tipo_user'],
properties: {
correo: {
type: 'string',
format: 'email',
example: 'usuario@ejemplo.com',
description: 'Correo electrónico del participante'
},
id_tipo_user: {
type: 'integer',
example: 1,
description: 'ID del tipo de usuario'
}
}
}
}),
ApiResponse({
status: 201,
description: 'Participante registrado exitosamente',
schema: {
type: 'object',
properties: {
id_participante: { type: 'number', example: 1 },
correo: { type: 'string', example: 'usuario@ejemplo.com' },
id_tipo_user: { type: 'number', example: 1 }
}
}
}),
ApiResponse({ status: 400, description: 'Datos del participante inválidos' }),
ApiResponse({ status: 409, description: 'El participante ya existe' })
);
// Documentación para obtener todos los participantes
static ApiGetAll = applyDecorators(
ApiOperation({
summary: 'Obtener todos los participantes',
description: 'Retorna una lista de todos los participantes registrados'
}),
ApiResponse({
status: 200,
description: 'Lista de participantes obtenida correctamente',
schema: {
type: 'array',
items: {
type: 'object',
properties: {
id_participante: { type: 'number', example: 1 },
correo: { type: 'string', example: 'usuario@ejemplo.com' },
id_tipo_user: { type: 'number', example: 1 },
tipo_user: {
type: 'object',
properties: {
id_tipo_user: { type: 'number', example: 1 },
tipo_user: { type: 'string', example: 'Estudiante' }
}
},
participanteEventos: {
type: 'array',
items: {
type: 'object',
properties: {
id_participante_evento: { type: 'number', example: 1 },
id_participante: { type: 'number', example: 1 },
id_evento: { type: 'number', example: 1 },
fecha_inscripcion: { type: 'string', format: 'date-time', example: '2025-04-01T10:00:00Z' },
estatus: { type: 'boolean', example: true }
}
}
}
}
}
}
})
);
// Documentación para obtener un participante por ID
static ApiGetOne = applyDecorators(
ApiOperation({
summary: 'Obtener un participante por ID',
description: 'Retorna los datos de un participante específico según su ID'
}),
ApiParam({
name: 'id',
description: 'ID del participante',
type: 'number',
example: 1
}),
ApiResponse({
status: 200,
description: 'Participante obtenido correctamente',
schema: {
type: 'object',
properties: {
id_participante: { type: 'number', example: 1 },
correo: { type: 'string', example: 'usuario@ejemplo.com' },
id_tipo_user: { type: 'number', example: 1 },
tipo_user: {
type: 'object',
properties: {
id_tipo_user: { type: 'number', example: 1 },
tipo_user: { type: 'string', example: 'Estudiante' }
}
},
participanteEventos: {
type: 'array',
items: {
type: 'object',
properties: {
id_participante_evento: { type: 'number', example: 1 },
id_participante: { type: 'number', example: 1 },
id_evento: { type: 'number', example: 1 },
fecha_inscripcion: { type: 'string', format: 'date-time', example: '2025-04-01T10:00:00Z' },
estatus: { type: 'boolean', example: true }
}
}
}
}
}
}),
ApiResponse({ status: 404, description: 'Participante no encontrado' })
);
// Documentación para actualizar un participante
static ApiUpdate = applyDecorators(
ApiOperation({
summary: 'Actualizar datos de un participante',
description: 'Actualiza la información de un participante existente'
}),
ApiParam({
name: 'id',
description: 'ID del participante a actualizar',
type: 'number',
example: 1
}),
ApiBody({
description: 'Datos a actualizar del participante',
schema: {
type: 'object',
properties: {
correo: {
type: 'string',
format: 'email',
example: 'nuevo_correo@ejemplo.com',
description: 'Nuevo correo electrónico del participante'
},
id_tipo_user: {
type: 'integer',
example: 2,
description: 'Nuevo tipo de usuario'
}
}
}
}),
ApiResponse({
status: 200,
description: 'Participante actualizado correctamente',
schema: {
type: 'object',
properties: {
id_participante: { type: 'number', example: 1 },
correo: { type: 'string', example: 'nuevo_correo@ejemplo.com' },
id_tipo_user: { type: 'number', example: 2 }
}
}
}),
ApiResponse({ status: 400, description: 'Datos de actualización inválidos' }),
ApiResponse({ status: 404, description: 'Participante no encontrado' })
);
// Documentación para eliminar un participante
static ApiRemove = applyDecorators(
ApiOperation({
summary: 'Eliminar un participante',
description: 'Elimina permanentemente un participante por su ID'
}),
ApiParam({
name: 'id',
description: 'ID del participante a eliminar',
type: 'number',
example: 1
}),
ApiResponse({
status: 200,
description: 'Participante eliminado correctamente',
schema: {
type: 'object',
properties: {
affected: { type: 'number', example: 1 }
}
}
}),
ApiResponse({ status: 404, description: 'Participante no encontrado' })
);
}
+8 -18
View File
@@ -1,8 +1,7 @@
import { CuestionarioRespondido } from "src/cuestionario_respondido/entities/cuestionario_respondido.entity";
import { Evento } from "src/evento/evento.entity";
import { ParticipanteEvento } from "src/participante_evento/participante_evento.entity";
import { TipoUser } from "src/tipo_user/tipo_user.entity";
import { Column, Entity, JoinColumn, JoinTable, ManyToMany, ManyToOne, OneToMany, PrimaryGeneratedColumn } from "typeorm";
import { Column, Entity, JoinColumn, ManyToOne, OneToMany, PrimaryGeneratedColumn } from "typeorm";
@Entity()
export class Participante {
@@ -15,35 +14,26 @@ export class Participante {
@Column()
id_tipo_user: number
@OneToMany(() => CuestionarioRespondido, cuestionarioRespondido => cuestionarioRespondido.participante )
cuestionarioRespondidos:CuestionarioRespondido[];
//Relacion con tipo usuario
@ManyToOne(() => TipoUser, tipoUser => tipoUser.participante)
@JoinColumn({ name: 'participante_id' }) //nombre de la relacion
tipo_user: TipoUser
tipo_user: TipoUser[]
/*
@ManyToOne(() => Administrador, (admin) => admin.eventos)
@JoinColumn({ name: "id_administrador" })
administrador: Administrador;
@ManyToMany(() => Evento, evento => evento.participantes)
@JoinTable({
name: "participante_evento",
joinColumn: { name: "id_participante", referencedColumnName: "id_participante" },
inverseJoinColumn: { name: "id_evento", referencedColumnName: "id_evento" }
})
eventos: Evento[];
@OneToMany(() => ParticipanteEvento, (pe) => pe.evento)
participantes: ParticipanteEvento[];
@OneToMany(() => Asistencia, (asistencia) => asistencia.evento)
asistencias: Asistencia[];
*/
@OneToMany(() => CuestionarioRespondido, cuestionario => cuestionario.participante)
cuestionariosRespondidos: CuestionarioRespondido[];
@OneToMany(() => ParticipanteEvento, participanteEvento => participanteEvento.participante)
participanteEventos: ParticipanteEvento[];
}
+24 -74
View File
@@ -1,99 +1,62 @@
import { BadRequestException, ConflictException, Injectable, NotFoundException } from '@nestjs/common';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Participante } from './participante.entity';
import { Repository } from 'typeorm';
import { CreateParticipanteDto } from './dto/create-participante.dto';
import { UpdateParticipanteDto } from './dto/update.participante.dto';
@Injectable()
export class ParticipanteService {
constructor(
@InjectRepository(Participante) private participanteRepository: Repository<Participante>
) {}
/**
* Crea un nuevo participante
* @param participante Datos del participante a crear
* @returns El participante creado
* @throws ConflictException si ya existe un participante con el mismo correo
*/
async createParticipante(participante: CreateParticipanteDto): Promise<Participante> {
// Verificar si ya existe un participante con el mismo correo
async createParticipante(participante: CreateParticipanteDto) {
const participanteFound = await this.participanteRepository.findOne({
where: {
correo: participante.correo
}
});
})
if (participanteFound) {
throw new ConflictException(`Ya existe un participante con el correo ${participante.correo}`);
}
if (participanteFound)
return new HttpException('Participante already exists', HttpStatus.CONFLICT)
const nuevoParticipante = this.participanteRepository.create(participante);
return this.participanteRepository.save(nuevoParticipante);
return this.participanteRepository.save(participante)
}
/**
* Obtiene todos los participantes
* @returns Lista de participantes con sus relaciones
*/
async getParticipantes(): Promise<Participante[]> {
getParticipantes() {
return this.participanteRepository.find({
relations: ['tipo_user', 'participanteEventos']
});
relations: ['tipo_user']
})
}
/**
* Obtiene un participante por su ID
* @param id_participante ID del participante a buscar
* @returns El participante encontrado con sus relaciones
* @throws NotFoundException si no se encuentra el participante
*/
async getParticipante(id_participante: number): Promise<Participante> {
async getParticipante(id_participante: number) {
const participanteFound = await this.participanteRepository.findOne({
where: {
id_participante
},
relations: ['tipo_user', 'participanteEventos']
});
relations: ['tipo_user']
})
if (!participanteFound) {
throw new NotFoundException(`Participante con ID ${id_participante} no encontrado`);
}
if (!participanteFound)
return new HttpException('Participante not found', HttpStatus.NOT_FOUND);
return participanteFound;
}
/**
* Elimina un participante por su ID
* @param id_participante ID del participante a eliminar
* @returns Resultado de la eliminación
* @throws NotFoundException si no se encuentra el participante
*/
async deleteParticipante(id_participante: number) {
const result = await this.participanteRepository.delete({ id_participante });
const result = await this.participanteRepository.delete({ id_participante })
if (result.affected === 0) {
throw new NotFoundException(`Participante con ID ${id_participante} no encontrado`);
return new HttpException('Participante not found', HttpStatus.NOT_FOUND);
}
return {
success: true,
message: `Participante con ID ${id_participante} eliminado correctamente`,
affected: result.affected
};
return result;
}
/**
* Actualiza los datos de un participante
* @param id_participante ID del participante a actualizar
* @param participante Datos actualizados del participante
* @returns El participante actualizado
* @throws NotFoundException si no se encuentra el participante
* @throws BadRequestException si los datos son inválidos
*/
async updateParticipante(id_participante: number, participante: UpdateParticipanteDto): Promise<Participante> {
// Verificar si el participante existe
async updateParticipante(id_participante: number, participante: UpdateParticipanteDto) {
const participanteFound = await this.participanteRepository.findOne({
where: {
id_participante
@@ -101,24 +64,11 @@ export class ParticipanteService {
});
if (!participanteFound) {
throw new NotFoundException(`Participante con ID ${id_participante} no encontrado`);
return new HttpException('Participante not found', HttpStatus.NOT_FOUND)
}
// Si se está actualizando el correo, verificar que no exista otro participante con ese correo
if (participante.correo && participante.correo !== participanteFound.correo) {
const existingParticipante = await this.participanteRepository.findOne({
where: {
correo: participante.correo
}
});
if (existingParticipante && existingParticipante.id_participante !== id_participante) {
throw new ConflictException(`Ya existe otro participante con el correo ${participante.correo}`);
}
}
// Actualizar el participante
const updateParticipante = Object.assign(participanteFound, participante);
return this.participanteRepository.save(updateParticipante);
const updateParticipante = Object.assign(participanteFound, participante)
return this.participanteRepository.save(updateParticipante)
}
}
@@ -3,182 +3,32 @@ import { ParticipanteEventoService } from './participante_evento.service';
import { ParticipanteEvento } from './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';
@ApiTags('Participantes-Eventos')
@Controller('participante-evento')
export class ParticipanteEventoController {
constructor(private participanteEventoService: ParticipanteEventoService) {}
@ApiOperation({ summary: 'Obtener participantes por evento' })
@ApiParam({ name: 'idEvento', description: 'ID del evento', type: 'number' })
@ApiResponse({
status: 200,
description: 'Lista de participantes que están registrados en el evento',
schema: {
type: 'array',
items: {
type: 'object',
properties: {
id_participante_evento: { type: 'number' },
id_participante: { type: 'number' },
id_evento: { type: 'number' },
fecha_inscripcion: { type: 'string', format: 'date-time' },
estatus: { type: 'boolean' },
fecha_asistencia: { type: 'string', format: 'date-time', nullable: true },
participante: {
type: 'object',
properties: {
id_participante: { type: 'number' },
correo: { type: 'string' },
id_tipo_user: { type: 'number' }
}
}
}
}
}
})
@ApiResponse({ status: 404, description: 'Evento no encontrado' })
@Get('evento/:idEvento')
getParticipantesPorEvento(@Param('idEvento', ParseIntPipe) idEvento: number) {
return this.participanteEventoService.getParticipantesPorEvento(idEvento);
}
@ApiOperation({ summary: 'Obtener eventos por participante' })
@ApiParam({ name: 'idParticipante', description: 'ID del participante', type: 'number' })
@ApiResponse({
status: 200,
description: 'Lista de eventos en los que está registrado el participante',
schema: {
type: 'array',
items: {
type: 'object',
properties: {
id_participante_evento: { type: 'number' },
id_participante: { type: 'number' },
id_evento: { type: 'number' },
fecha_inscripcion: { type: 'string', format: 'date-time' },
estatus: { type: 'boolean' },
fecha_asistencia: { type: 'string', format: 'date-time', nullable: true },
evento: {
type: 'object',
properties: {
id_evento: { type: 'number' },
nombre_evento: { type: 'string' },
tipo_evento: { type: 'string' },
fecha_inicio: { type: 'string', format: 'date-time' },
fecha_fin: { type: 'string', format: 'date-time' }
}
}
}
}
}
})
@ApiResponse({ status: 404, description: 'Participante no encontrado' })
@Get('participante/:idParticipante')
getEventosPorParticipante(@Param('idParticipante', ParseIntPipe) idParticipante: number) {
return this.participanteEventoService.getEventosPorParticipante(idParticipante);
}
@ApiOperation({ summary: 'Obtener un registro específico por IDs de participante y evento' })
@ApiParam({ name: 'idParticipante', description: 'ID del participante', type: 'number' })
@ApiParam({ name: 'idEvento', description: 'ID del evento', type: 'number' })
@ApiResponse({
status: 200,
description: 'Registro participante-evento obtenido correctamente'
})
@ApiResponse({ status: 404, description: 'Registro no encontrado' })
@Get(':idParticipante/:idEvento')
getParticipanteEventoEspecifico(
@Param('idParticipante', ParseIntPipe) idParticipante: number,
@Param('idEvento', ParseIntPipe) idEvento: number
) {
return this.participanteEventoService.getParticipanteEventoEspecifico(idParticipante, idEvento)
}
@ApiOperation({ summary: 'Obtener un registro participante-evento por ID' })
@ApiParam({ name: 'id', description: 'ID del participante', type: 'number' })
@ApiResponse({
status: 200,
description: 'Registro participante-evento obtenido correctamente'
})
@ApiResponse({ status: 404, description: 'Registro no encontrado' })
@Get(':id')
getParticipanteEvento(@Param('id', ParseIntPipe) id: number) {
return this.participanteEventoService.getParticipanteEvento(id)
}
@ApiOperation({ summary: 'Obtener todos los registros de participante-evento' })
@ApiResponse({
status: 200,
description: 'Lista de todos los registros participante-evento con sus relaciones',
schema: {
type: 'array',
items: {
type: 'object',
properties: {
id_participante_evento: { type: 'number' },
id_participante: { type: 'number' },
id_evento: { type: 'number' },
fecha_inscripcion: { type: 'string', format: 'date-time' },
estatus: { type: 'boolean' },
fecha_asistencia: { type: 'string', format: 'date-time', nullable: true }
}
}
}
})
@Get()
getParticipantesEvento(): Promise<ParticipanteEvento[]> {
return this.participanteEventoService.getParticipantesEvento()
}
@ApiOperation({ summary: 'Registrar un participante en un evento' })
@ApiResponse({
status: 201,
description: 'Participante registrado en el evento correctamente'
})
@ApiResponse({ status: 409, description: 'El participante ya está registrado en este evento' })
@Get(':id')
getParticipanteEvento(@Param('id', ParseIntPipe) id: number) {
return this.participanteEventoService.getParticipanteEvento(id)
}
@Post()
createParticipanteEvento(@Body() newParticipanteEvento: CreateParticipanteEventoDto) {
return this.participanteEventoService.createParticipanteEvento(newParticipanteEvento)
}
@ApiOperation({ summary: 'Registrar asistencia de un participante a un evento' })
@ApiParam({ name: 'idParticipante', description: 'ID del participante', type: 'number' })
@ApiParam({ name: 'idEvento', description: 'ID del evento', type: 'number' })
@ApiResponse({
status: 200,
description: 'Asistencia registrada correctamente'
})
@ApiResponse({ status: 404, description: 'Registro no encontrado' })
@Post('asistencia/:idParticipante/:idEvento')
registrarAsistencia(
@Param('idParticipante', ParseIntPipe) idParticipante: number,
@Param('idEvento', ParseIntPipe) idEvento: number
) {
return this.participanteEventoService.registrarAsistencia(idParticipante, idEvento)
}
@ApiOperation({ summary: 'Eliminar un registro participante-evento' })
@ApiParam({ name: 'id', description: 'ID del participante', type: 'number' })
@ApiResponse({
status: 200,
description: 'Registro eliminado correctamente'
})
@ApiResponse({ status: 404, description: 'Registro no encontrado' })
@Delete(':id')
deleteParticipanteEvento(@Param('id', ParseIntPipe) id: number) {
return this.participanteEventoService.deleteParticipanteEvento(id)
}
@ApiOperation({ summary: 'Actualizar un registro participante-evento' })
@ApiParam({ name: 'id', description: 'ID del participante', type: 'number' })
@ApiResponse({
status: 200,
description: 'Registro actualizado correctamente'
})
@ApiResponse({ status: 404, description: 'Registro no encontrado' })
@Patch(':id')
updateParticipanteEvento(@Param('id', ParseIntPipe) id_participante: number, id_evento: number, @Body() participanteEvento: UpdateParticipanteEventoDto) {
return this.participanteEventoService.updateParticipanteEvento(id_participante, id_evento, participanteEvento)
@@ -1,5 +1,4 @@
import { Evento } from "src/evento/evento.entity";
import { Participante } from "src/participante/participante.entity";
import { Column, Entity, JoinColumn, ManyToOne, PrimaryColumn, PrimaryGeneratedColumn } from "typeorm";
@Entity()
@@ -15,27 +14,20 @@ export class ParticipanteEvento {
@Column()
estatus: boolean
@Column({ nullable: true, type: 'datetime' })
fecha_asistencia: Date
/*
@OneToOne(() => Qr, (qr) => qr.participanteEvento)
qr: Qr;
@OneToMany(() => ParticipanteEvento, (pe) => pe.evento)
participantes: ParticipanteEvento[];
@ManyToOne(() => Participante, (participante) => participante.eventos)
@JoinColumn({ name: "id_participante" })
participante: Participante;
@ManyToOne(() => Evento, (evento) => evento.participantes)
@JoinColumn({ name: "id_evento" })
evento: Evento;
*/
@ManyToOne(() => Participante, participante => participante.participanteEventos)
@JoinColumn({ name: "id_participante" })
participante: Participante;
@ManyToOne(() => Evento, evento => evento.participanteEventos)
@ManyToOne(() => Evento, (evento) => evento.participantes)
@JoinColumn({ name: "id_evento" })
evento: Evento;
}
@@ -4,12 +4,11 @@ import { ParticipanteEventoController } from './participante_evento.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ParticipanteEvento } from './participante_evento.entity';
import { Evento } from 'src/evento/evento.entity';
import { Participante } from 'src/participante/participante.entity';
@Module({
imports: [TypeOrmModule.forFeature([ParticipanteEvento, Evento, Participante])],
imports: [TypeOrmModule.forFeature([ParticipanteEvento]), Evento],
controllers: [ParticipanteEventoController],
providers: [ParticipanteEventoService],
exports: [ParticipanteEventoService],
exports:[TypeOrmModule,ParticipanteEventoService]
})
export class ParticipanteEventoModule {}
@@ -4,16 +4,12 @@ import { ParticipanteEvento } from './participante_evento.entity';
import { Repository } from 'typeorm';
import { CreateParticipanteEventoDto } from './dto/create-participante_evento.dto';
import { UpdateParticipanteEventoDto } from './dto/update.participante_evento.dto';
import { Evento } from 'src/evento/evento.entity';
import { Participante } from 'src/participante/participante.entity';
@Injectable()
export class ParticipanteEventoService {
constructor(
@InjectRepository(ParticipanteEvento) private participanteEventoRepository: Repository<ParticipanteEvento>,
@InjectRepository(Evento) private eventoRepository: Repository<Evento>,
@InjectRepository(Participante) private participanteRepository: Repository<Participante>
@InjectRepository(ParticipanteEvento) private participanteEventoRepository: Repository<ParticipanteEvento>
) {}
async createParticipanteEvento(participanteEvento: CreateParticipanteEventoDto) {
@@ -34,7 +30,7 @@ export class ParticipanteEventoService {
getParticipantesEvento() {
return this.participanteEventoRepository.find({
relations: ['evento', 'participante']
relations: ['evento']
})
}
@@ -43,7 +39,7 @@ export class ParticipanteEventoService {
where: {
id_participante
},
relations: ['evento', 'participante']
relations: ['evento']
})
if (!participante_eventoFound) {
@@ -53,70 +49,6 @@ export class ParticipanteEventoService {
return participante_eventoFound
}
async getParticipanteEventoEspecifico(id_participante: number, id_evento: number) {
const participante_eventoFound = await this.participanteEventoRepository.findOne({
where: {
id_participante,
id_evento
},
relations: ['evento', 'participante']
})
if (!participante_eventoFound) {
return new HttpException('Registro no encontrado', HttpStatus.NOT_FOUND);
}
return participante_eventoFound
}
/**
* Obtiene todos los participantes de un evento específico
* @param id_evento ID del evento
* @returns Lista de registros participante-evento con información del participante
*/
async getParticipantesPorEvento(id_evento: number) {
// Verificar si el evento existe
const eventoExists = await this.eventoRepository.findOne({
where: { id_evento }
});
if (!eventoExists) {
throw new HttpException(`Evento con ID ${id_evento} no encontrado`, HttpStatus.NOT_FOUND);
}
// Obtener todos los registros participante-evento para este evento
const participantesEvento = await this.participanteEventoRepository.find({
where: { id_evento },
relations: ['participante']
});
return participantesEvento;
}
/**
* Obtiene todos los eventos de un participante específico
* @param id_participante ID del participante
* @returns Lista de registros participante-evento con información del evento
*/
async getEventosPorParticipante(id_participante: number) {
// Verificar si el participante existe
const participanteExists = await this.participanteRepository.findOne({
where: { id_participante }
});
if (!participanteExists) {
throw new HttpException(`Participante con ID ${id_participante} no encontrado`, HttpStatus.NOT_FOUND);
}
// Obtener todos los registros participante-evento para este participante
const eventosParticipante = await this.participanteEventoRepository.find({
where: { id_participante },
relations: ['evento']
});
return eventosParticipante;
}
async deleteParticipanteEvento(id_participante: number) {
const result = await this.participanteEventoRepository.delete({ id_participante })
@@ -142,19 +74,4 @@ export class ParticipanteEventoService {
return this.participanteEventoRepository.save(participante_evento)
}
async registrarAsistencia(id_participante: number, id_evento: number) {
const participante_eventoFound = await this.participanteEventoRepository.findOne({
where: {
id_participante,
id_evento
}
});
if (!participante_eventoFound) {
return new HttpException('Registro no encontrado', HttpStatus.NOT_FOUND);
}
participante_eventoFound.fecha_asistencia = new Date();
return this.participanteEventoRepository.save(participante_eventoFound);
}
}
+7 -99
View File
@@ -1,115 +1,23 @@
import { IsNotEmpty, IsString, IsBoolean, IsNumber, IsOptional, IsArray, ValidateNested } from 'class-validator';
import { Type } from 'class-transformer';
import { IsArray, IsBoolean, IsEnum, IsNotEmpty, IsNumber, IsOptional, IsString, ValidateNested } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export enum TipoPreguntaEnum {
CERRADA = 'Cerrada',
ABIERTA = 'Abierta',
MULTIPLE = 'Multiple'
}
export class OpcionDto {
@ApiProperty({
description: 'Valor de la opción',
example: 'Sí'
})
@IsNotEmpty()
@IsString()
valor: string;
}
import { CreateOpcionDto } from '../../opcion/dto/create-opcion.dto';
export class CreatePreguntaDto {
@ApiProperty({
description: 'Título o texto de la pregunta',
example: '¿Eres parte de la comunidad de la FES Acatlán?'
})
@IsNotEmpty()
@IsString()
titulo: string;
pregunta: string;
@ApiProperty({
description: 'Indica si la pregunta es obligatoria',
example: true,
default: false
})
@IsOptional()
@IsBoolean()
obligatoria?: boolean;
@ApiProperty({
description: 'Límite de caracteres para respuestas de texto (solo aplica a preguntas tipo Abierta)',
example: 250,
required: false
})
@IsOptional()
@IsNumber()
limite?: number;
@ApiProperty({
description: 'Tipo de pregunta',
enum: Object.values(TipoPreguntaEnum),
enumName: 'TipoPreguntaEnum',
example: TipoPreguntaEnum.CERRADA,
examples: {
'cerrada': {
summary: 'Pregunta de opción única',
value: TipoPreguntaEnum.CERRADA
},
'abierta': {
summary: 'Pregunta de texto libre',
value: TipoPreguntaEnum.ABIERTA
},
'multiple': {
summary: 'Pregunta de selección múltiple',
value: TipoPreguntaEnum.MULTIPLE
}
}
})
@IsNotEmpty()
@IsEnum(TipoPreguntaEnum, {
message: 'El tipo debe ser uno de los siguientes valores: Cerrada, Abierta, Multiple'
})
tipo: string;
@ApiProperty({
description: 'Contador de opciones (se calcula automáticamente)',
required: false
})
@IsOptional()
@IsNumber()
contador_opcion?: number;
id_tipo_pregunta: number;
@ApiProperty({
description: 'ID de la opción de la que depende esta pregunta (para preguntas condicionadas)',
required: false,
example: null
})
@IsOptional()
@IsNumber()
id_opcion_dependiente?: number;
@ApiProperty({
description: 'Tipo de validación para la respuesta',
required: false,
examples: ['cuenta_alumno', 'correo', 'telefono', 'nombre', 'entero', 'decimal'],
example: 'correo'
})
@IsOptional()
@IsString()
validacion?: string;
@ApiProperty({
description: 'Lista de opciones para preguntas de tipo Cerrada o Multiple',
type: [OpcionDto],
required: false,
examples: [
{ valor: 'Sí' },
{ valor: 'No' }
]
})
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => OpcionDto)
opciones?: OpcionDto[];
}
@Type(() => CreateOpcionDto)
opciones?: CreateOpcionDto[];
}
+14 -3
View File
@@ -1,4 +1,15 @@
import { PartialType } from '@nestjs/mapped-types';
import { CreatePreguntaDto } from './create-pregunta.dto';
import { IsOptional, IsString, IsBoolean, IsNumber } from 'class-validator';
export class UpdatePreguntaDto extends PartialType(CreatePreguntaDto) {}
export class UpdatePreguntaDto {
@IsOptional()
@IsString()
pregunta?: string;
@IsOptional()
@IsBoolean()
obligatoria?: boolean;
@IsOptional()
@IsNumber()
id_tipo_pregunta?: number;
}
+17 -18
View File
@@ -1,39 +1,38 @@
import { PreguntaOpcion } from 'src/pregunta_opcion/entities/pregunta_opcion.entity';
import { RespuestaParticipanteAbierta } from 'src/respuesta_participante_abierta/entities/respuesta_participante_abierta.entity';
import { SeccionPregunta } from 'src/seccion_pregunta/entities/seccion_pregunta.entity';
import { TipoPregunta } from 'src/tipo_pregunta/entities/tipo_pregunta.entity';
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, OneToMany, JoinColumn } from 'typeorm';
@Entity()
@Entity('pregunta')
export class Pregunta {
@PrimaryGeneratedColumn()
@PrimaryGeneratedColumn({ name: 'id_pregunta' })
id_pregunta: number;
@Column()
@Column({ type: 'text' })
pregunta: string;
@Column({ default: 0 })
@Column({ name: 'contador_opcion', default: 0 })
contador_opcion: number;
@Column({ default: false })
obligatoria: boolean;
@ManyToOne(() => TipoPregunta, tipo => tipo.preguntas)
// Relación con TipoPregunta
@ManyToOne(() => TipoPregunta, tipoPregunta => tipoPregunta.preguntas)
@JoinColumn({ name: 'id_tipo_pregunta' })
tipoPregunta: TipoPregunta;
@Column()
id_tipo_pregunta: number;
@Column({ type: 'int', nullable: true })
id_opcion_dependiente?: number;
@Column({ nullable: true })
validacion?: string;
@OneToMany(() => SeccionPregunta, seccionPregunta => seccionPregunta.pregunta)
seccionPreguntas: SeccionPregunta[];
tipo_pregunta: TipoPregunta;
// Relación con PreguntaOpcion (tabla intermedia para opciones)
@OneToMany(() => PreguntaOpcion, preguntaOpcion => preguntaOpcion.pregunta)
opciones: PreguntaOpcion[];
// Relación con SeccionPregunta (tabla intermedia para secciones)
@OneToMany(() => SeccionPregunta, seccionPregunta => seccionPregunta.pregunta)
seccionesPregunta: SeccionPregunta[];
// Relación con RespuestasAbiertas
@OneToMany(() => RespuestaParticipanteAbierta, respuestaAbierta => respuestaAbierta.pregunta)
respuestasAbiertas: RespuestaParticipanteAbierta[];
}
+89 -20
View File
@@ -1,41 +1,110 @@
import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common';
import {
Controller,
Post,
Body,
Get,
Param,
Put,
Delete,
HttpStatus,
HttpException,
Query
} from '@nestjs/common';
import { PreguntaService } from './pregunta.service';
import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiQuery } from '@nestjs/swagger';
import { CreatePreguntaDto } from './dto/create-pregunta.dto';
import { UpdatePreguntaDto } from './dto/update-pregunta.dto';
import { PreguntaApiDocumentation } from './pregunta.documentation';
@Controller('pregunta')
@PreguntaApiDocumentation.ApiController
@ApiTags('Preguntas')
@Controller('preguntas')
export class PreguntaController {
constructor(private readonly preguntaService: PreguntaService) {}
@Post()
@PreguntaApiDocumentation.ApiCreate
create(@Body() createPreguntaDto: CreatePreguntaDto) {
return this.preguntaService.create(createPreguntaDto);
@ApiOperation({ summary: 'Crear una nueva pregunta' })
@ApiResponse({ status: 201, description: 'Pregunta creada exitosamente' })
@ApiResponse({ status: 400, description: 'Datos de entrada inválidos' })
@ApiResponse({ status: 404, description: 'Tipo de pregunta no encontrado' })
async create(@Body() createPreguntaDto: CreatePreguntaDto) {
try {
return await this.preguntaService.create(createPreguntaDto);
} catch (error) {
throw new HttpException({
status: HttpStatus.BAD_REQUEST,
error: error.message,
}, HttpStatus.BAD_REQUEST);
}
}
@Get()
@PreguntaApiDocumentation.ApiGetAll
findAll() {
@ApiOperation({ summary: 'Obtener todas las preguntas' })
@ApiQuery({ name: 'tipo', required: false, description: 'Filtrar por tipo de pregunta' })
@ApiResponse({ status: 200, description: 'Lista de preguntas' })
async findAll(@Query('tipo') tipoPreguntaId?: number) {
if (tipoPreguntaId) {
return this.preguntaService.findAllByTipo(tipoPreguntaId);
}
return this.preguntaService.findAll();
}
@Get(':id')
@PreguntaApiDocumentation.ApiGetOne
findOne(@Param('id') id: string) {
return this.preguntaService.findOne(+id);
@ApiOperation({ summary: 'Obtener una pregunta específica' })
@ApiParam({ name: 'id', description: 'ID de la pregunta' })
@ApiResponse({ status: 200, description: 'Pregunta encontrada' })
@ApiResponse({ status: 404, description: 'Pregunta no encontrada' })
async findOne(@Param('id') id: string) {
const pregunta = await this.preguntaService.findOneWithRelations(+id);
if (!pregunta) {
throw new HttpException('Pregunta no encontrada', HttpStatus.NOT_FOUND);
}
return pregunta;
}
@Patch(':id')
@PreguntaApiDocumentation.ApiUpdate
update(@Param('id') id: string, @Body() updatePreguntaDto: UpdatePreguntaDto) {
return this.preguntaService.update(+id, updatePreguntaDto);
@Put(':id')
@ApiOperation({ summary: 'Actualizar una pregunta' })
@ApiParam({ name: 'id', description: 'ID de la pregunta a actualizar' })
@ApiResponse({ status: 200, description: 'Pregunta actualizada' })
@ApiResponse({ status: 404, description: 'Pregunta no encontrada' })
async update(
@Param('id') id: string,
@Body() updatePreguntaDto: UpdatePreguntaDto,
) {
try {
return await this.preguntaService.update(+id, updatePreguntaDto);
} catch (error) {
throw new HttpException({
status: HttpStatus.BAD_REQUEST,
error: error.message,
}, HttpStatus.BAD_REQUEST);
}
}
@Delete(':id')
@PreguntaApiDocumentation.ApiRemove
remove(@Param('id') id: string) {
return this.preguntaService.remove(+id);
@ApiOperation({ summary: 'Eliminar una pregunta' })
@ApiParam({ name: 'id', description: 'ID de la pregunta a eliminar' })
@ApiResponse({ status: 200, description: 'Pregunta eliminada' })
@ApiResponse({ status: 404, description: 'Pregunta no encontrada' })
async remove(@Param('id') id: string) {
try {
return await this.preguntaService.remove(+id);
} catch (error) {
throw new HttpException({
status: HttpStatus.BAD_REQUEST,
error: error.message,
}, HttpStatus.BAD_REQUEST);
}
}
}
@Get(':id/opciones')
@ApiOperation({ summary: 'Obtener opciones de una pregunta' })
@ApiParam({ name: 'id', description: 'ID de la pregunta' })
@ApiResponse({ status: 200, description: 'Opciones de la pregunta' })
@ApiResponse({ status: 404, description: 'Pregunta no encontrada' })
async getOpciones(@Param('id') id: string) {
const pregunta = await this.preguntaService.findOneWithRelations(+id);
if (!pregunta) {
throw new HttpException('Pregunta no encontrada', HttpStatus.NOT_FOUND);
}
return pregunta.opciones;
}
}
-241
View File
@@ -1,241 +0,0 @@
import { ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
import { applyDecorators } from '@nestjs/common';
import { TipoPreguntaEnum } from './dto/create-pregunta.dto';
export class PreguntaApiDocumentation {
// Decoradores para toda la clase del controlador
static ApiController = ApiTags('Preguntas');
// Documentación para crear una pregunta
static ApiCreate = applyDecorators(
ApiOperation({
summary: 'Crear una nueva pregunta',
description: 'Crea una nueva pregunta para una sección'
}),
ApiBody({
description: 'Datos de la pregunta a crear',
schema: {
type: 'object',
required: ['titulo', 'tipo'],
properties: {
titulo: { type: 'string', example: '¿Eres parte de la comunidad?' },
obligatoria: { type: 'boolean', example: true },
tipo: {
type: 'string',
enum: Object.values(TipoPreguntaEnum),
description: 'Tipo de pregunta: Cerrada (opción única), Abierta (texto libre), Multiple (varias opciones)',
example: TipoPreguntaEnum.CERRADA
},
contador_opcion: { type: 'number', example: 0 },
id_opcion_dependiente: { type: 'number', example: null },
validacion: {
type: 'string',
description: 'Tipo de validación para la respuesta (cuenta_alumno, correo, telefono, nombre, entero, decimal, etc)',
example: 'correo'
},
opciones: {
type: 'array',
items: {
type: 'object',
properties: {
valor: { type: 'string', example: 'Si' }
}
}
}
}
},
examples: {
cerrada: {
summary: 'Pregunta de opción única',
value: {
titulo: '¿Eres parte de la comunidad de la FES Acatlán?',
obligatoria: true,
tipo: TipoPreguntaEnum.CERRADA,
validacion: null,
opciones: [
{ valor: 'Sí' },
{ valor: 'No' }
]
}
},
abierta: {
summary: 'Pregunta de texto libre',
value: {
titulo: '¿Qué mejorarías en nuestro servicio?',
obligatoria: false,
tipo: TipoPreguntaEnum.ABIERTA,
validacion: null
}
},
multiple: {
summary: 'Pregunta de selección múltiple',
value: {
titulo: '¿Qué aspectos del servicio fueron de tu agrado?',
obligatoria: false,
tipo: TipoPreguntaEnum.MULTIPLE,
validacion: null,
opciones: [
{ valor: 'Rapidez' },
{ valor: 'Atención al cliente' },
{ valor: 'Facilidad de uso' }
]
}
}
}
}),
ApiResponse({
status: 201,
description: 'Pregunta creada correctamente',
schema: {
type: 'object',
properties: {
id_pregunta: { type: 'number', example: 1 },
pregunta: { type: 'string', example: '¿Eres parte de la comunidad?' },
obligatoria: { type: 'boolean', example: true },
contador_opcion: { type: 'number', example: 0 },
id_tipo_pregunta: { type: 'number', example: 1 },
id_opcion_dependiente: { type: 'number', example: null },
validacion: { type: 'string', example: 'correo' }
}
}
}),
ApiResponse({ status: 400, description: 'Datos de la pregunta inválidos' }),
ApiResponse({ status: 500, description: 'Error interno del servidor' })
);
// Documentación para obtener todas las preguntas
static ApiGetAll = applyDecorators(
ApiOperation({
summary: 'Obtener todas las preguntas',
description: 'Retorna una lista de todas las preguntas registradas'
}),
ApiResponse({
status: 200,
description: 'Lista de preguntas obtenida correctamente',
schema: {
type: 'array',
items: {
type: 'object',
properties: {
id_pregunta: { type: 'number', example: 1 },
pregunta: { type: 'string', example: '¿Eres parte de la comunidad?' },
obligatoria: { type: 'boolean', example: true },
contador_opcion: { type: 'number', example: 0 },
id_tipo_pregunta: { type: 'number', example: 1 },
id_opcion_dependiente: { type: 'number', example: null },
validacion: { type: 'string', example: 'correo' }
}
}
}
}),
ApiResponse({ status: 500, description: 'Error interno del servidor' })
);
// Documentación para obtener una pregunta por ID
static ApiGetOne = applyDecorators(
ApiOperation({
summary: 'Obtener una pregunta por ID',
description: 'Retorna una pregunta específica por su ID'
}),
ApiParam({
name: 'id',
description: 'ID de la pregunta',
required: true,
type: 'number',
example: 1
}),
ApiResponse({
status: 200,
description: 'Pregunta obtenida correctamente',
schema: {
type: 'object',
properties: {
id_pregunta: { type: 'number', example: 1 },
pregunta: { type: 'string', example: '¿Eres parte de la comunidad?' },
obligatoria: { type: 'boolean', example: true },
contador_opcion: { type: 'number', example: 0 },
id_tipo_pregunta: { type: 'number', example: 1 },
id_opcion_dependiente: { type: 'number', example: null },
validacion: { type: 'string', example: 'correo' },
opciones: {
type: 'array',
items: {
type: 'object',
properties: {
id_opcion: { type: 'number', example: 1 },
opcion: { type: 'string', example: 'Si' }
}
}
}
}
}
}),
ApiResponse({ status: 404, description: 'Pregunta no encontrada' }),
ApiResponse({ status: 500, description: 'Error interno del servidor' })
);
// Documentación para actualizar una pregunta
static ApiUpdate = applyDecorators(
ApiOperation({
summary: 'Actualizar una pregunta',
description: 'Actualiza los datos de una pregunta existente'
}),
ApiParam({
name: 'id',
description: 'ID de la pregunta a actualizar',
required: true,
type: 'number',
example: 1
}),
ApiBody({
description: 'Datos a actualizar de la pregunta',
schema: {
type: 'object',
properties: {
pregunta: { type: 'string', example: 'Pregunta actualizada' },
obligatoria: { type: 'boolean', example: false }
}
}
}),
ApiResponse({
status: 200,
description: 'Pregunta actualizada correctamente',
schema: {
type: 'object',
properties: {
affected: { type: 'number', example: 1 }
}
}
}),
ApiResponse({ status: 400, description: 'Datos de actualización inválidos' }),
ApiResponse({ status: 404, description: 'Pregunta no encontrada' }),
ApiResponse({ status: 500, description: 'Error interno del servidor' })
);
// Documentación para eliminar una pregunta
static ApiRemove = applyDecorators(
ApiOperation({
summary: 'Eliminar una pregunta',
description: 'Elimina permanentemente una pregunta por su ID'
}),
ApiParam({
name: 'id',
description: 'ID de la pregunta a eliminar',
required: true,
type: 'number',
example: 1
}),
ApiResponse({
status: 200,
description: 'Pregunta eliminada correctamente',
schema: {
type: 'object',
properties: {
affected: { type: 'number', example: 1 }
}
}
}),
ApiResponse({ status: 404, description: 'Pregunta no encontrada' }),
ApiResponse({ status: 500, description: 'Error interno del servidor' })
);
}
+10 -15
View File
@@ -1,25 +1,20 @@
import { Module } from '@nestjs/common';
import { PreguntaService } from './pregunta.service';
import { PreguntaController } from './pregunta.controller';
import { Module, forwardRef } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Pregunta } from './entities/pregunta.entity';
import { TipoPreguntaModule } from '../tipo_pregunta/tipo_pregunta.module';
import { TipoPregunta } from '../tipo_pregunta/entities/tipo_pregunta.entity';
import { Opcion } from '../opcion/entities/opcion.entity';
import { PreguntaService } from './pregunta.service';
import { PreguntaController } from './pregunta.controller';
import { OpcionModule } from '../opcion/opcion.module';
import { PreguntaOpcion } from '../pregunta_opcion/entities/pregunta_opcion.entity';
import { TipoPreguntaModule } from '../tipo_pregunta/tipo_pregunta.module';
@Module({
imports: [
TypeOrmModule.forFeature([
Pregunta,
TipoPregunta,
Opcion,
PreguntaOpcion
]),
TipoPreguntaModule
TypeOrmModule.forFeature([Pregunta, PreguntaOpcion]),
forwardRef(() => OpcionModule),
TipoPreguntaModule,
],
controllers: [PreguntaController],
providers: [PreguntaService],
exports: [TypeOrmModule, PreguntaService]
exports: [PreguntaService],
})
export class PreguntaModule {}
export class PreguntaModule {}
+126 -225
View File
@@ -1,256 +1,157 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, DataSource } from 'typeorm';
import { CreatePreguntaDto, OpcionDto } from './dto/create-pregunta.dto';
import { UpdatePreguntaDto } from './dto/update-pregunta.dto';
import { Repository } from 'typeorm';
import { Pregunta } from './entities/pregunta.entity';
import { TipoPregunta } from '../tipo_pregunta/entities/tipo_pregunta.entity';
import { Opcion } from '../opcion/entities/opcion.entity';
import { PreguntaOpcion } from '../pregunta_opcion/entities/pregunta_opcion.entity';
import { OpcionService } from '../opcion/opcion.service';
import { CreatePreguntaDto } from './dto/create-pregunta.dto';
import { CreateOpcionDto } from '../opcion/dto/create-opcion.dto';
import { TipoPregunta } from 'src/tipo_pregunta/entities/tipo_pregunta.entity';
@Injectable()
export class PreguntaService {
constructor(
@InjectRepository(Pregunta)
private preguntaRepository: Repository<Pregunta>,
private readonly preguntaRepository: Repository<Pregunta>,
@InjectRepository(TipoPregunta)
private tipoPreguntaRepository: Repository<TipoPregunta>,
@InjectRepository(Opcion)
private opcionRepository: Repository<Opcion>,
@InjectRepository(PreguntaOpcion)
private preguntaOpcionRepository: Repository<PreguntaOpcion>,
private dataSource: DataSource
private readonly tipoPreguntaRepository: Repository<TipoPregunta>,
private readonly opcionService: OpcionService,
) {}
async create(createPreguntaDto: CreatePreguntaDto) {
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
// Verificar que el tipo de pregunta exista
try {
// Obtener el tipo de pregunta por su nombre
const tipoPregunta = await this.tipoPreguntaRepository.findOne({
where: { tipo_pregunta: createPreguntaDto.tipo }
});
if (!tipoPregunta) {
throw new Error(`Tipo de pregunta '${createPreguntaDto.tipo}' no encontrado`);
}
// Crear pregunta
const pregunta = new Pregunta();
pregunta.pregunta = createPreguntaDto.titulo;
pregunta.obligatoria = createPreguntaDto.obligatoria || false;
pregunta.id_tipo_pregunta = tipoPregunta.id_tipo;
pregunta.id_opcion_dependiente = createPreguntaDto.id_opcion_dependiente || undefined;
pregunta.contador_opcion = createPreguntaDto.opciones?.length || 0;
pregunta.validacion = createPreguntaDto.validacion || undefined;
const savedPregunta = await queryRunner.manager.save(pregunta);
// Crear opciones para preguntas de tipo multiple/radio
if (createPreguntaDto.opciones && createPreguntaDto.opciones.length > 0) {
await this.createOpciones(
queryRunner,
savedPregunta.id_pregunta,
createPreguntaDto.opciones
);
}
//Se espera que solo exista el ripo de pregunta abierta y cerrada
const tipoPregunta = await this.tipoPreguntaRepository.findOne({
where: { id_tipo: createPreguntaDto.id_tipo_pregunta },
});
await queryRunner.commitTransaction();
return {
success: true,
pregunta: savedPregunta
};
} catch (error) {
await queryRunner.rollbackTransaction();
throw error;
} finally {
await queryRunner.release();
if (!tipoPregunta) {
throw new NotFoundException(
`Tipo de pregunta con ID ${createPreguntaDto.id_tipo_pregunta} no encontrado`,
);
}
// Crear la pregunta base
const pregunta = this.preguntaRepository.create({
pregunta: createPreguntaDto.pregunta,
obligatoria: createPreguntaDto.obligatoria || false,
tipo_pregunta: tipoPregunta,
});
const preguntaGuardada = await this.preguntaRepository.save(pregunta);
// Si es pregunta cerrada y tiene opciones, crearlas
if (
tipoPregunta.tipo_pregunta === 'Cerrada' &&
createPreguntaDto.opciones &&
createPreguntaDto.opciones.length > 0
) {
await this.agregarOpcionesAPregunta(
preguntaGuardada.id_pregunta,
createPreguntaDto.opciones,
);
// Actualizar el contador de opciones
await this.preguntaRepository.update(
{ id_pregunta: preguntaGuardada.id_pregunta },
{ contador_opcion: createPreguntaDto.opciones.length },
);
}
return this.findOneWithRelations(preguntaGuardada.id_pregunta);
}
private async createOpciones(
queryRunner: any,
preguntaId: number,
opciones: OpcionDto[]
private async agregarOpcionesAPregunta(
preguntaId: number,
opciones: CreateOpcionDto[],
) {
for (let i = 0; i < opciones.length; i++) {
const opcionDto = opciones[i];
// Crear opción
const opcion = new Opcion();
opcion.opcion = opcionDto.valor;
const savedOpcion = await queryRunner.manager.save(opcion);
// Vincular opción con pregunta
const preguntaOpcion = new PreguntaOpcion();
preguntaOpcion.id_pregunta = preguntaId;
preguntaOpcion.opcion = savedOpcion;
preguntaOpcion.posicion = i + 1;
await queryRunner.manager.save(preguntaOpcion);
for (const opcionDto of opciones) {
await this.opcionService.create(preguntaId, opcionDto);
}
}
async findAll() {
return this.preguntaRepository.find();
}
async findOne(id: number) {
const pregunta = await this.preguntaRepository.findOne({
where: { id_pregunta: id }
});
if (!pregunta) {
throw new NotFoundException(`Pregunta con ID ${id} no encontrada`);
}
return pregunta;
}
async findWithOptions(id: number) {
const pregunta = await this.preguntaRepository.findOne({
async findOneWithRelations(id: number) {
return this.preguntaRepository.findOne({
where: { id_pregunta: id },
relations: ['tipoPregunta']
relations: [
'tipo_pregunta',
'opciones',
'opciones.opcion',
'seccionesPregunta',
'seccionesPregunta.seccion',
],
});
if (!pregunta) {
throw new NotFoundException(`Pregunta con ID ${id} no encontrada`);
}
// Obtener las opciones para esta pregunta
const preguntaOpciones = await this.preguntaOpcionRepository.find({
where: { id_pregunta: id },
relations: ['opcion'],
order: { posicion: 'ASC' }
});
const opciones = preguntaOpciones.map(po => ({
id: po.opcion.id_opcion,
valor: po.opcion.opcion,
posicion: po.posicion
}));
return {
...pregunta,
tipo: pregunta.tipoPregunta?.tipo_pregunta,
opciones
};
}
async update(id: number, updatePreguntaDto: UpdatePreguntaDto) {
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
const pregunta = await this.preguntaRepository.findOne({
where: { id_pregunta: id }
});
if (!pregunta) {
throw new NotFoundException(`Pregunta con ID ${id} no encontrada`);
}
// Actualizar los campos básicos de la pregunta
if (updatePreguntaDto.titulo) pregunta.pregunta = updatePreguntaDto.titulo;
if (updatePreguntaDto.obligatoria !== undefined) pregunta.obligatoria = updatePreguntaDto.obligatoria;
if (updatePreguntaDto.id_opcion_dependiente !== undefined) pregunta.id_opcion_dependiente = updatePreguntaDto.id_opcion_dependiente;
if (updatePreguntaDto.validacion !== undefined) pregunta.validacion = updatePreguntaDto.validacion;
// Si se proporciona un nuevo tipo de pregunta, actualizarlo
if (updatePreguntaDto.tipo) {
const tipoPregunta = await this.tipoPreguntaRepository.findOne({
where: { tipo_pregunta: updatePreguntaDto.tipo }
});
if (!tipoPregunta) {
throw new Error(`Tipo de pregunta '${updatePreguntaDto.tipo}' no encontrado`);
}
pregunta.id_tipo_pregunta = tipoPregunta.id_tipo;
}
// Actualizar la pregunta
const updatedPregunta = await queryRunner.manager.save(pregunta);
// Si se proporcionan nuevas opciones, actualizarlas
if (updatePreguntaDto.opciones && updatePreguntaDto.opciones.length > 0) {
// Eliminar las opciones existentes
const existingOptions = await this.preguntaOpcionRepository.find({
where: { id_pregunta: id }
});
for (const option of existingOptions) {
await queryRunner.manager.remove(option);
}
// Crear las nuevas opciones
await this.createOpciones(
queryRunner,
id,
updatePreguntaDto.opciones
);
// Actualizar el contador de opciones
updatedPregunta.contador_opcion = updatePreguntaDto.opciones.length;
await queryRunner.manager.save(updatedPregunta);
}
await queryRunner.commitTransaction();
return {
success: true,
pregunta: updatedPregunta
};
} catch (error) {
await queryRunner.rollbackTransaction();
throw error;
} finally {
await queryRunner.release();
}
async findAllByTipo(tipoPreguntaId: number) {
return this.preguntaRepository.find({
where: { tipo_pregunta: { id_tipo: tipoPreguntaId } },
relations: ['opciones', 'opciones.opcion'],
});
}
async remove(id: number) {
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
const pregunta = await this.preguntaRepository.findOne({
where: { id_pregunta: id },
relations: ['opciones'],
});
try {
const pregunta = await this.preguntaRepository.findOne({
where: { id_pregunta: id }
});
if (!pregunta) {
throw new NotFoundException(`Pregunta con ID ${id} no encontrada`);
}
// Eliminar las relaciones con opciones
const preguntaOpciones = await this.preguntaOpcionRepository.find({
where: { id_pregunta: id }
});
for (const po of preguntaOpciones) {
await queryRunner.manager.remove(po);
}
// Eliminar la pregunta
await queryRunner.manager.remove(pregunta);
await queryRunner.commitTransaction();
return {
success: true,
message: `Pregunta con ID ${id} eliminada correctamente`
};
} catch (error) {
await queryRunner.rollbackTransaction();
throw error;
} finally {
await queryRunner.release();
if (!pregunta) {
throw new NotFoundException(`Pregunta con ID ${id} no encontrada`);
}
// Eliminar opciones primero si existen
if (pregunta.opciones && pregunta.opciones.length > 0) {
for (const opcion of pregunta.opciones) {
await this.opcionService.remove(id, opcion.id_pregunta_opcion);
}
}
// Eliminar la pregunta
await this.preguntaRepository.delete(id);
return { message: 'Pregunta eliminada correctamente' };
}
}
async update(id: number, updatePreguntaDto: any) {
const pregunta = await this.preguntaRepository.findOne({
where: { id_pregunta: id },
});
if (!pregunta) {
throw new NotFoundException(`Pregunta con ID ${id} no encontrada`);
}
// Actualizar campos básicos
if (updatePreguntaDto.pregunta !== undefined) {
pregunta.pregunta = updatePreguntaDto.pregunta;
}
if (updatePreguntaDto.obligatoria !== undefined) {
pregunta.obligatoria = updatePreguntaDto.obligatoria;
}
// Si se cambia el tipo de pregunta
if (updatePreguntaDto.id_tipo_pregunta !== undefined) {
const tipoPregunta = await this.tipoPreguntaRepository.findOne({
where: { id_tipo: updatePreguntaDto.id_tipo_pregunta },
});
if (!tipoPregunta) {
throw new NotFoundException(
`Tipo de pregunta con ID ${updatePreguntaDto.id_tipo_pregunta} no encontrado`,
);
}
pregunta.tipo_pregunta = tipoPregunta;
}
return this.preguntaRepository.save(pregunta);
}
findAll() {
return this.preguntaRepository.find();
}
}
@@ -4,28 +4,28 @@ import { RespuestaParticipanteCerrada } from 'src/respuesta_participante_cerrada
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, OneToMany, JoinColumn } from 'typeorm';
@Entity()
@Entity('pregunta_opcion')
export class PreguntaOpcion {
@PrimaryGeneratedColumn()
@PrimaryGeneratedColumn({ name: 'id_pregunta_opcion' })
id_pregunta_opcion: number;
@Column()
posicion: number;
@ManyToOne(() => Pregunta)
// Relación con Pregunta
@ManyToOne(() => Pregunta, pregunta => pregunta.opciones)
@JoinColumn({ name: 'id_pregunta' })
pregunta: Pregunta;
@Column()
id_pregunta: number;
@ManyToOne(() => Opcion, opcion => opcion.preguntasOpciones)
// Relación con Opcion
@ManyToOne(() => Opcion, opcion => opcion.Preguntas)
@JoinColumn({ name: 'id_opcion' })
opcion: Opcion;
@Column()
id_opcion: number;
@OneToMany(()=> RespuestaParticipanteCerrada, respuestaParticipanteCerrada=>respuestaParticipanteCerrada.preguntaOpcion)
respuestaParticipanteCerrada:RespuestaParticipanteCerrada[];
// Nueva relación con RespuestaParticipanteCerrada
@OneToMany(
() => RespuestaParticipanteCerrada,
respuestaCerrada => respuestaCerrada.preguntaOpcion
)
respuestasCerradas: RespuestaParticipanteCerrada[];
}
+5 -11
View File
@@ -1,19 +1,13 @@
import { Module } from '@nestjs/common';
import { PreguntaOpcionService } from './pregunta_opcion.service';
import { PreguntaOpcionController } from './pregunta_opcion.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { PreguntaOpcion } from './entities/pregunta_opcion.entity';
import { OpcionModule } from '../opcion/opcion.module';
import { PreguntaModule } from '../pregunta/pregunta.module';
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [
TypeOrmModule.forFeature([PreguntaOpcion]),
OpcionModule,
PreguntaModule
],
controllers: [PreguntaOpcionController],
providers: [PreguntaOpcionService],
exports: [TypeOrmModule]
imports: [TypeOrmModule.forFeature([PreguntaOpcion])],
controllers: [PreguntaOpcionController],
providers: [PreguntaOpcionService],
exports:[PreguntaOpcionService, TypeOrmModule]
})
export class PreguntaOpcionModule {}
+22 -54
View File
@@ -1,68 +1,36 @@
import {
Body,
Controller,
Delete,
Get,
Param,
ParseIntPipe,
Patch,
Post,
Query,
} from '@nestjs/common';
import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post } from '@nestjs/common';
import { QrService } from './qr.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';
@Controller('qr')
export class QrController {
constructor(private qrService: QrService) {}
@Get('generate')
@ApiOperation({ summary: 'Genera un código QR a partir de un texto' })
@ApiQuery({
name: 'text',
required: true,
description: 'Texto a codificar en el QR',
})
async generateQRCode(@Query('text') text: string): Promise<string> {
return this.qrService.generateQRCode(text);
}
constructor(private qrService: QrService) {}
@Get('asistencia/:idParticipante/:idEvento')
@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
): Promise<string> {
return this.qrService.generateAsistenciaQR(idParticipante, idEvento);
}
@Get()
getQrs(): Promise<Qr[]> {
return this.qrService.getQrs();
}
@Get()
getQrs(): Promise<Qr[]> {
return this.qrService.getQrs();
}
@Get(':id')
getQr(@Param('id', ParseIntPipe) id: number) {
return this.qrService.getQr(id);
}
@Get(':id')
getQr(@Param('id', ParseIntPipe) id: number) {
return this.qrService.getQr(id);
}
@Post() //en el body ValidationPipe
createQr(@Body() newQr: CreateQrDto) {
return this.qrService.createQr(newQr)
}
@Post() //en el body ValidationPipe
createQr(@Body() newQr: CreateQrDto) {
return this.qrService.createQr(newQr);
}
@Delete(':id')
deleteQr(@Param('id', ParseIntPipe) id:number) {
return this.qrService.deleteQr(id);
}
@Delete(':id')
deleteQr(@Param('id', ParseIntPipe) id: number) {
return this.qrService.deleteQr(id);
}
@Patch(':id')
updateQr(@Param('id', ParseIntPipe) id: number, @Body() qr: UpdateQrDto) {
return this.qrService.updateQr(id, qr);
}
@Patch(':id')
updateQr(@Param('id', ParseIntPipe) id: number, @Body() qr: UpdateQrDto) {
return this.qrService.updateQr(id, qr)
}
}
+44 -76
View File
@@ -4,102 +4,70 @@ import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CreateQrDto } from './dto/create-qr.dto';
import { UpdateQrDto } from './dto/update.qr.dto';
// @ts-ignore
import * as QRCode from 'qrcode';
@Injectable()
export class QrService {
constructor(@InjectRepository(Qr) private qrRepository: Repository<Qr>) {}
async generateQRCode(text: string): Promise<string> {
return await QRCode.toDataURL(text);
}
constructor(
@InjectRepository(Qr) private qrRepository: Repository<Qr>
) {}
async generateAsistenciaQR(id_participante: number, id_evento: number): Promise<string> {
// Crear un objeto JSON que contenga los IDs
const qrData = {
id_participante,
id_evento
};
// Convertir el objeto a una cadena JSON
const jsonStr = JSON.stringify(qrData);
// Generar el código QR con la cadena JSON
return await QRCode.toDataURL(jsonStr);
}
async createQr(qr: CreateQrDto) {
const qrFound = await this.qrRepository.findOne({
where: {
id_qr: qr.id_participante_evento
}
})
async generateBuffer(text: string): Promise<Buffer> {
return await QRCode.toBuffer(text); // Devuelve un buffer para adjuntar en email o stream
}
if (qrFound) {
return new HttpException('Qr already exists', HttpStatus.CONFLICT)
}
// Para enviar el QR por email
/* const qrBuffer = await this.qrService.generateBuffer('Texto a codificar');
await transporter.sendMail({
to: 'destinatario@example.com',
subject: 'Tu código QR',
html: '<p>Escanea el siguiente código:</p><img src="cid:qrcode"/>',
attachments: [{
filename: 'qrcode.png',
content: qrBuffer,
cid: 'qrcode',
}],
}); */
async createQr(qr: CreateQrDto) {
const qrFound = await this.qrRepository.findOne({
where: {
id_qr: qr.id_participante_evento,
},
});
if (qrFound) {
return new HttpException('Qr already exists', HttpStatus.CONFLICT);
return this.qrRepository.save(qr)
}
return this.qrRepository.save(qr);
}
getQrs() {
return this.qrRepository.find({})
}
getQrs() {
return this.qrRepository.find({});
}
async getQr(id_qr) {
const qrFound = await this.qrRepository.findOne({
where: {
id_qr
}
})
async getQr(id_qr) {
const qrFound = await this.qrRepository.findOne({
where: {
id_qr,
},
});
if (!qrFound) {
return new HttpException('User not found', HttpStatus.NOT_FOUND);
}
if (!qrFound) {
return new HttpException('User not found', HttpStatus.NOT_FOUND);
return qrFound
}
return qrFound;
}
async deleteQr(id_qr: number) {
const result = await this.qrRepository.delete({ id_qr })
async deleteQr(id_qr: number) {
const result = await this.qrRepository.delete({ id_qr });
if (result.affected === 0) {
return new HttpException('User not found', HttpStatus.NOT_FOUND);
}
if (result.affected === 0) {
return new HttpException('User not found', HttpStatus.NOT_FOUND);
return result
}
return result;
}
async updateQr(id_qr: number, qr: UpdateQrDto) {
const qrFound = await this.qrRepository.findOne({
where: {
id_qr
}
})
async updateQr(id_qr: number, qr: UpdateQrDto) {
const qrFound = await this.qrRepository.findOne({
where: {
id_qr,
},
});
if (!qrFound) {
return new HttpException('User not found', HttpStatus.NOT_FOUND)
}
if (!qrFound) {
return new HttpException('User not found', HttpStatus.NOT_FOUND);
const updateQr = Object.assign(qrFound, qr)
return this.qrRepository.save(updateQr)
}
const updateQr = Object.assign(qrFound, qr);
return this.qrRepository.save(updateQr);
}
}
@@ -2,26 +2,45 @@ import { CuestionarioRespondido } from 'src/cuestionario_respondido/entities/cue
import { Pregunta } from 'src/pregunta/entities/pregunta.entity';
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
@Entity('respuesta_participante_abierta')
export class RespuestaParticipanteAbierta {
@PrimaryGeneratedColumn()
idRespuestaParticipanteAbierta: number;
@PrimaryGeneratedColumn({
name: 'id_respuesta_participante_abierta',
type: 'integer'
})
id: number;
@Column({ type: 'text' })
@Column({
type: 'text',
nullable: false
})
respuesta: string;
// Relación con CuestionarioRespondido
@ManyToOne(
() => CuestionarioRespondido,
cuestionarioRespondido => cuestionarioRespondido.respuestasAbiertas
(cuestionarioRespondido) => cuestionarioRespondido.respuestasAbiertas
)
@JoinColumn({ name: 'id_cuestionario_respondido' })
@JoinColumn({
name: 'id_cuestionario_respondido'
})
cuestionarioRespondido: CuestionarioRespondido;
@ManyToOne(() => Pregunta)
@JoinColumn({ name: 'id_pregunta' })
// Relación con Pregunta
@ManyToOne(
() => Pregunta,
(pregunta) => pregunta.respuestasAbiertas
)
@JoinColumn({
name: 'id_pregunta'
})
pregunta: Pregunta;
/* Campo adicional para tracking
@Column({
name: 'fecha_respuesta',
type: 'timestamp',
default: () => 'CURRENT_TIMESTAMP'
})
fechaRespuesta: Date;*/
}
@@ -1,19 +1,15 @@
import { Module } from '@nestjs/common';
import { RespuestaParticipanteAbiertaService } from './respuesta_participante_abierta.service';
import { RespuestaParticipanteAbiertaController } from './respuesta_participante_abierta.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { RespuestaParticipanteAbierta } from './entities/respuesta_participante_abierta.entity';
import { CuestionarioRespondidoModule } from '../cuestionario_respondido/cuestionario_respondido.module';
import { PreguntaModule } from '../pregunta/pregunta.module';
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [
TypeOrmModule.forFeature([RespuestaParticipanteAbierta]),
CuestionarioRespondidoModule,
PreguntaModule
],
controllers: [RespuestaParticipanteAbiertaController],
providers: [RespuestaParticipanteAbiertaService],
exports: [TypeOrmModule]
TypeOrmModule.forFeature([RespuestaParticipanteAbierta]),
],
controllers: [RespuestaParticipanteAbiertaController],
providers: [RespuestaParticipanteAbiertaService],
exports: [RespuestaParticipanteAbiertaService, TypeOrmModule],
})
export class RespuestaParticipanteAbiertaModule {}
@@ -12,7 +12,7 @@ export class RespuestaParticipanteCerrada {
@JoinColumn({ name: 'id_cuestionario_respondido' })
cuestionarioRespondido: CuestionarioRespondido;
@ManyToOne(() => PreguntaOpcion, preguntaOpcion => preguntaOpcion.respuestaParticipanteCerrada)
@ManyToOne(() => PreguntaOpcion, preguntaOpcion => preguntaOpcion.respuestasCerradas)
@JoinColumn({ name: 'id_pregunta_opcion' })
preguntaOpcion: PreguntaOpcion;
@@ -1,19 +1,13 @@
import { Module } from '@nestjs/common';
import { RespuestaParticipanteCerradaService } from './respuesta_participante_cerrada.service';
import { RespuestaParticipanteCerradaController } from './respuesta_participante_cerrada.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { RespuestaParticipanteCerrada } from './entities/respuesta_participante_cerrada.entity';
import { PreguntaOpcionModule } from '../pregunta_opcion/pregunta_opcion.module';
import { CuestionarioRespondidoModule } from '../cuestionario_respondido/cuestionario_respondido.module';
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [
TypeOrmModule.forFeature([RespuestaParticipanteCerrada]),
PreguntaOpcionModule,
CuestionarioRespondidoModule
],
controllers: [RespuestaParticipanteCerradaController],
providers: [RespuestaParticipanteCerradaService],
exports: [TypeOrmModule]
imports: [TypeOrmModule.forFeature([RespuestaParticipanteCerrada])],
controllers: [RespuestaParticipanteCerradaController],
providers: [RespuestaParticipanteCerradaService],
exports:[RespuestaParticipanteCerradaService, TypeOrmModule]
})
export class RespuestaParticipanteCerradaModule {}
+15 -8
View File
@@ -1,6 +1,6 @@
import { CreatePreguntaDto } from '../../pregunta/dto/create-pregunta.dto';
import { IsArray, IsNotEmpty, IsNumber, IsOptional, IsString, ValidateNested } from 'class-validator';
import { IsNotEmpty, IsString, IsOptional, IsArray, ValidateNested, IsInt } from 'class-validator';
import { Type } from 'class-transformer';
import { CreatePreguntaDto } from '../../pregunta/dto/create-pregunta.dto';
export class CreateSeccionDto {
@IsNotEmpty()
@@ -11,13 +11,20 @@ export class CreateSeccionDto {
@IsString()
descripcion?: string;
@IsOptional()
@IsNumber()
contador_pregunta?: number;
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => CreatePreguntaDto)
preguntas?: CreatePreguntaDto[];
@Type(() => PreguntaEnSeccionDto)
preguntas?: PreguntaEnSeccionDto[];
}
export class PreguntaEnSeccionDto {
@IsNotEmpty()
@IsInt()
posicion: number;
@IsNotEmpty()
@ValidateNested()
@Type(() => CreatePreguntaDto)
pregunta: CreatePreguntaDto;
}
+4 -4
View File
@@ -11,14 +11,14 @@ export class Seccion {
contador_pregunta: number;
@Column({ type: 'text', nullable: true })
descripcion?: string;
descripcion: string;
@Column({ type: 'text', nullable: true })
titulo: string;
@OneToMany(() => CuestionarioSeccion,(cuestionario_seccion) => cuestionario_seccion.id_seccion )
seccion:CuestionarioSeccion[];
@OneToMany(() => CuestionarioSeccion,(cuestionario_seccion) => cuestionario_seccion.seccion )
cuestionarioSeccion:CuestionarioSeccion[];
@OneToMany(()=> SeccionPregunta, (seccion_pregunta)=> seccion_pregunta.seccion)
seccionPreguntas:SeccionPregunta[];
seccion_pregunta:SeccionPregunta[];
}
+146 -21
View File
@@ -1,41 +1,166 @@
import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common';
import {
Controller,
Post,
Body,
Get,
Param,
Put,
Delete,
HttpStatus,
HttpException,
Query,
} from '@nestjs/common';
import { SeccionService } from './seccion.service';
import { CreateSeccionDto } from './dto/create-seccion.dto';
import { CreateSeccionDto, PreguntaEnSeccionDto } from './dto/create-seccion.dto';
import { UpdateSeccionDto } from './dto/update-seccion.dto';
import { SeccionApiDocumentation } from './seccion.documentation';
import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiBody } from '@nestjs/swagger';
@Controller('seccion')
@SeccionApiDocumentation.ApiController
@ApiTags('Secciones')
@Controller('secciones')
export class SeccionController {
constructor(private readonly seccionService: SeccionService) {}
@Post()
@SeccionApiDocumentation.ApiCreate
create(@Body() createSeccionDto: CreateSeccionDto) {
return this.seccionService.create(createSeccionDto);
@ApiOperation({ summary: 'Crear una nueva sección con preguntas' })
@ApiBody({ type: CreateSeccionDto })
@ApiResponse({
status: HttpStatus.CREATED,
description: 'Sección creada exitosamente con sus preguntas'
})
@ApiResponse({
status: HttpStatus.BAD_REQUEST,
description: 'Datos de entrada inválidos'
})
async create(@Body() createSeccionDto: CreateSeccionDto) {
try {
return await this.seccionService.create(createSeccionDto);
} catch (error) {
throw new HttpException({
status: HttpStatus.BAD_REQUEST,
error: error.message,
}, HttpStatus.BAD_REQUEST);
}
}
@Get()
@SeccionApiDocumentation.ApiGetAll
findAll() {
@ApiOperation({ summary: 'Obtener todas las secciones' })
@ApiResponse({
status: HttpStatus.OK,
description: 'Lista de secciones obtenida exitosamente'
})
async findAll() {
return this.seccionService.findAll();
}
@Get(':id')
@SeccionApiDocumentation.ApiGetOne
findOne(@Param('id') id: string) {
return this.seccionService.findOne(+id);
@ApiOperation({ summary: 'Obtener una sección específica con sus preguntas' })
@ApiParam({ name: 'id', description: 'ID de la sección' })
@ApiResponse({
status: HttpStatus.OK,
description: 'Sección encontrada con todas sus relaciones'
})
@ApiResponse({
status: HttpStatus.NOT_FOUND,
description: 'Sección no encontrada'
})
async findOne(@Param('id') id: string) {
const seccion = await this.seccionService.findOneWithRelations(+id);
if (!seccion) {
throw new HttpException('Sección no encontrada', HttpStatus.NOT_FOUND);
}
return seccion;
}
@Patch(':id')
@SeccionApiDocumentation.ApiUpdate
update(@Param('id') id: string, @Body() updateSeccionDto: UpdateSeccionDto) {
return this.seccionService.update(+id, updateSeccionDto);
@Put(':id')
@ApiOperation({ summary: 'Actualizar los metadatos de una sección' })
@ApiParam({ name: 'id', description: 'ID de la sección a actualizar' })
@ApiBody({ type: UpdateSeccionDto })
@ApiResponse({
status: HttpStatus.OK,
description: 'Sección actualizada exitosamente'
})
async update(
@Param('id') id: string,
@Body() updateSeccionDto: UpdateSeccionDto,
) {
try {
return await this.seccionService.update(+id, updateSeccionDto);
} catch (error) {
throw new HttpException({
status: HttpStatus.BAD_REQUEST,
error: error.message,
}, HttpStatus.BAD_REQUEST);
}
}
@Delete(':id')
@SeccionApiDocumentation.ApiRemove
remove(@Param('id') id: string) {
return this.seccionService.remove(+id);
@ApiOperation({ summary: 'Eliminar una sección y sus relaciones' })
@ApiParam({ name: 'id', description: 'ID de la sección a eliminar' })
@ApiResponse({
status: HttpStatus.OK,
description: 'Sección eliminada exitosamente'
})
async remove(@Param('id') id: string) {
try {
return await this.seccionService.remove(+id);
} catch (error) {
throw new HttpException({
status: HttpStatus.BAD_REQUEST,
error: error.message,
}, HttpStatus.BAD_REQUEST);
}
}
}
@Post(':id/preguntas')
@ApiOperation({ summary: 'Agregar una nueva pregunta a la sección' })
@ApiParam({ name: 'id', description: 'ID de la sección' })
@ApiBody({ type: PreguntaEnSeccionDto })
@ApiResponse({
status: HttpStatus.CREATED,
description: 'Pregunta agregada a la sección exitosamente'
})
async agregarPregunta(
@Param('id') seccionId: string,
@Body() preguntaDto: PreguntaEnSeccionDto,
) {
try {
return await this.seccionService.agregarPreguntaExistente(
+seccionId,
preguntaDto.pregunta.id_tipo_pregunta,
preguntaDto.posicion,
);
} catch (error) {
throw new HttpException({
status: error.status || HttpStatus.BAD_REQUEST,
error: error.message,
}, error.status || HttpStatus.BAD_REQUEST);
}
}
@Delete(':seccionId/preguntas/:preguntaId')
@ApiOperation({ summary: 'Remover una pregunta de la sección' })
@ApiParam({ name: 'seccionId', description: 'ID de la sección' })
@ApiParam({ name: 'preguntaId', description: 'ID de la pregunta a remover' })
@ApiResponse({
status: HttpStatus.OK,
description: 'Pregunta removida de la sección exitosamente'
})
async removerPregunta(
@Param('seccionId') seccionId: string,
@Param('preguntaId') preguntaId: string,
) {
try {
return await this.seccionService.eliminarPreguntaDeSeccion(
+seccionId,
+preguntaId,
);
} catch (error) {
throw new HttpException({
status: error.status || HttpStatus.BAD_REQUEST,
error: error.message,
}, error.status || HttpStatus.BAD_REQUEST);
}
}
}
-185
View File
@@ -1,185 +0,0 @@
import { ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
import { applyDecorators } from '@nestjs/common';
export class SeccionApiDocumentation {
// Decoradores para toda la clase del controlador
static ApiController = ApiTags('Secciones');
// Documentación para crear una sección
static ApiCreate = applyDecorators(
ApiOperation({
summary: 'Crear una nueva sección',
description: 'Crea una nueva sección para un cuestionario'
}),
ApiBody({
description: 'Datos de la sección a crear',
schema: {
type: 'object',
required: ['titulo'],
properties: {
titulo: { type: 'string', example: 'Información Personal' },
descripcion: {
type: 'string',
example: 'Proporciona tus datos personales para poder contactarte'
},
contador_pregunta: { type: 'number', example: 0 },
preguntas: {
type: 'array',
items: {
type: 'object',
properties: {
titulo: { type: 'string', example: '¿Eres parte de la comunidad?' },
obligatoria: { type: 'boolean', example: true },
tipo: { type: 'string', example: 'Multiple' },
opciones: {
type: 'array',
items: {
type: 'object',
properties: {
valor: { type: 'string', example: 'Si' }
}
}
}
}
}
}
}
}
}),
ApiResponse({
status: 201,
description: 'Sección creada correctamente',
schema: {
type: 'object',
properties: {
id_seccion: { type: 'number', example: 1 },
titulo: { type: 'string', example: 'Información Personal' },
descripcion: { type: 'string' },
contador_pregunta: { type: 'number', example: 0 }
}
}
}),
ApiResponse({ status: 400, description: 'Datos de la sección inválidos' }),
ApiResponse({ status: 500, description: 'Error interno del servidor' })
);
// Documentación para obtener todas las secciones
static ApiGetAll = applyDecorators(
ApiOperation({
summary: 'Obtener todas las secciones',
description: 'Retorna una lista de todas las secciones registradas'
}),
ApiResponse({
status: 200,
description: 'Lista de secciones obtenida correctamente',
schema: {
type: 'array',
items: {
type: 'object',
properties: {
id_seccion: { type: 'number', example: 1 },
titulo: { type: 'string', example: 'Información Personal' },
descripcion: { type: 'string' },
contador_pregunta: { type: 'number', example: 0 }
}
}
}
}),
ApiResponse({ status: 500, description: 'Error interno del servidor' })
);
// Documentación para obtener una sección por ID
static ApiGetOne = applyDecorators(
ApiOperation({
summary: 'Obtener una sección por ID',
description: 'Retorna una sección específica por su ID'
}),
ApiParam({
name: 'id',
description: 'ID de la sección',
required: true,
type: 'number',
example: 1
}),
ApiResponse({
status: 200,
description: 'Sección obtenida correctamente',
schema: {
type: 'object',
properties: {
id_seccion: { type: 'number', example: 1 },
titulo: { type: 'string', example: 'Información Personal' },
descripcion: { type: 'string' },
contador_pregunta: { type: 'number', example: 0 }
}
}
}),
ApiResponse({ status: 404, description: 'Sección no encontrada' }),
ApiResponse({ status: 500, description: 'Error interno del servidor' })
);
// Documentación para actualizar una sección
static ApiUpdate = applyDecorators(
ApiOperation({
summary: 'Actualizar una sección',
description: 'Actualiza los datos de una sección existente'
}),
ApiParam({
name: 'id',
description: 'ID de la sección a actualizar',
required: true,
type: 'number',
example: 1
}),
ApiBody({
description: 'Datos a actualizar de la sección',
schema: {
type: 'object',
properties: {
titulo: { type: 'string', example: 'Título actualizado' },
descripcion: { type: 'string', example: 'Descripción actualizada' }
}
}
}),
ApiResponse({
status: 200,
description: 'Sección actualizada correctamente',
schema: {
type: 'object',
properties: {
affected: { type: 'number', example: 1 }
}
}
}),
ApiResponse({ status: 400, description: 'Datos de actualización inválidos' }),
ApiResponse({ status: 404, description: 'Sección no encontrada' }),
ApiResponse({ status: 500, description: 'Error interno del servidor' })
);
// Documentación para eliminar una sección
static ApiRemove = applyDecorators(
ApiOperation({
summary: 'Eliminar una sección',
description: 'Elimina permanentemente una sección por su ID'
}),
ApiParam({
name: 'id',
description: 'ID de la sección a eliminar',
required: true,
type: 'number',
example: 1
}),
ApiResponse({
status: 200,
description: 'Sección eliminada correctamente',
schema: {
type: 'object',
properties: {
affected: { type: 'number', example: 1 }
}
}
}),
ApiResponse({ status: 404, description: 'Sección no encontrada' }),
ApiResponse({ status: 500, description: 'Error interno del servidor' })
);
}
+7 -16
View File
@@ -1,27 +1,18 @@
import { Module } from '@nestjs/common';
import { Module, forwardRef } from '@nestjs/common';
import { SeccionService } from './seccion.service';
import { SeccionController } from './seccion.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Seccion } from './entities/seccion.entity';
import { Pregunta } from '../pregunta/entities/pregunta.entity';
import { TypeOrmModule } from '@nestjs/typeorm';
import { SeccionPregunta } from '../seccion_pregunta/entities/seccion_pregunta.entity';
import { TipoPregunta } from '../tipo_pregunta/entities/tipo_pregunta.entity';
import { Opcion } from '../opcion/entities/opcion.entity';
import { PreguntaOpcion } from '../pregunta_opcion/entities/pregunta_opcion.entity';
import { PreguntaModule } from '../pregunta/pregunta.module';
@Module({
imports: [
TypeOrmModule.forFeature([
Seccion,
Pregunta,
SeccionPregunta,
TipoPregunta,
Opcion,
PreguntaOpcion
])
TypeOrmModule.forFeature([Seccion, SeccionPregunta]),
forwardRef(() => PreguntaModule)
],
controllers: [SeccionController],
providers: [SeccionService],
exports: [TypeOrmModule, SeccionService]
exports: [SeccionService],
})
export class SeccionModule {}
export class SeccionModule {}
+144 -172
View File
@@ -1,205 +1,177 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, DataSource } from 'typeorm';
import { CreateSeccionDto } from './dto/create-seccion.dto';
import { UpdateSeccionDto } from './dto/update-seccion.dto';
import { DeleteResult, Repository } from 'typeorm';
import { Seccion } from './entities/seccion.entity';
import { PreguntaService } from '../pregunta/pregunta.service';
import { CreateSeccionDto, PreguntaEnSeccionDto } from './dto/create-seccion.dto';
import { Pregunta } from '../pregunta/entities/pregunta.entity';
import { SeccionPregunta } from '../seccion_pregunta/entities/seccion_pregunta.entity';
import { TipoPregunta } from '../tipo_pregunta/entities/tipo_pregunta.entity';
import { Opcion } from '../opcion/entities/opcion.entity';
import { PreguntaOpcion } from '../pregunta_opcion/entities/pregunta_opcion.entity';
import { SeccionPregunta } from 'src/seccion_pregunta/entities/seccion_pregunta.entity';
import { UpdateSeccionDto } from './dto/update-seccion.dto';
@Injectable()
export class SeccionService {
constructor(
@InjectRepository(Seccion)
private seccionRepository: Repository<Seccion>,
@InjectRepository(Pregunta)
private preguntaRepository: Repository<Pregunta>,
private readonly seccionRepository: Repository<Seccion>,
@InjectRepository(SeccionPregunta)
private seccionPreguntaRepository: Repository<SeccionPregunta>,
@InjectRepository(TipoPregunta)
private tipoPreguntaRepository: Repository<TipoPregunta>,
@InjectRepository(Opcion)
private opcionRepository: Repository<Opcion>,
@InjectRepository(PreguntaOpcion)
private preguntaOpcionRepository: Repository<PreguntaOpcion>,
private dataSource: DataSource
private readonly seccionPreguntaRepository: Repository<SeccionPregunta>,
private readonly preguntaService: PreguntaService,
) {}
async create(createSeccionDto: CreateSeccionDto) {
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
// 1. Crear sección
const seccion = new Seccion();
seccion.titulo = createSeccionDto.titulo;
seccion.descripcion = createSeccionDto.descripcion;
seccion.contador_pregunta = createSeccionDto.preguntas?.length || 0;
const savedSeccion = await queryRunner.manager.save(seccion);
// 2. Crear preguntas y vincularlas a la sección
if (createSeccionDto.preguntas && createSeccionDto.preguntas.length > 0) {
for (let i = 0; i < createSeccionDto.preguntas.length; i++) {
const preguntaDto = createSeccionDto.preguntas[i];
// Obtener el tipo de pregunta por su nombre
const tipoPregunta = await queryRunner.manager.findOne(TipoPregunta, {
where: { tipo_pregunta: preguntaDto.tipo },
});
if (!tipoPregunta) {
throw new Error(`Tipo de pregunta '${preguntaDto.tipo}' no encontrado`);
}
// Crear pregunta
const pregunta = new Pregunta();
pregunta.pregunta = preguntaDto.titulo;
pregunta.obligatoria = preguntaDto.obligatoria || false;
pregunta.id_tipo_pregunta = tipoPregunta.id_tipo;
pregunta.id_opcion_dependiente = preguntaDto.id_opcion_dependiente || undefined;
pregunta.contador_opcion = preguntaDto.opciones?.length || 0;
const savedPregunta = await queryRunner.manager.save(pregunta);
// Vincular pregunta con sección
const seccionPregunta = new SeccionPregunta();
seccionPregunta.id_seccion = savedSeccion.id_seccion;
seccionPregunta.id_pregunta = savedPregunta.id_pregunta;
seccionPregunta.posicion = i + 1;
await queryRunner.manager.save(seccionPregunta);
// Crear opciones para preguntas de tipo multiple/radio
if (preguntaDto.opciones && preguntaDto.opciones.length > 0) {
for (let j = 0; j < preguntaDto.opciones.length; j++) {
const opcionDto = preguntaDto.opciones[j];
// Crear opción
const opcion = new Opcion();
opcion.opcion = opcionDto.valor;
const savedOpcion = await queryRunner.manager.save(opcion);
// Vincular opción con pregunta
const preguntaOpcion = new PreguntaOpcion();
preguntaOpcion.id_pregunta = savedPregunta.id_pregunta;
preguntaOpcion.opcion = savedOpcion;
preguntaOpcion.posicion = j + 1;
await queryRunner.manager.save(preguntaOpcion);
}
}
}
}
await queryRunner.commitTransaction();
return {
success: true,
seccion: savedSeccion,
};
} catch (error) {
await queryRunner.rollbackTransaction();
throw error;
} finally {
await queryRunner.release();
}
}
async findAll() {
return this.seccionRepository.find();
}
async findOne(id: number) {
const seccion = await this.seccionRepository.findOne({
where: { id_seccion: id }
// Crear la sección base
const seccion = this.seccionRepository.create({
titulo: createSeccionDto.titulo,
descripcion: createSeccionDto.descripcion,
contador_pregunta: createSeccionDto.preguntas?.length || 0,
});
if (!seccion) {
throw new NotFoundException(`Sección con ID ${id} no encontrada`);
const seccionGuardada = await this.seccionRepository.save(seccion);
// Si hay preguntas, crearlas y asociarlas
if (createSeccionDto.preguntas && createSeccionDto.preguntas.length > 0) {
await this.agregarPreguntasASeccion(
seccionGuardada.id_seccion,
createSeccionDto.preguntas,
);
}
return seccion;
return this.findOneWithRelations(seccionGuardada.id_seccion);
}
async findWithPreguntas(id: number) {
const seccion = await this.seccionRepository.findOne({
where: { id_seccion: id }
});
if (!seccion) {
throw new NotFoundException(`Sección con ID ${id} no encontrada`);
private async agregarPreguntasASeccion(
seccionId: number,
preguntas: PreguntaEnSeccionDto[], // Tipo actualizado
) {
for (const { posicion, pregunta } of preguntas) {
const preguntaCreada = await this.preguntaService.create(pregunta);
const seccionPregunta = this.seccionPreguntaRepository.create({
posicion,
seccion: { id_seccion: seccionId },
pregunta: { id_pregunta: preguntaCreada?.id_pregunta },
});
await this.seccionPreguntaRepository.save(seccionPregunta);
}
// Obtener las relaciones seccion-pregunta para esta sección
const seccionPreguntas = await this.seccionPreguntaRepository.find({
}
async findOneWithRelations(id: number) {
return this.seccionRepository.findOne({
where: { id_seccion: id },
relations: ['pregunta']
relations: [
'seccionPreguntas',
'seccionPreguntas.pregunta',
'seccionPreguntas.pregunta.tipoPregunta',
'seccionPreguntas.pregunta.opciones',
'seccionPreguntas.pregunta.opciones.opcion',
],
});
// Extraer y organizar las preguntas
const preguntas = await Promise.all(
seccionPreguntas.map(async (sp) => {
const pregunta = await this.preguntaRepository.findOne({
where: { id_pregunta: sp.id_pregunta }
});
// Obtener las opciones para preguntas de tipo multiple/radio
const preguntaOpciones = await this.preguntaOpcionRepository.find({
where: { id_pregunta: sp.id_pregunta },
relations: ['opcion'],
order: { posicion: 'ASC' }
});
const opciones = preguntaOpciones.map(po => po.opcion);
return {
...pregunta,
posicion: sp.posicion,
opciones
};
})
);
// Ordenar las preguntas por posición
preguntas.sort((a, b) => a.posicion - b.posicion);
return {
...seccion,
preguntas
};
}
async update(id: number, updateSeccionDto: UpdateSeccionDto) {
async agregarPreguntaExistente(
seccionId: number,
preguntaId: number,
posicion: number,
) {
// Verificar que existan ambos elementos
const seccion = await this.seccionRepository.findOne({
where: { id_seccion: id }
where: { id_seccion: seccionId },
});
if (!seccion) {
throw new NotFoundException(`Sección con ID ${seccionId} no encontrada`);
}
const pregunta = await this.preguntaService.findOneWithRelations(preguntaId);
if (!pregunta) {
throw new NotFoundException(`Pregunta con ID ${preguntaId} no encontrada`);
}
// Crear la relación
const seccionPregunta = this.seccionPreguntaRepository.create({
posicion,
seccion: { id_seccion: seccionId },
pregunta: { id_pregunta: preguntaId },
});
await this.seccionPreguntaRepository.save(seccionPregunta);
// Actualizar contador de preguntas
await this.seccionRepository.increment(
{ id_seccion: seccionId },
'contador_pregunta',
1,
);
return this.findOneWithRelations(seccionId);
}
async eliminarPreguntaDeSeccion(seccionId: number, preguntaId: number) {
const relacion = await this.seccionPreguntaRepository.findOne({
where: {
seccion: { id_seccion: seccionId },
pregunta: { id_pregunta: preguntaId },
},
});
if (!relacion) {
throw new NotFoundException(
'La pregunta no está asociada a esta sección',
);
}
await this.seccionPreguntaRepository.remove(relacion);
// Actualizar contador de preguntas
await this.seccionRepository.decrement(
{ id_seccion: seccionId },
'contador_pregunta',
1,
);
return { success: true };
}
async findAll(): Promise<Seccion[]> {
return this.seccionRepository.find({
relations: ['seccionPreguntas', 'seccionPreguntas.pregunta'],
});
}
async update(id: number, updateSeccionDto: UpdateSeccionDto): Promise<Seccion> {
const seccion = await this.seccionRepository.findOne({
where: { id_seccion: id }
});
if (!seccion) {
throw new NotFoundException(`Sección con ID ${id} no encontrada`);
}
// Actualizar solo los campos proporcionados
if (updateSeccionDto.titulo) seccion.titulo = updateSeccionDto.titulo;
if (updateSeccionDto.descripcion !== undefined) seccion.descripcion = updateSeccionDto.descripcion;
if (updateSeccionDto.titulo !== undefined) {
seccion.titulo = updateSeccionDto.titulo;
}
if (updateSeccionDto.descripcion !== undefined) {
seccion.descripcion = updateSeccionDto.descripcion;
}
return this.seccionRepository.save(seccion);
}
async remove(id: number) {
const seccion = await this.seccionRepository.findOne({
where: { id_seccion: id }
});
async remove(id: number): Promise<DeleteResult> {
// Primero eliminamos las relaciones con preguntas (esto sigue siendo asíncrono)
this.seccionPreguntaRepository.delete({ seccion: { id_seccion: id } })
.catch(err => {
throw new Error(`Error al eliminar relaciones: ${err.message}`);
});
// Eliminación sincrónica usando delete (devuelve DeleteResult directamente)
const result = this.seccionRepository.delete(id);
if (!seccion) {
if ((await result).affected === 0) {
throw new NotFoundException(`Sección con ID ${id} no encontrada`);
}
return this.seccionRepository.remove(seccion);
return result;
}
}
}
@@ -1,25 +1,20 @@
import { Pregunta } from 'src/pregunta/entities/pregunta.entity';
import { Seccion } from 'src/seccion/entities/seccion.entity';
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from 'typeorm';
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
@Entity('seccion_pregunta')
@Entity()
export class SeccionPregunta {
@PrimaryGeneratedColumn()
id_seccion_pregunta: number;
@Column({ type: 'tinyint' })
@Column()
posicion: number;
@ManyToOne(() => Pregunta, (pregunta) => pregunta.seccionPreguntas)
pregunta: Pregunta;
@Column({ type: 'int' })
id_pregunta: number;
@ManyToOne(() => Seccion, (seccion) => seccion.seccionPreguntas)
@ManyToOne(() => Seccion, seccion => seccion.seccion_pregunta)
@JoinColumn({name:'id_seccion'})
seccion: Seccion;
@Column({ type: 'int' })
id_seccion: number;
}
@ManyToOne(() => Pregunta, pregunta => pregunta.seccionesPregunta)
@JoinColumn({name:'id_pregunta'})
pregunta: Pregunta;
}
@@ -1,19 +1,13 @@
import { Module } from '@nestjs/common';
import { SeccionPreguntaService } from './seccion_pregunta.service';
import { SeccionPreguntaController } from './seccion_pregunta.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { SeccionPregunta } from './entities/seccion_pregunta.entity';
import { PreguntaModule } from '../pregunta/pregunta.module';
import { SeccionModule } from '../seccion/seccion.module';
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [
TypeOrmModule.forFeature([SeccionPregunta]),
PreguntaModule,
SeccionModule
],
imports: [TypeOrmModule.forFeature([SeccionPregunta])],
controllers: [SeccionPreguntaController],
providers: [SeccionPreguntaService],
exports: [TypeOrmModule]
exports:[SeccionPreguntaService, TypeOrmModule]
})
export class SeccionPreguntaModule {}
@@ -1,5 +1,5 @@
import { Cuestionario } from 'src/cuestionario/entities/cuestionario.entity';
import { Entity, PrimaryGeneratedColumn, Column, ManyToMany, ManyToOne } from 'typeorm';
import { Entity, PrimaryGeneratedColumn, Column, ManyToMany, ManyToOne, OneToMany } from 'typeorm';
@Entity('tipo_cuestionario')
export class TipoCuestionario {
@@ -9,6 +9,6 @@ export class TipoCuestionario {
@Column({ type: 'varchar', length: 30 })
tipo_cuestionario: string;
@ManyToOne(()=> Cuestionario,(cuestionario)=> cuestionario.id_tipo_cuestionario)
cuestionario:Cuestionario[];
@OneToMany(()=> Cuestionario,(cuestionario)=> cuestionario.tipo_cuestionario)
cuestionarios:Cuestionario[];
}
@@ -1,9 +1,13 @@
import { Module } from '@nestjs/common';
import { TipoCuestionarioService } from './tipo_cuestionario.service';
import { TipoCuestionarioController } from './tipo_cuestionario.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TipoCuestionario } from './entities/tipo_cuestionario.entity';
@Module({
imports: [TypeOrmModule.forFeature([TipoCuestionario])],
controllers: [TipoCuestionarioController],
providers: [TipoCuestionarioService],
exports:[TipoCuestionarioService, TypeOrmModule]
})
export class TipoCuestionarioModule {}
@@ -1 +1,9 @@
export class CreateTipoPreguntaDto {}
import { IsNotEmpty, IsString, MaxLength, MinLength } from 'class-validator';
export class CreateTipoPreguntaDto {
@IsNotEmpty()
@IsString()
@MinLength(3)
@MaxLength(50)
tipo_pregunta: string;
}
@@ -9,6 +9,6 @@ export class TipoPregunta {
@Column()
tipo_pregunta: string;
@OneToMany(() => Pregunta, pregunta => pregunta.tipoPregunta)
@OneToMany(() => Pregunta, pregunta => pregunta.tipo_pregunta)
preguntas: Pregunta[];
}

Some files were not shown because too many files have changed in this diff Show More