From d652f9f0129aa06495716b044244cbdd0c7501d0 Mon Sep 17 00:00:00 2001 From: jorgemike Date: Fri, 13 Jun 2025 22:41:20 -0600 Subject: [PATCH] muchos cambios :'v --- env.example.txt => .env.example | 4 - Dockerfile | 2 +- README.md | 131 +--- docker-compose.yml | 22 +- package-lock.json | 27 +- package.json | 3 +- src/administrador/administrador.controller.ts | 123 ++-- .../administrador.documentation.ts | 253 ++++---- src/administrador/administrador.entity.ts | 34 -- src/administrador/administrador.module.ts | 2 +- src/administrador/administrador.service.ts | 309 +++++----- .../entities/administrador.entity.ts | 20 + src/alumnos/alumnos.module.ts | 42 +- src/app.module.ts | 94 ++- src/asistencia/asistencia.controller.ts | 2 +- src/asistencia/asistencia.entity.ts | 41 -- src/asistencia/asistencia.module.ts | 2 +- src/asistencia/asistencia.service.ts | 2 +- src/asistencia/entities/asistencia.entity.ts | 25 + src/auth/auth.module.ts | 18 +- src/auth/jwt.strategy.ts | 2 +- .../cuestionario.controller.spec.ts | 20 - src/cuestionario/cuestionario.controller.ts | 75 ++- src/cuestionario/cuestionario.module.ts | 2 +- src/cuestionario/cuestionario.service.spec.ts | 18 - src/cuestionario/cuestionario.service.ts | 531 ++++++++++++----- .../{ => docs}/cuestionario.documentation.ts | 211 ++++--- .../docs/cuestionario.examples.ts | 139 +++++ .../dto/create-cuestionario.dto.ts | 47 +- src/cuestionario/dto/formulario.schema.ts | 6 - .../entities/cuestionario.entity.ts | 64 +- ...cuestionario_respondido.controller.spec.ts | 20 - .../cuestionario_respondido.controller.ts | 12 +- .../cuestionario_respondido.documentation.ts | 154 +++-- .../cuestionario_respondido.module.ts | 8 +- .../cuestionario_respondido.service.spec.ts | 18 - .../cuestionario_respondido.service.ts | 557 ++++++++++++------ .../dto/submit-respuestas.dto.ts | 75 +-- .../cuestionario_respondido.entity.ts | 2 +- .../entities/cuestionario_seccion.entity.ts | 1 - src/db/typeorm.config.ts | 37 ++ src/docs/swagger.config.ts | 4 +- src/emails/registro-evento.ts | 44 ++ src/evento/docs/evento.documentation.ts | 68 +++ src/evento/docs/evento.examples.ts | 57 ++ src/evento/dto/create-evento.dto.ts | 27 +- src/evento/dto/update.evento.dto.ts | 27 +- src/evento/entities/evento.entity.ts | 37 ++ src/evento/evento.controller.ts | 112 +++- src/evento/evento.entity.ts | 41 -- src/evento/evento.module.ts | 2 +- src/evento/evento.service.ts | 147 +++-- src/main.ts | 59 +- src/opcion/entities/opcion.entity.ts | 8 +- .../dto/create-participante.dto.ts | 9 - .../entities/participante.entity.ts | 31 + src/participante/participante.controller.ts | 2 +- src/participante/participante.entity.ts | 49 -- src/participante/participante.module.ts | 2 +- src/participante/participante.service.ts | 256 ++++---- .../entities/participante_evento.entity.ts | 40 ++ .../participante_evento.controller.ts | 404 +++++++------ .../participante_evento.entity.ts | 41 -- .../participante_evento.module.ts | 14 +- .../participante_evento.service.ts | 267 +++++---- src/pregunta/dto/create-pregunta.dto.ts | 27 +- src/pregunta/entities/pregunta.entity.ts | 44 +- src/pregunta/pregunta.controller.spec.ts | 20 - src/pregunta/pregunta.documentation.ts | 155 ++--- src/pregunta/pregunta.service.spec.ts | 18 - src/pregunta/pregunta.service.ts | 2 - .../respuesta_participante_abierta.entity.ts | 17 +- .../respuesta_participante_cerrada.entity.ts | 23 +- src/seccion/seccion.service.ts | 92 +-- .../entities/tipo_cuestionario.entity.ts | 23 +- .../entities/tipo_pregunta.entity.ts | 15 +- src/tipo_pregunta/tipo_pregunta.service.ts | 17 +- src/tipo_user/entities/tipo_user.entity.ts | 15 + src/tipo_user/tipo_user.controller.ts | 2 +- src/tipo_user/tipo_user.entity.ts | 28 - src/tipo_user/tipo_user.module.ts | 6 +- src/tipo_user/tipo_user.service.ts | 139 ++--- utils/get_formulario_feria.ts | 4 - 83 files changed, 3289 insertions(+), 2261 deletions(-) rename env.example.txt => .env.example (96%) delete mode 100644 src/administrador/administrador.entity.ts create mode 100644 src/administrador/entities/administrador.entity.ts delete mode 100644 src/asistencia/asistencia.entity.ts create mode 100644 src/asistencia/entities/asistencia.entity.ts delete mode 100644 src/cuestionario/cuestionario.controller.spec.ts delete mode 100644 src/cuestionario/cuestionario.service.spec.ts rename src/cuestionario/{ => docs}/cuestionario.documentation.ts (51%) create mode 100644 src/cuestionario/docs/cuestionario.examples.ts delete mode 100644 src/cuestionario_respondido/cuestionario_respondido.controller.spec.ts delete mode 100644 src/cuestionario_respondido/cuestionario_respondido.service.spec.ts create mode 100644 src/db/typeorm.config.ts create mode 100644 src/emails/registro-evento.ts create mode 100644 src/evento/docs/evento.documentation.ts create mode 100644 src/evento/docs/evento.examples.ts create mode 100644 src/evento/entities/evento.entity.ts delete mode 100644 src/evento/evento.entity.ts create mode 100644 src/participante/entities/participante.entity.ts delete mode 100644 src/participante/participante.entity.ts create mode 100644 src/participante_evento/entities/participante_evento.entity.ts delete mode 100644 src/participante_evento/participante_evento.entity.ts delete mode 100644 src/pregunta/pregunta.controller.spec.ts delete mode 100644 src/pregunta/pregunta.service.spec.ts create mode 100644 src/tipo_user/entities/tipo_user.entity.ts delete mode 100644 src/tipo_user/tipo_user.entity.ts diff --git a/env.example.txt b/.env.example similarity index 96% rename from env.example.txt rename to .env.example index 14564ff..35d6bb5 100644 --- a/env.example.txt +++ b/.env.example @@ -1,7 +1,5 @@ PORT=4200 - - db_type=mysql db_host=mariadb # <-- uso del nombre del servicio, NO IP db_port=3306 @@ -9,6 +7,4 @@ 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 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 2ac73f4..c8a0157 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,7 @@ 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 +RUN npm i COPY . . diff --git a/README.md b/README.md index 35f1236..e2d5f64 100644 --- a/README.md +++ b/README.md @@ -1,121 +1,26 @@ -

- Nest Logo -

+El Sistema de Registros es una plataforma diseñada para gestionar eventos que requieren registro y control de asistencia mediante formularios personalizados. -[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 -[circleci-url]: https://circleci.com/gh/nestjs/nest +Cada evento puede estar compuesto por varios subeventos (como conferencias, talleres o sesiones), y los participantes deben registrarse en aquellos que sean de su interés. Posteriormente, el sistema permite llevar un pase de lista para verificar la asistencia. -

A progressive Node.js framework for building efficient and scalable server-side applications.

-

-NPM Version -Package License -NPM Downloads -CircleCI -Coverage -Discord -Backers on Open Collective -Sponsors on Open Collective - Donate us - Support us - Follow us on Twitter -

- +📌 Tablas principales: +Evento: Representa un evento principal que requiere control de asistencia. Ejemplo: Data & AI Forum 2025. -## Description +Cuestionario: Corresponde a un subevento dentro de un evento. Por ejemplo, dentro del evento Data & AI Forum, puede haber varias conferencias y sesiones en paralelo, cada una representada como un Cuestionario. -[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. +Participante: Representa a una persona registrada en uno o varios subeventos (Cuestionario). Su asistencia puede ser verificada posteriormente. -## Project setup +```sql +Table Evento { + id_evento int [pk, increment] + tipo_evento varchar(50) + nombre_evento varchar(200) + descripcion_evento varchar(500) + fecha_inicio datetime [default: `CURRENT_TIMESTAMP`] + fecha_fin datetime [default: `CURRENT_TIMESTAMP`] + asistencias int + banner varchar -```bash -$ npm install + Note: 'Tabla que representa un evento general, como un foro o conferencia.' +} ``` -## Compile and run the project - -```bash -# development -$ npm run start - -# watch mode -$ npm run start:dev - -# production mode -$ npm run start:prod -``` - -## Run tests - -```bash -# unit tests -$ npm run test - -# e2e tests -$ npm run test:e2e - -# test coverage -$ npm run test:cov -``` - -## Deployment - -When you're ready to deploy your NestJS application to production, there are some key steps you can take to ensure it runs as efficiently as possible. Check out the [deployment documentation](https://docs.nestjs.com/deployment) for more information. - -If you are looking for a cloud-based platform to deploy your NestJS application, check out [Mau](https://mau.nestjs.com), our official platform for deploying NestJS applications on AWS. Mau makes deployment straightforward and fast, requiring just a few simple steps: - -```bash -$ npm install -g mau -$ mau deploy -``` - -With Mau, you can deploy your application in just a few clicks, allowing you to focus on building features rather than managing infrastructure. - -## Resources - -Check out a few resources that may come in handy when working with NestJS: - -- Visit the [NestJS Documentation](https://docs.nestjs.com) to learn more about the framework. -- For questions and support, please visit our [Discord channel](https://discord.gg/G7Qnnhy). -- To dive deeper and get more hands-on experience, check out our official video [courses](https://courses.nestjs.com/). -- Deploy your application to AWS with the help of [NestJS Mau](https://mau.nestjs.com) in just a few clicks. -- Visualize your application graph and interact with the NestJS application in real-time using [NestJS Devtools](https://devtools.nestjs.com). -- Need help with your project (part-time to full-time)? Check out our official [enterprise support](https://enterprise.nestjs.com). -- To stay in the loop and get updates, follow us on [X](https://x.com/nestframework) and [LinkedIn](https://linkedin.com/company/nestjs). -- Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.nestjs.com). - -## Support - -Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). - -## Stay in touch - -- Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec) -- Website - [https://nestjs.com](https://nestjs.com/) -- Twitter - [@nestframework](https://twitter.com/nestframework) - -## 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'); \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index d933344..c811aec 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,7 +11,7 @@ services: MARIADB_DATABASE: formularios_test volumes: - mariadb_data:/var/lib/mysql - - ./init-scripts:/docker-entrypoint-initdb.d + - ./init-scripts:/docker-entrypoint-initdb.d # Base de datos simulada para escolares restart: unless-stopped api: @@ -32,16 +32,16 @@ services: - front: - build: ../formularios_front - container_name: formularios_front - restart: always - depends_on: - - api - env_file: - - ../formularios_front/.env - ports: - - "4202:3000" +# front: +# build: ../formularios_front +# container_name: formularios_front +# restart: always +# depends_on: +# - api +# env_file: +# - ../formularios_front/.env +# ports: +# - "4202:3000" volumes: diff --git a/package-lock.json b/package-lock.json index 5e57e9d..4e4a26b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "@nestjs/mapped-types": "*", "@nestjs/passport": "^11.0.5", "@nestjs/platform-express": "^11.0.1", - "@nestjs/swagger": "^11.1.0", + "@nestjs/swagger": "^11.2.0", "@nestjs/typeorm": "^11.0.0", "@types/bcrypt": "^5.0.2", "@types/bcryptjs": "^2.4.6", @@ -44,6 +44,7 @@ "@swc/core": "^1.10.7", "@types/express": "^5.0.0", "@types/jest": "^29.5.14", + "@types/multer": "^1.4.13", "@types/node": "^22.10.7", "@types/qrcode": "^1.5.5", "@types/supertest": "^6.0.2", @@ -2540,9 +2541,9 @@ } }, "node_modules/@nestjs/swagger": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-11.1.0.tgz", - "integrity": "sha512-+GQ+q1ASTBvGi0DYHukWi8NVVVLszedwLLqHdLRnJh8rjokt8YTDb7roImvT/YMmYgPvaWBv/4JYdZH4FueLPQ==", + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-11.2.0.tgz", + "integrity": "sha512-5wolt8GmpNcrQv34tIPUtPoV1EeFbCetm40Ij3+M0FNNnf2RJ3FyWfuQvI8SBlcJyfaounYVTKzKHreFXsUyOg==", "license": "MIT", "dependencies": { "@microsoft/tsdoc": "0.15.1", @@ -2550,7 +2551,7 @@ "js-yaml": "4.1.0", "lodash": "4.17.21", "path-to-regexp": "8.2.0", - "swagger-ui-dist": "5.20.1" + "swagger-ui-dist": "5.21.0" }, "peerDependencies": { "@fastify/static": "^8.0.0", @@ -3323,6 +3324,16 @@ "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", "license": "MIT" }, + "node_modules/@types/multer": { + "version": "1.4.13", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.13.tgz", + "integrity": "sha512-bhhdtPw7JqCiEfC9Jimx5LqX9BDIPJEh2q/fQ4bqbBPtyEZYr3cvF22NwG0DmPZNYA0CAf2CnqDB4KIGGpJcaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/node": { "version": "22.13.10", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", @@ -10835,9 +10846,9 @@ } }, "node_modules/swagger-ui-dist": { - "version": "5.20.1", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.20.1.tgz", - "integrity": "sha512-qBPCis2w8nP4US7SvUxdJD3OwKcqiWeZmjN2VWhq2v+ESZEXOP/7n4DeiOiiZcGYTKMHAHUUrroHaTsjUWTEGw==", + "version": "5.21.0", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.21.0.tgz", + "integrity": "sha512-E0K3AB6HvQd8yQNSMR7eE5bk+323AUxjtCz/4ZNKiahOlPhPJxqn3UPIGs00cyY/dhrTDJ61L7C/a8u6zhGrZg==", "license": "Apache-2.0", "dependencies": { "@scarf/scarf": "=1.4.0" diff --git a/package.json b/package.json index 8c10e20..9d5c151 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "@nestjs/mapped-types": "*", "@nestjs/passport": "^11.0.5", "@nestjs/platform-express": "^11.0.1", - "@nestjs/swagger": "^11.1.0", + "@nestjs/swagger": "^11.2.0", "@nestjs/typeorm": "^11.0.0", "@types/bcrypt": "^5.0.2", "@types/bcryptjs": "^2.4.6", @@ -55,6 +55,7 @@ "@swc/core": "^1.10.7", "@types/express": "^5.0.0", "@types/jest": "^29.5.14", + "@types/multer": "^1.4.13", "@types/node": "^22.10.7", "@types/qrcode": "^1.5.5", "@types/supertest": "^6.0.2", diff --git a/src/administrador/administrador.controller.ts b/src/administrador/administrador.controller.ts index 0f6e321..bffcfe1 100644 --- a/src/administrador/administrador.controller.ts +++ b/src/administrador/administrador.controller.ts @@ -1,72 +1,87 @@ -import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post, UseGuards } from '@nestjs/common'; +import { + Body, + Controller, + Delete, + Get, + Param, + ParseIntPipe, + Patch, + Post, + UseGuards, +} from '@nestjs/common'; import { AdministradorService } from './administrador.service'; -import { Administrador } from './administrador.entity'; +import { Administrador } from './entities/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 { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; import { JwtAuthGuard } from '../auth/jwt-auth.guard'; import { AdministradorApiDocumentation } from './administrador.documentation'; -@Controller('administrador') -@ApiBearerAuth() @AdministradorApiDocumentation.ApiController +@ApiBearerAuth() +@Controller('administrador') export class AdministradorController { + constructor(private administradorService: AdministradorService) {} - constructor(private administradorService: AdministradorService) {} + @Get() + @UseGuards(JwtAuthGuard) + @AdministradorApiDocumentation.ApiGetAll + getAdministradores(): Promise { + return this.administradorService.getAdministradores(); + } - @Get() - @UseGuards(JwtAuthGuard) - @AdministradorApiDocumentation.ApiGetAll - getAdministradores(): Promise { - return this.administradorService.getAdministradores(); - } + @Get(':id') + @UseGuards(JwtAuthGuard) + @AdministradorApiDocumentation.ApiGetOne + getAdministrador(@Param('id', ParseIntPipe) id: number) { + return this.administradorService.getAdministrador(id); + } - @Get(':id') - @UseGuards(JwtAuthGuard) - @AdministradorApiDocumentation.ApiGetOne - getAdministrador(@Param('id', ParseIntPipe) id: number) { - return this.administradorService.getAdministrador(id); - } + @Post() + @AdministradorApiDocumentation.ApiCreate + createAdministrador(@Body() newAdministrador: CreateAdministradorDto) { + return this.administradorService.createAdministrador(newAdministrador); + } - @Post() - @AdministradorApiDocumentation.ApiCreate - 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, + ); + } - @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, + ); + } - @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 - ); - } + @Delete(':id') + @UseGuards(JwtAuthGuard) + @AdministradorApiDocumentation.ApiRemove + deleteAdministrador(@Param('id', ParseIntPipe) id: number) { + return this.administradorService.deleteAdministrador(id); + } - @Delete(':id') - @UseGuards(JwtAuthGuard) - @AdministradorApiDocumentation.ApiRemove - deleteAdministrador(@Param('id', ParseIntPipe) id: number) { - 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); - } + @Patch(':id') + @UseGuards(JwtAuthGuard) + @AdministradorApiDocumentation.ApiUpdate + updateAdministrador( + @Param('id', ParseIntPipe) id: number, + @Body() administrador: UpdateAdministradorDto, + ) { + return this.administradorService.updateAdministrador(id, administrador); + } } diff --git a/src/administrador/administrador.documentation.ts b/src/administrador/administrador.documentation.ts index 063bad1..207ade5 100644 --- a/src/administrador/administrador.documentation.ts +++ b/src/administrador/administrador.documentation.ts @@ -1,15 +1,24 @@ -import { ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { + ApiBearerAuth, + 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'); - + static ApiController = applyDecorators( + ApiTags('Administrador'), + ); // 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' + description: + 'Crea un nuevo administrador con correo, contraseña y tipo de administrador', }), ApiBody({ description: 'Datos del administrador a registrar', @@ -17,47 +26,51 @@ export class AdministradorApiDocumentation { type: 'object', required: ['correo', 'password', 'id_tipo_user'], properties: { - correo: { - type: 'string', + correo: { + type: 'string', format: 'email', example: 'admin@ejemplo.com', - description: 'Correo electrónico del administrador' + description: 'Correo electrónico del administrador', }, - password: { - type: 'string', + password: { + type: 'string', format: 'password', - example: 'Password123', - description: 'Contraseña del administrador (mínimo 6 caracteres)' + example: 'password', + description: 'Contraseña del administrador (mínimo 6 caracteres)', }, - id_tipo_user: { - type: 'integer', + id_tipo_user: { + type: 'integer', example: 1, - description: 'ID del tipo de usuario administrador' - } - } - } + description: 'ID del tipo de usuario administrador', + }, + }, + }, }), - ApiResponse({ - status: 201, + 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 } - } - } + 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' }) + 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' + description: + 'Autentica un administrador con correo y contraseña, y devuelve un token JWT', }), ApiBody({ description: 'Credenciales de inicio de sesión', @@ -65,58 +78,59 @@ export class AdministradorApiDocumentation { type: 'object', required: ['correo', 'password'], properties: { - correo: { - type: 'string', + correo: { + type: 'string', format: 'email', example: 'admin@ejemplo.com', - description: 'Correo electrónico del administrador' + description: 'Correo electrónico del administrador', }, - password: { - type: 'string', + password: { + type: 'string', format: 'password', - example: 'Password123', - description: 'Contraseña del administrador' - } - } - } + example: 'password', + description: 'Contraseña del administrador', + }, + }, + }, }), - ApiResponse({ - status: 200, + ApiResponse({ + status: 200, description: 'Inicio de sesión exitoso', schema: { type: 'object', properties: { - access_token: { - type: 'string', + access_token: { + type: 'string', example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...', - description: 'Token JWT para autenticación' + description: 'Token JWT para autenticación', }, - id_administrador: { - type: 'number', + id_administrador: { + type: 'number', example: 1, - description: 'ID del administrador autenticado' + description: 'ID del administrador autenticado', }, - correo: { - type: 'string', + correo: { + type: 'string', example: 'admin@ejemplo.com', - description: 'Correo del administrador autenticado' + description: 'Correo del administrador autenticado', }, - tipo_user: { - type: 'number', + tipo_user: { + type: 'number', example: 1, - description: 'Tipo de usuario del administrador' - } - } - } + description: 'Tipo de usuario del administrador', + }, + }, + }, }), - ApiResponse({ status: 401, description: 'Credenciales inválidas' }) + 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)' + description: + 'Retorna una lista de todos los administradores registrados (requiere autenticación)', }), ApiResponse({ status: 200, @@ -135,28 +149,32 @@ export class AdministradorApiDocumentation { type: 'object', properties: { id_tipo_user: { type: 'number', example: 1 }, - tipo_user: { type: 'string', example: 'Administrador General' } - } - } - } - } - } - } + tipo_user: { + type: 'string', + example: 'Administrador General', + }, + }, + }, + }, + }, + }, + }, }), - ApiResponse({ status: 401, description: 'No autorizado' }) + 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)' + 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 + example: 1, }), ApiResponse({ status: 200, @@ -173,53 +191,55 @@ export class AdministradorApiDocumentation { type: 'object', properties: { id_tipo_user: { type: 'number', example: 1 }, - tipo_user: { type: 'string', example: 'Administrador General' } - } - } - } - } - } + tipo_user: { type: 'string', example: 'Administrador General' }, + }, + }, + }, + }, + }, }), ApiResponse({ status: 404, description: 'Administrador no encontrado' }), - ApiResponse({ status: 401, description: 'No autorizado' }) + 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)' + 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 + example: 1, }), ApiBody({ description: 'Datos a actualizar del administrador', schema: { type: 'object', properties: { - correo: { - type: 'string', + correo: { + type: 'string', format: 'email', example: 'nuevo_admin@ejemplo.com', - description: 'Nuevo correo electrónico del administrador' + description: 'Nuevo correo electrónico del administrador', }, - password: { - type: 'string', + password: { + type: 'string', format: 'password', example: 'NuevaPassword123', - description: 'Nueva contraseña del administrador (mínimo 6 caracteres)' + description: + 'Nueva contraseña del administrador (mínimo 6 caracteres)', }, - id_tipo_user: { - type: 'integer', + id_tipo_user: { + type: 'integer', example: 2, - description: 'Nuevo tipo de usuario administrador' - } - } - } + description: 'Nuevo tipo de usuario administrador', + }, + }, + }, }), ApiResponse({ status: 200, @@ -229,26 +249,30 @@ export class AdministradorApiDocumentation { properties: { id_admnistrador: { type: 'number', example: 1 }, correo: { type: 'string', example: 'nuevo_admin@ejemplo.com' }, - id_tipo_user: { type: 'number', example: 2 } - } - } + id_tipo_user: { type: 'number', example: 2 }, + }, + }, + }), + ApiResponse({ + status: 400, + description: 'Datos de actualización inválidos', }), - ApiResponse({ status: 400, description: 'Datos de actualización inválidos' }), ApiResponse({ status: 404, description: 'Administrador no encontrado' }), - ApiResponse({ status: 401, description: 'No autorizado' }) + 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)' + 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 + example: 1, }), ApiBody({ description: 'Datos para cambio de contraseña', @@ -256,20 +280,21 @@ export class AdministradorApiDocumentation { type: 'object', required: ['currentPassword', 'newPassword'], properties: { - currentPassword: { - type: 'string', + currentPassword: { + type: 'string', format: 'password', example: 'Password123', - description: 'Contraseña actual del administrador' + description: 'Contraseña actual del administrador', }, - newPassword: { - type: 'string', + newPassword: { + type: 'string', format: 'password', example: 'NuevaPassword123', - description: 'Nueva contraseña del administrador (mínimo 6 caracteres)' - } - } - } + description: + 'Nueva contraseña del administrador (mínimo 6 caracteres)', + }, + }, + }, }), ApiResponse({ status: 200, @@ -277,26 +302,30 @@ export class AdministradorApiDocumentation { schema: { type: 'object', properties: { - message: { type: 'string', example: 'Contraseña actualizada correctamente' } - } - } + 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' }) + 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)' + description: + 'Elimina permanentemente un administrador por su ID (requiere autenticación)', }), ApiParam({ name: 'id', description: 'ID del administrador a eliminar', type: 'number', - example: 1 + example: 1, }), ApiResponse({ status: 200, @@ -304,11 +333,11 @@ export class AdministradorApiDocumentation { schema: { type: 'object', properties: { - affected: { type: 'number', example: 1 } - } - } + affected: { type: 'number', example: 1 }, + }, + }, }), ApiResponse({ status: 404, description: 'Administrador no encontrado' }), - ApiResponse({ status: 401, description: 'No autorizado' }) + ApiResponse({ status: 401, description: 'No autorizado' }), ); -} \ No newline at end of file +} diff --git a/src/administrador/administrador.entity.ts b/src/administrador/administrador.entity.ts deleted file mode 100644 index f414872..0000000 --- a/src/administrador/administrador.entity.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { TipoUser } from "src/tipo_user/tipo_user.entity"; -import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm"; - -@Entity() -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" }) - tipoUser: TipoUser; - */ - //Relacion con tipo_user - @Column() - id_tipo_user: number - - @ManyToOne(() => TipoUser, (tipoUser) => tipoUser.administrador) - tipoUser: TipoUser[] - - /* - @OneToMany(() => Evento, (evento) => evento.administrador) - eventos: Evento[]; - - @OneToMany(() => Asistencia, (asistencia) => asistencia.administrador) - asistencias: Asistencia[]; - */ -} \ No newline at end of file diff --git a/src/administrador/administrador.module.ts b/src/administrador/administrador.module.ts index e2a9b80..7eaa84d 100644 --- a/src/administrador/administrador.module.ts +++ b/src/administrador/administrador.module.ts @@ -2,7 +2,7 @@ import { Module } from '@nestjs/common'; import { AdministradorService } from './administrador.service'; import { AdministradorController } from './administrador.controller'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { Administrador } from './administrador.entity'; +import { Administrador } from './entities/administrador.entity'; import { AuthModule } from '../auth/auth.module'; @Module({ diff --git a/src/administrador/administrador.service.ts b/src/administrador/administrador.service.ts index db067f7..818188e 100644 --- a/src/administrador/administrador.service.ts +++ b/src/administrador/administrador.service.ts @@ -1,6 +1,6 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Administrador } from './administrador.entity'; +import { Administrador } from './entities/administrador.entity'; import { Repository } from 'typeorm'; import { CreateAdministradorDto } from './dto/create-administrador.dto'; import { UpdateAdministradorDto } from './dto/update.administrador.dto'; @@ -9,163 +9,204 @@ import { JwtService } from '@nestjs/jwt'; @Injectable() export class AdministradorService { - - constructor( - @InjectRepository(Administrador) private administradorRepository: Repository, - private jwtService: JwtService - ) {} + constructor( + @InjectRepository(Administrador) + private administradorRepository: Repository, + private jwtService: JwtService, + ) {} - async createAdministrador(administrador: CreateAdministradorDto) { - // Verificar si ya existe un administrador con el mismo correo - const administradorFound = await this.administradorRepository.findOne({ - where: { - correo: administrador.correo - } - }); + async createAdministrador(administrador: CreateAdministradorDto) { + // Verificar si ya existe un administrador con el mismo correo + const administradorFound = await this.administradorRepository.findOne({ + where: { + correo: administrador.correo, + }, + }); - if (administradorFound) { - throw new HttpException('El correo ya está registrado', 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; + if (administradorFound) { + throw new HttpException( + 'El correo ya está registrado', + HttpStatus.CONFLICT, + ); } - async login(correo: string, password: string) { - // Buscar administrador por correo - const administrador = await this.administradorRepository.findOne({ - where: { correo }, - relations: ['tipoUser'] - }); + // Encriptar la contraseña + const hashedPassword = await this.hashPassword(administrador.password); - if (!administrador) { - throw new HttpException('Credenciales inválidas', HttpStatus.UNAUTHORIZED); - } + // Crear y guardar el nuevo administrador + const newAdministrador = this.administradorRepository.create({ + correo: administrador.correo, + password: hashedPassword, + id_tipo_user: administrador.id_tipo_user, + }); - // Verificar contraseña - const isPasswordValid = await bcrypt.compare(password, administrador.password); - if (!isPasswordValid) { - throw new HttpException('Credenciales inválidas', HttpStatus.UNAUTHORIZED); - } + const savedAdministrador = + await this.administradorRepository.save(newAdministrador); - // 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 - }; + // 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, + ); } - 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' }; + // Verificar contraseña + const isPasswordValid = await bcrypt.compare( + password, + administrador.password, + ); + if (!isPasswordValid) { + throw new HttpException( + 'Credenciales inválidas', + HttpStatus.UNAUTHORIZED, + ); } - private async hashPassword(password: string): Promise { - const salt = await bcrypt.genSalt(); - return bcrypt.hash(password, salt); + // Generar token JWT + const payload = { + sub: administrador.id_admnistrador, + correo: administrador.correo, + id_tipo_user: administrador.id_tipo_user, + }; + + return { + token: this.jwtService.sign(payload), + }; + } + + 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, + ); } - getAdministradores() { - return this.administradorRepository.find({ - relations: ['tipoUser'], - select: { - id_admnistrador: true, - correo: true, - id_tipo_user: true - } - }); + // Verificar contraseña actual + const isPasswordValid = await bcrypt.compare( + currentPassword, + administrador.password, + ); + if (!isPasswordValid) { + throw new HttpException( + 'Contraseña actual incorrecta', + HttpStatus.BAD_REQUEST, + ); } - async getAdministrador(id_admnistrador: number) { - const administradorFound = await this.administradorRepository.findOne({ - where: { - id_admnistrador - }, - relations: ['tipoUser'], - select: { - id_admnistrador: true, - correo: true, - id_tipo_user: true - } - }); + // Hashear y actualizar nueva contraseña + administrador.password = await this.hashPassword(newPassword); + await this.administradorRepository.save(administrador); - if (!administradorFound) { - throw new HttpException('Administrador no encontrado', HttpStatus.NOT_FOUND); - } + return { message: 'Contraseña actualizada correctamente' }; + } - return administradorFound; + private async hashPassword(password: string): Promise { + const salt = await bcrypt.genSalt(); + return bcrypt.hash(password, salt); + } + + getAdministradores() { + return this.administradorRepository.find({ + relations: ['tipoUser'], + select: { + id_admnistrador: true, + correo: true, + id_tipo_user: true, + }, + }); + } + + async getAdministrador(id_admnistrador: number) { + const administradorFound = await this.administradorRepository.findOne({ + where: { + id_admnistrador, + }, + relations: ['tipoUser'], + select: { + id_admnistrador: true, + correo: true, + id_tipo_user: true, + }, + }); + + if (!administradorFound) { + throw new HttpException( + 'Administrador no encontrado', + HttpStatus.NOT_FOUND, + ); } - async deleteAdministrador(id_admnistrador: number) { - const result = await this.administradorRepository.delete({ id_admnistrador }); + return administradorFound; + } - if (result.affected === 0) { - throw new HttpException('Administrador no encontrado', HttpStatus.NOT_FOUND); - } + async deleteAdministrador(id_admnistrador: number) { + const result = await this.administradorRepository.delete({ + id_admnistrador, + }); - return result; + if (result.affected === 0) { + throw new HttpException( + 'Administrador no encontrado', + HttpStatus.NOT_FOUND, + ); } - async updateAdministrador(id_admnistrador: number, administrador: UpdateAdministradorDto) { - const administradorFound = await this.administradorRepository.findOne({ - where: { - id_admnistrador - } - }); + return result; + } - if (!administradorFound) { - throw new HttpException('Administrador no encontrado', HttpStatus.NOT_FOUND); - } + async updateAdministrador( + id_admnistrador: number, + administrador: UpdateAdministradorDto, + ) { + const administradorFound = await this.administradorRepository.findOne({ + where: { + id_admnistrador, + }, + }); - // 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; + if (!administradorFound) { + throw new HttpException( + 'Administrador no encontrado', + 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; + } } diff --git a/src/administrador/entities/administrador.entity.ts b/src/administrador/entities/administrador.entity.ts new file mode 100644 index 0000000..eb639a0 --- /dev/null +++ b/src/administrador/entities/administrador.entity.ts @@ -0,0 +1,20 @@ +import { TipoUser } from 'src/tipo_user/entities/tipo_user.entity'; +import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; + +@Entity('administrador') +export class Administrador { + @PrimaryGeneratedColumn() + id_admnistrador: number; + + @Column({ unique: true }) + correo: string; + + @Column() + password: string; + + @Column() + id_tipo_user: number; + + @ManyToOne(() => TipoUser, (tipoUser) => tipoUser.administrador) + tipoUser: TipoUser[]; +} diff --git a/src/alumnos/alumnos.module.ts b/src/alumnos/alumnos.module.ts index 9a4935d..ff25a76 100644 --- a/src/alumnos/alumnos.module.ts +++ b/src/alumnos/alumnos.module.ts @@ -3,49 +3,9 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { AlumnosController } from './alumnos.controller'; import { AlumnosService } from './alumnos.service'; import { RegistroAlumno } from './entities/registro-alumno.entity'; -import { ConfigService } from '@nestjs/config'; @Module({ - imports: [ - TypeOrmModule.forFeature([RegistroAlumno], 'alumnosConnection'), - - - TypeOrmModule.forRootAsync({ - name: 'alumnosConnection', - useFactory: (configService: ConfigService) => ({ - type: 'mysql', - host: configService.get('ALUMNOS_DB_HOST'), - port: Number(configService.get('ALUMNOS_DB_PORT')), - username: configService.get('ALUMNOS_DB_USERNAME'), - password: configService.get('ALUMNOS_DB_PASSWORD'), - database: configService.get('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) │ │ - - }), */ - ], + imports: [TypeOrmModule.forFeature([RegistroAlumno], 'alumnosConnection')], controllers: [AlumnosController], providers: [AlumnosService], exports: [AlumnosService], diff --git a/src/app.module.ts b/src/app.module.ts index 42dc231..7496496 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -2,6 +2,11 @@ import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { createAlumnosDbConfig, createMainDbConfig } from './db/typeorm.config'; +import { AdministradorModule } from './administrador/administrador.module'; +import { AuthModule } from './auth/auth.module'; import { CuestionarioModule } from './cuestionario/cuestionario.module'; import { TipoCuestionarioModule } from './tipo_cuestionario/tipo_cuestionario.module'; import { CuestionarioSeccionModule } from './cuestionario_seccion/cuestionario_seccion.module'; @@ -14,80 +19,53 @@ import { PreguntaOpcionModule } from './pregunta_opcion/pregunta_opcion.module'; import { RespuestaParticipanteCerradaModule } from './respuesta_participante_cerrada/respuesta_participante_cerrada.module'; 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 } from '@nestjs/config'; import { EventoModule } from './evento/evento.module'; import { TipoUserModule } from './tipo_user/tipo_user.module'; 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'; - @Module({ imports: [ - ConfigModule.forRoot({ - isGlobal: true, + ConfigModule.forRoot({ isGlobal: true }), + + TypeOrmModule.forRootAsync({ + imports: [ConfigModule], + useFactory: createMainDbConfig, + inject: [ConfigService], }), - TypeOrmModule.forRoot({ - - type: 'mysql', - host: process.env.db_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: false, - dropSchema: false, // elimina la base de datos - - // logging: true, // Habilita los logs para depuración - 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.forRootAsync({ + imports: [ConfigModule], + name: 'alumnosConnection', + useFactory: createAlumnosDbConfig, + inject: [ConfigService], }), - AuthModule, - CuestionarioModule, - TipoCuestionarioModule, - CuestionarioSeccionModule, - SeccionModule, - SeccionPreguntaModule, - PreguntaModule, - TipoPreguntaModule, - OpcionModule, PreguntaOpcionModule, - RespuestaParticipanteCerradaModule, - CuestionarioRespondidoModule, - RespuestaParticipanteAbiertaModule, - EventoModule, - TipoUserModule, - ParticipanteModule, - QrModule, AdministradorModule, - AsistenciaModule, - ParticipanteEventoModule, - ValidacionesModule, AlumnosModule, + AsistenciaModule, + AuthModule, + CuestionarioModule, + CuestionarioRespondidoModule, + CuestionarioSeccionModule, + EventoModule, + OpcionModule, + ParticipanteEventoModule, + ParticipanteModule, + PreguntaModule, + PreguntaOpcionModule, + RespuestaParticipanteAbiertaModule, + RespuestaParticipanteCerradaModule, + SeccionModule, + SeccionPreguntaModule, + TipoCuestionarioModule, + TipoPreguntaModule, + TipoUserModule, + ValidacionesModule, + QrModule, ], controllers: [AppController], diff --git a/src/asistencia/asistencia.controller.ts b/src/asistencia/asistencia.controller.ts index 1107ab8..94814ff 100644 --- a/src/asistencia/asistencia.controller.ts +++ b/src/asistencia/asistencia.controller.ts @@ -1,6 +1,6 @@ import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post } from '@nestjs/common'; import { AsistenciaService } from './asistencia.service'; -import { Asistencia } from './asistencia.entity'; +import { Asistencia } from './entities/asistencia.entity'; import { CreateAsistenciaDto } from './dto/create-asistencia.dto'; import { UpdateAsistenciaDto } from './dto/update.asistencia.dto'; diff --git a/src/asistencia/asistencia.entity.ts b/src/asistencia/asistencia.entity.ts deleted file mode 100644 index 83c5dc5..0000000 --- a/src/asistencia/asistencia.entity.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Column, Entity, PrimaryGeneratedColumn } from "typeorm"; - -@Entity() -export class Asistencia { - @PrimaryGeneratedColumn() - id_asistecia: number - - //({type: 'datetime', default: () => 'CURRENT_TIMESTAMP' } - @Column({type: 'datetime', default: () => 'CURRENT_TIMESTAMP'}) - fecha_asistencia: Date - - @Column() - metodo: boolean - - @Column() - estado: boolean - - //relaciones con las otras tablas - @Column() - id_participante: number - - @Column() - id_evento: number - - @Column() - id_administrador: number - - /* - @ManyToOne(() => Administrador, (admin) => admin.asistencias) - @JoinColumn({ name: "id_administrador" }) - administrador: Administrador; - - @ManyToOne(() => Evento, (evento) => evento.asistencias) - @JoinColumn({ name: "id_evento" }) - evento: Evento; - - @ManyToOne(() => Participante, (participante) => participante.asistencias) - @JoinColumn({ name: "id_participante" }) - participante: Participante; - */ -} \ No newline at end of file diff --git a/src/asistencia/asistencia.module.ts b/src/asistencia/asistencia.module.ts index f16c339..8adbaea 100644 --- a/src/asistencia/asistencia.module.ts +++ b/src/asistencia/asistencia.module.ts @@ -2,7 +2,7 @@ import { Module } from '@nestjs/common'; import { AsistenciaService } from './asistencia.service'; import { AsistenciaController } from './asistencia.controller'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { Asistencia } from './asistencia.entity'; +import { Asistencia } from './entities/asistencia.entity'; @Module({ imports: [TypeOrmModule.forFeature([Asistencia])], diff --git a/src/asistencia/asistencia.service.ts b/src/asistencia/asistencia.service.ts index 1732a79..277e97d 100644 --- a/src/asistencia/asistencia.service.ts +++ b/src/asistencia/asistencia.service.ts @@ -1,6 +1,6 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Asistencia } from './asistencia.entity'; +import { Asistencia } from './entities/asistencia.entity'; import { Repository } from 'typeorm'; import { CreateAsistenciaDto } from './dto/create-asistencia.dto'; import { UpdateAsistenciaDto } from './dto/update.asistencia.dto'; diff --git a/src/asistencia/entities/asistencia.entity.ts b/src/asistencia/entities/asistencia.entity.ts new file mode 100644 index 0000000..b111314 --- /dev/null +++ b/src/asistencia/entities/asistencia.entity.ts @@ -0,0 +1,25 @@ +import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; + +@Entity('asistencia') +export class Asistencia { + @PrimaryGeneratedColumn() + id_asistecia: number; + + @Column({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP' }) + fecha_asistencia: Date; + + @Column() + metodo: boolean; + + @Column() + estado: boolean; + + @Column() + id_participante: number; + + @Column() + id_evento: number; + + @Column() + id_administrador: number; +} diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts index dc5a2a9..4fdcaab 100644 --- a/src/auth/auth.module.ts +++ b/src/auth/auth.module.ts @@ -2,17 +2,23 @@ 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 { ConfigModule, ConfigService } from '@nestjs/config'; +import { Administrador } from '../administrador/entities/administrador.entity'; import { JwtStrategy } from './jwt.strategy'; @Module({ imports: [ + ConfigModule, PassportModule.register({ defaultStrategy: 'jwt' }), - JwtModule.register({ - secret: process.env.JWT_SECRET || 'tu_clave_secreta', - signOptions: { - expiresIn: '24h', // Tokens expiran en 24 horas - }, + JwtModule.registerAsync({ + imports: [ConfigModule], + inject: [ConfigService], + useFactory: (configService: ConfigService) => ({ + secret: configService.get('JWT_SECRET', 'tu_clave_secreta'), + signOptions: { + expiresIn: '24h', + }, + }), }), TypeOrmModule.forFeature([Administrador]), ], diff --git a/src/auth/jwt.strategy.ts b/src/auth/jwt.strategy.ts index f830986..20fb513 100644 --- a/src/auth/jwt.strategy.ts +++ b/src/auth/jwt.strategy.ts @@ -3,7 +3,7 @@ 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'; +import { Administrador } from '../administrador/entities/administrador.entity'; @Injectable() export class JwtStrategy extends PassportStrategy(Strategy) { diff --git a/src/cuestionario/cuestionario.controller.spec.ts b/src/cuestionario/cuestionario.controller.spec.ts deleted file mode 100644 index cff08e4..0000000 --- a/src/cuestionario/cuestionario.controller.spec.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { CuestionarioController } from './cuestionario.controller'; -import { CuestionarioService } from './cuestionario.service'; - -describe('CuestionarioController', () => { - let controller: CuestionarioController; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [CuestionarioController], - providers: [CuestionarioService], - }).compile(); - - controller = module.get(CuestionarioController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); -}); diff --git a/src/cuestionario/cuestionario.controller.ts b/src/cuestionario/cuestionario.controller.ts index f62f04f..a88662a 100644 --- a/src/cuestionario/cuestionario.controller.ts +++ b/src/cuestionario/cuestionario.controller.ts @@ -1,9 +1,25 @@ -import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common'; +import { + Controller, + Get, + Post, + Body, + Patch, + Param, + Delete, + UseInterceptors, + ParseIntPipe, + UploadedFile, +} from '@nestjs/common'; import { CuestionarioService } from './cuestionario.service'; -import { CreateCuestionarioDto } from './dto/create-cuestionario.dto'; +import { + CreateCuestionarioDto, + CreateCuestionarioEventoDto, +} 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 { FileInterceptor } from '@nestjs/platform-express'; +import { diskStorage } from 'multer'; +import { extname } from 'path'; +import { CuestionarioApiDocumentation } from './docs/cuestionario.documentation'; @Controller('cuestionario') @CuestionarioApiDocumentation.ApiController @@ -16,6 +32,50 @@ export class CuestionarioController { return this.cuestionarioService.create(createCuestionarioDto); } + @Post('evento') + @CuestionarioApiDocumentation.ApiCreateWithEvento + createCuestionarioEvento( + @Body() createCuestionarioDto: CreateCuestionarioEventoDto, + ) { + return this.cuestionarioService.createCuestionarioEvento( + createCuestionarioDto, + ); + } + + @Post(':id/banner') + @UseInterceptors( + FileInterceptor('banner', { + storage: diskStorage({ + destination: './public/banners', // Asegúrate de que esta carpeta exista + filename: (req, file, cb) => { + const uniqueSuffix = + Date.now() + '-' + Math.round(Math.random() * 1e9); + const ext = extname(file.originalname); + cb(null, `banner-${uniqueSuffix}${ext}`); + }, + }), + fileFilter: (req, file, cb) => { + const allowedTypes = /jpeg|jpg|png|webp/; + const ext = extname(file.originalname).toLowerCase(); + if (allowedTypes.test(ext)) { + cb(null, true); + } else { + cb(new Error('Tipo de archivo no permitido'), false); + } + }, + }), + ) + async subirBannerEvento( + @Param('id', ParseIntPipe) id: number, + @UploadedFile() file: Express.Multer.File, + ) { + if (!file) { + throw new Error('No se ha recibido un archivo válido'); + } + + return this.cuestionarioService.asociarBanner(id, file.filename); + } + @Get() @CuestionarioApiDocumentation.ApiGetAll findAll() { @@ -27,7 +87,7 @@ export class CuestionarioController { findOne(@Param('id') id: string) { return this.cuestionarioService.findOne(+id); } - + @Get(':id/formulario') @CuestionarioApiDocumentation.ApiGetFormulario findFormulario(@Param('id') id: string) { @@ -36,7 +96,10 @@ export class CuestionarioController { @Patch(':id') @CuestionarioApiDocumentation.ApiUpdate - update(@Param('id') id: string, @Body() updateCuestionarioDto: UpdateCuestionarioDto) { + update( + @Param('id') id: string, + @Body() updateCuestionarioDto: UpdateCuestionarioDto, + ) { return this.cuestionarioService.update(+id, updateCuestionarioDto); } diff --git a/src/cuestionario/cuestionario.module.ts b/src/cuestionario/cuestionario.module.ts index b759268..019c0ae 100644 --- a/src/cuestionario/cuestionario.module.ts +++ b/src/cuestionario/cuestionario.module.ts @@ -11,7 +11,7 @@ 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 { Evento } from '../evento/entities/evento.entity'; import { EventoService } from '../evento/evento.service'; @Module({ diff --git a/src/cuestionario/cuestionario.service.spec.ts b/src/cuestionario/cuestionario.service.spec.ts deleted file mode 100644 index e88a9db..0000000 --- a/src/cuestionario/cuestionario.service.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { CuestionarioService } from './cuestionario.service'; - -describe('CuestionarioService', () => { - let service: CuestionarioService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [CuestionarioService], - }).compile(); - - service = module.get(CuestionarioService); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); -}); diff --git a/src/cuestionario/cuestionario.service.ts b/src/cuestionario/cuestionario.service.ts index f3e05ad..01aad4f 100644 --- a/src/cuestionario/cuestionario.service.ts +++ b/src/cuestionario/cuestionario.service.ts @@ -1,7 +1,10 @@ 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 { + CreateCuestionarioDto, + CreateCuestionarioEventoDto, +} from './dto/create-cuestionario.dto'; import { UpdateCuestionarioDto } from './dto/update-cuestionario.dto'; import { Cuestionario } from './entities/cuestionario.entity'; import { Seccion } from '../seccion/entities/seccion.entity'; @@ -10,9 +13,11 @@ 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 { + TipoPregunta, + TipoPreguntaEnum, +} from '../tipo_pregunta/entities/tipo_pregunta.entity'; +import { Evento } from '../evento/entities/evento.entity'; import { EventoService } from '../evento/evento.service'; @Injectable() @@ -37,138 +42,115 @@ export class CuestionarioService { @InjectRepository(Evento) private eventoRepository: Repository, private dataSource: DataSource, - private eventoService: EventoService + private eventoService: EventoService, ) {} async create(createCuestionarioDto: CreateCuestionarioDto) { + // Verificar que el evento exista + await this.eventoService.getEventoOrFail(createCuestionarioDto.id_evento); + 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.fecha_inicio = new Date(createCuestionarioDto.fecha_inicio); + cuestionario.fecha_fin = new Date(createCuestionarioDto.fecha_fin); + 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; - } + cuestionario.id_evento = createCuestionarioDto.id_evento; const savedCuestionario = await queryRunner.manager.save(cuestionario); - // 2. Crear secciones y vincularlas al cuestionario - if (createCuestionarioDto.secciones && createCuestionarioDto.secciones.length > 0) { + // 2. Crear secciones y vincularlas + 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_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) { + + // 3. Preguntas por 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.`); + + let idTipoPregunta = 7; + + switch (preguntaDto.tipo) { + case TipoPreguntaEnum.AbiertaParrafo: + idTipoPregunta = 1; + break; + case TipoPreguntaEnum.AbiertaRespuestaCorta: + idTipoPregunta = 2; + break; + case TipoPreguntaEnum.Cerrada: + idTipoPregunta = 3; + break; + case TipoPreguntaEnum.Multiple: + idTipoPregunta = 4; + break; + default: + console.warn( + `Tipo de pregunta no reconocido: ${preguntaDto.tipo}. Se asignará como desconocido.`, + ); + idTipoPregunta = 7; + break; } - - 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.id_tipo_pregunta = idTipoPregunta; 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) { + + // 4. Crear opciones si las hay + 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); } } @@ -178,7 +160,184 @@ export class CuestionarioService { } await queryRunner.commitTransaction(); - + + return { + success: true, + message: 'Cuestionario creado exitosamente', + cuestionario: savedCuestionario, + }; + } catch (error) { + await queryRunner.rollbackTransaction(); + throw error; + } finally { + await queryRunner.release(); + } + } + + async createCuestionarioEvento( + createCuestionarioDto: CreateCuestionarioEventoDto, + ) { + 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 = new Date(createCuestionarioDto.fecha_inicio); + cuestionario.fecha_fin = new Date(createCuestionarioDto.fecha_fin); + 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]; + + let idTipoPregunta = 7; + + switch (preguntaDto.tipo) { + case TipoPreguntaEnum.AbiertaParrafo: + idTipoPregunta = 1; + break; + case TipoPreguntaEnum.AbiertaRespuestaCorta: + idTipoPregunta = 2; + break; + case TipoPreguntaEnum.Cerrada: + idTipoPregunta = 3; + break; + case TipoPreguntaEnum.Multiple: + idTipoPregunta = 4; + break; + default: + console.warn( + `Tipo de pregunta no reconocido: ${preguntaDto.tipo}. Se asignará como desconocido.`, + ); + idTipoPregunta = 7; + break; + } + + 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.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, @@ -191,106 +350,146 @@ export class CuestionarioService { } } + async asociarBanner(id: number, banner: string) { + const cuestionario = await this.cuestionarioRepository.findOne({ + where: { id_cuestionario: id }, + }); + + if (!cuestionario) { + throw new NotFoundException(`Cuestionario con ID ${id} no encontrado`); + } + + // Actualizar el banner del cuestionario + cuestionario.banner = banner; + return this.cuestionarioRepository.save(cuestionario); + } + + async getCuestionarioOrFail(id_cuestionario: number): Promise { + const cuestionario = await this.cuestionarioRepository.findOne({ + where: { id_cuestionario: id_cuestionario }, + relations: ['evento'], + }); + + if (!cuestionario) { + throw new NotFoundException( + `Cuestionario con ID ${id_cuestionario} no encontrado`, + ); + } + + return cuestionario; + } + findAll() { return this.cuestionarioRepository.find(); } async findOne(id: number) { - const cuestionario = await this.cuestionarioRepository.findOne({ - where: { id_cuestionario: id } + const cuestionario = await this.cuestionarioRepository.findOne({ + where: { id_cuestionario: id }, }); - + if (!cuestionario) { throw new NotFoundException(`Cuestionario con ID ${id} no encontrado`); } - + return cuestionario; } - + 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 + 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' + 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' } + 1: { id_tipo: 1, tipo_pregunta: TipoPreguntaEnum.AbiertaParrafo }, + 2: { id_tipo: 2, tipo_pregunta: TipoPreguntaEnum.AbiertaRespuestaCorta }, + 3: { id_tipo: 3, tipo_pregunta: TipoPreguntaEnum.Cerrada }, + 4: { id_tipo: 4, tipo_pregunta: TipoPreguntaEnum.Multiple }, }; - + // Obtener las relaciones cuestionario-seccion - const cuestionarioSecciones = await this.cuestionarioSeccionRepository.find({ - where: { id_cuestionario: id }, - order: { posicion: 'ASC' } - }); - + 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 seccionIds = cuestionarioSecciones.map((cs) => cs.id_seccion); const secciones = await this.seccionRepository.find({ - where: { id_seccion: In(seccionIds) } + 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); - + 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' } + order: { posicion: 'ASC' }, }); - + // Obtener preguntas - const preguntaIds = seccionPreguntas.map(sp => sp.id_pregunta); + const preguntaIds = seccionPreguntas.map((sp) => sp.id_pregunta); const preguntas = await this.preguntaRepository.find({ - where: { id_pregunta: In(preguntaIds) } + 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); - + 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' }; - + 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' } + order: { posicion: 'ASC' }, }); - + // Formatear las opciones según el formato requerido - const opcionesFormateadas = preguntaOpciones.map(po => ({ + 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 - } + opcion: po.opcion.opcion, + }, })); - + + console.log(opcionesFormateadas); + // Construir objeto de pregunta según el formato requerido return { id_seccion_pregunta: sp.id_seccion_pregunta, @@ -301,18 +500,17 @@ export class CuestionarioService { 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 + tipo_pregunta: tipoPregunta.tipo_pregunta, }, - opciones: opcionesFormateadas - } + opciones: opcionesFormateadas, + }, }; - }) - ).then(results => results.filter(p => p !== null)); - + }), + ).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, @@ -321,39 +519,48 @@ export class CuestionarioService { id_seccion: seccion.id_seccion, contador_pregunta: seccion.contador_pregunta, descripcion: seccion.descripcion, - titulo: seccion.titulo + titulo: seccion.titulo, }, - preguntas: preguntasFormateadas + preguntas: preguntasFormateadas, }; - }) - ).then(results => results.filter(s => s !== null)); - + }), + ).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 + 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, + 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, + fecha_inicio: cuestionario.fecha_inicio + ? cuestionario.fecha_inicio.toISOString() + : null, + fecha_fin: cuestionario.fecha_fin + ? cuestionario.fecha_fin.toISOString() + : null, id_tipo_cuestionario: cuestionario.id_tipo_cuestionario, id_evento: cuestionario.id_evento || null, - secciones: seccionesFormateadas - } + secciones: seccionesFormateadas, + }, }; } @@ -362,31 +569,31 @@ export class CuestionarioService { if (updateCuestionarioDto.evento) { // Buscar o crear evento let evento = await this.eventoRepository.findOne({ - where: { nombre_evento: updateCuestionarioDto.evento } + 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) + 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; } - + // Preparar los datos para la actualización const updateData: any = { ...updateCuestionarioDto }; - + return this.cuestionarioRepository.update(id, updateData); } diff --git a/src/cuestionario/cuestionario.documentation.ts b/src/cuestionario/docs/cuestionario.documentation.ts similarity index 51% rename from src/cuestionario/cuestionario.documentation.ts rename to src/cuestionario/docs/cuestionario.documentation.ts index 8caf16a..cf8884f 100644 --- a/src/cuestionario/cuestionario.documentation.ts +++ b/src/cuestionario/docs/cuestionario.documentation.ts @@ -1,87 +1,126 @@ -import { ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags, getSchemaPath } from '@nestjs/swagger'; -import { feriaSexualidad } from '../../utils/crear_formulario_feria'; +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'; +import { FormularioDto } from '../dto/formulario.schema'; +import { + CreateCuestionarioDto, + CreateCuestionarioEventoDto, +} from '../dto/create-cuestionario.dto'; +import { ejemploCuestionarioAlumno } from './cuestionario.examples'; export class CuestionarioApiDocumentation { // Decoradores para toda la clase del controlador - static ApiController = ApiTags('Cuestionarios'); - + static ApiController = ApiTags('Cuestionario'); + + static ApiCreate = applyDecorators( + ApiOperation({ + summary: 'Crear un nuevo cuestionario para un evento existente', + description: + 'Este endpoint crea un cuestionario con sus secciones, preguntas y opciones. Se debe proporcionar el ID de un evento existente para asociarlo.', + }), + ApiBody({ + type: CreateCuestionarioDto, + examples: { + comunidad_estudiantil: ejemploCuestionarioAlumno, + }, + }), + ); + // 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' + 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 + 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.)', + 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 - } - } + example: feriaSexualidad, + }, + }, }), - ApiResponse({ status: 404, description: 'Cuestionario no encontrado' }) + ApiResponse({ status: 404, description: 'Cuestionario no encontrado' }), ); - // Documentación para crear un cuestionario - static ApiCreate = applyDecorators( + static ApiCreateWithEvento = applyDecorators( ApiOperation({ - summary: 'Crear un nuevo cuestionario', - description: 'Crea un nuevo cuestionario con secciones, preguntas y opciones, incluyendo validaciones personalizadas' + summary: 'Crear un nuevo cuestionario con nuevo evento', + description: + 'Este endpoint crea un cuestionario y también un evento (si no existe) usando el nombre proporcionado. No se requiere `id_evento`, sólo `evento` (nombre del evento, es opcional).', + }), + ApiResponse({ + status: 201, + description: 'Cuestionario y evento creados exitosamente', + }), + ApiResponse({ + status: 400, + description: 'Datos inválidos', }), 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, + type: CreateCuestionarioEventoDto, 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 - } - } + ejemplo_con_evento_nuevo: { + summary: 'Cuestionario que crea evento automáticamente', + value: { + nombre_form: 'Registro de participantes', + descripcion: 'Formulario para inscripción de asistentes.', + evento: 'Foro Juvenil de Tecnología 2025', + fecha_inicio: '2025-07-20T10:00:00', + fecha_fin: '2025-07-20T17:00:00', + id_tipo_cuestionario: 2, + secciones: [ + { + titulo: 'Datos generales', + descripcion: 'Recopilación de información personal', + preguntas: [ + { + titulo: '¿Cuál es tu edad?', + tipo: 'Abierta', + obligatoria: true, + }, + { + titulo: '¿Cómo te enteraste del evento?', + tipo: 'Multiple', + opciones: [ + { valor: 'Redes sociales' }, + { valor: 'Correo electrónico' }, + { valor: 'Por un amigo' }, + ], + }, + ], + }, + ], + }, + }, + }, }), - 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' + description: 'Retorna una lista de todos los cuestionarios registrados', }), ApiResponse({ status: 200, @@ -92,32 +131,35 @@ export class CuestionarioApiDocumentation { type: 'object', properties: { id_cuestionario: { type: 'number', example: 1 }, - nombre_form: { type: 'string', example: 'Registro para la Feria de la Sexualidad - FES Acatlán' }, + 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 } - } - } - } + id_tipo_cuestionario: { type: 'number', example: 1 }, + }, + }, + }, }), - ApiResponse({ status: 500, description: 'Error interno del servidor' }) + 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' + description: 'Retorna un cuestionario específico por su ID', }), ApiParam({ name: 'id', description: 'ID del cuestionario', required: true, type: 'number', - example: 1 + example: 1, }), ApiResponse({ status: 200, @@ -126,45 +168,51 @@ export class CuestionarioApiDocumentation { type: 'object', properties: { id_cuestionario: { type: 'number', example: 1 }, - nombre_form: { type: 'string', example: 'Registro para la Feria de la Sexualidad - FES Acatlán' }, + 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 } - } - } + id_tipo_cuestionario: { type: 'number', example: 1 }, + }, + }, }), ApiResponse({ status: 404, description: 'Cuestionario no encontrado' }), - ApiResponse({ status: 500, description: 'Error interno del servidor' }) + 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' + description: 'Actualiza los datos de un cuestionario existente', }), ApiParam({ name: 'id', description: 'ID del cuestionario a actualizar', required: true, type: 'number', - example: 1 + example: 1, }), ApiBody({ description: 'Datos a actualizar del cuestionario', schema: { type: 'object', properties: { - nombre_form: { type: 'string', example: 'Nombre actualizado del formulario' }, + 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 } - } - } + editable: { type: 'boolean', example: true }, + }, + }, }), ApiResponse({ status: 200, @@ -172,27 +220,30 @@ export class CuestionarioApiDocumentation { schema: { type: 'object', properties: { - affected: { type: 'number', example: 1 } - } - } + affected: { type: 'number', example: 1 }, + }, + }, + }), + ApiResponse({ + status: 400, + description: 'Datos de actualización inválidos', }), - 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' }) + 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' + description: 'Elimina permanentemente un cuestionario por su ID', }), ApiParam({ name: 'id', description: 'ID del cuestionario a eliminar', required: true, type: 'number', - example: 1 + example: 1, }), ApiResponse({ status: 200, @@ -200,11 +251,11 @@ export class CuestionarioApiDocumentation { schema: { type: 'object', properties: { - affected: { type: 'number', example: 1 } - } - } + affected: { type: 'number', example: 1 }, + }, + }, }), ApiResponse({ status: 404, description: 'Cuestionario no encontrado' }), - ApiResponse({ status: 500, description: 'Error interno del servidor' }) + ApiResponse({ status: 500, description: 'Error interno del servidor' }), ); -} \ No newline at end of file +} diff --git a/src/cuestionario/docs/cuestionario.examples.ts b/src/cuestionario/docs/cuestionario.examples.ts new file mode 100644 index 0000000..10fb0a7 --- /dev/null +++ b/src/cuestionario/docs/cuestionario.examples.ts @@ -0,0 +1,139 @@ +import { TiposValidacion } from 'src/pregunta/entities/pregunta.entity'; + +export const ejemploCuestionarioAlumno = { + summary: + 'Cuestionario para el registro de asistencia a un evento para comunidad estudiantil', + value: { + id_evento: 1, + nombre_form: 'Registro', + descripcion: + 'Con este formulario estaras reservando tu lugar en el evento. Por favor, completa todos los campos obligatorios.', + fecha_inicio: '2025-08-01T08:00:00', + fecha_fin: '2025-08-10T23:59:59', + id_tipo_cuestionario: 1, + secciones: [ + { + titulo: 'Información Personal', + descripcion: + 'Proporciona tus datos personales para poder contactarte y enviarte información relevante sobre la Feria de la Sexualidad.', + preguntas: [ + { + titulo: '¿Eres parte de la comunidad de la FES Acatlán?', + tipo: 'Cerrada', + opciones: [{ valor: 'Si' }, { valor: 'No' }], + obligatoria: true, + validacion: TiposValidacion.COMUNIDAD_ALUMNO, + }, + { + titulo: 'Numero de cuenta', + tipo: 'Abierta (Respuesta corta)', + obligatoria: false, + validacion: TiposValidacion.CUENTA_ALUMNO, + }, + { + titulo: 'Correo electrónico', + tipo: 'Abierta (Respuesta corta)', + obligatoria: true, + validacion: TiposValidacion.CORREO, + }, + { + titulo: 'Nombre(s)', + tipo: 'Abierta (Respuesta corta)', + obligatoria: true, + validacion: TiposValidacion.NOMBRE, + }, + { + titulo: 'Apellidos', + tipo: 'Abierta (Respuesta corta)', + obligatoria: true, + validacion: TiposValidacion.NOMBRE, + }, + { + titulo: 'Género', + tipo: 'Cerrada', + obligatoria: true, + opciones: [ + { valor: 'Masculino' }, + { valor: 'Femenino' }, + { valor: 'Prefiero no decirlo' }, + ], + }, + { + titulo: 'Institución de procedencia', + tipo: 'Abierta (Respuesta corta)', + obligatoria: true, + }, + { + titulo: 'Carrera', + tipo: 'Abierta (Respuesta corta)', + obligatoria: true, + }, + ], + }, + ], + }, +}; + +export const ejemploCuestionarioTrabajador = { + summary: + 'Cuestionario para el registro de asistencia a un evento para comunidad trabajadora', + value: { + id_evento: 1, + nombre_form: 'Registro', + descripcion: + 'Con este formulario estaras reservando tu lugar en el evento. Por favor, completa todos los campos obligatorios.', + fecha_inicio: '2025-08-01T08:00:00', + fecha_fin: '2025-08-10T23:59:59', + id_tipo_cuestionario: 2, + secciones: [ + { + titulo: 'Información Personal', + descripcion: + 'Proporciona tus datos personales para poder contactarte y enviarte información relevante sobre la Feria de la Sexualidad.', + preguntas: [ + { + titulo: '¿Eres parte de la comunidad de la FES Acatlán?', + tipo: 'Cerrada', + opciones: [{ valor: 'Si' }, { valor: 'No' }], + obligatoria: true, + validacion: TiposValidacion.COMUNIDAD_TRABAJADOR, + }, + { + titulo: 'Número de trabajador', + tipo: 'Abierta (Respuesta corta)', + obligatoria: false, + validacion: TiposValidacion.CUENTA_TRABAJADOR, + }, + { + titulo: 'Correo electrónico institucional', + tipo: 'Abierta (Respuesta corta)', + obligatoria: true, + validacion: TiposValidacion.CORREO_INSTITUCIONAL, + }, + { + titulo: 'Nombre(s)', + tipo: 'Abierta (Respuesta corta)', + obligatoria: true, + validacion: TiposValidacion.NOMBRE, + }, + { + titulo: 'Apellidos', + tipo: 'Abierta (Respuesta corta)', + obligatoria: true, + validacion: TiposValidacion.NOMBRE, + }, + { + titulo: 'Género', + tipo: 'Cerrada', + obligatoria: true, + opciones: [ + { valor: 'Masculino' }, + { valor: 'Femenino' }, + { valor: 'Prefiero no decirlo' }, + ], + }, + ], + }, + ], + }, +}; diff --git a/src/cuestionario/dto/create-cuestionario.dto.ts b/src/cuestionario/dto/create-cuestionario.dto.ts index ee07e94..eb9c642 100644 --- a/src/cuestionario/dto/create-cuestionario.dto.ts +++ b/src/cuestionario/dto/create-cuestionario.dto.ts @@ -1,12 +1,30 @@ import { CreateSeccionDto } from '../../seccion/dto/create-seccion.dto'; -import { IsArray, IsDateString, IsNotEmpty, IsNumber, IsOptional, IsString, ValidateNested } from 'class-validator'; +import { + IsArray, + IsDateString, + IsNotEmpty, + IsNumber, + IsOptional, + IsString, + ValidateNested, +} from 'class-validator'; import { Type } from 'class-transformer'; import { ApiProperty } from '@nestjs/swagger'; +import { OmitType } from '@nestjs/mapped-types'; export class CreateCuestionarioDto { + @ApiProperty({ + description: 'ID del evento al que pertenece el cuestionario', + example: 1, + required: false, + }) + @IsNotEmpty() + @IsNumber() + id_evento: number; + @ApiProperty({ description: 'Nombre del formulario', - example: 'Registro para la Feria de la Sexualidad - FES Acatlán' + example: 'Registro para la Feria de la Sexualidad - FES Acatlán', }) @IsNotEmpty() @IsString() @@ -15,7 +33,7 @@ export class CreateCuestionarioDto { @ApiProperty({ description: 'Descripción del formulario', example: 'Formulario de registro para la feria...', - required: false + required: false, }) @IsOptional() @IsString() @@ -24,33 +42,31 @@ export class CreateCuestionarioDto { @ApiProperty({ description: 'Fecha de inicio', example: '2025-03-26T00:00:00', - required: false + required: false, }) - @IsOptional() @IsDateString() - fecha_inicio?: Date; + fecha_inicio: Date; @ApiProperty({ description: 'Fecha de fin', example: '2025-03-31T23:59:59', - required: false + required: false, }) - @IsOptional() @IsDateString() - fecha_fin?: Date; + fecha_fin: Date; @ApiProperty({ description: 'ID del tipo de cuestionario', - example: 1 + example: 1, }) @IsNotEmpty() @IsNumber() id_tipo_cuestionario: number; - + @ApiProperty({ description: 'Nombre del evento asociado', example: 'Feria de la Sexualidad', - required: false + required: false, }) @IsOptional() @IsString() @@ -59,7 +75,7 @@ export class CreateCuestionarioDto { @ApiProperty({ description: 'Secciones del cuestionario', type: [CreateSeccionDto], - required: false + required: false, }) @IsOptional() @IsArray() @@ -67,3 +83,8 @@ export class CreateCuestionarioDto { @Type(() => CreateSeccionDto) secciones?: CreateSeccionDto[]; } + +export class CreateCuestionarioEventoDto extends OmitType( + CreateCuestionarioDto, + ['id_evento'] as const, +) {} diff --git a/src/cuestionario/dto/formulario.schema.ts b/src/cuestionario/dto/formulario.schema.ts index f1478be..00205cc 100644 --- a/src/cuestionario/dto/formulario.schema.ts +++ b/src/cuestionario/dto/formulario.schema.ts @@ -1,5 +1,4 @@ import { ApiProperty } from '@nestjs/swagger'; -import { TipoPreguntaEnum } from '../../pregunta/dto/create-pregunta.dto'; // Ejemplo de respuesta de cuestionario completo export const FormularioFeriaSchema = { @@ -37,7 +36,6 @@ export const FormularioFeriaSchema = { contador_opcion: 2, obligatoria: true, id_tipo_pregunta: 1, - id_opcion_dependiente: null, tipo_pregunta: { id_tipo: 1, tipo_pregunta: 'Cerrada' @@ -73,7 +71,6 @@ export const FormularioFeriaSchema = { 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: { @@ -92,7 +89,6 @@ export const FormularioFeriaSchema = { 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: { @@ -123,7 +119,6 @@ export const FormularioFeriaSchema = { contador_opcion: 5, obligatoria: true, id_tipo_pregunta: 3, - id_opcion_dependiente: null, tipo_pregunta: { id_tipo: 3, tipo_pregunta: 'Multiple' @@ -186,7 +181,6 @@ export const FormularioFeriaSchema = { 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, diff --git a/src/cuestionario/entities/cuestionario.entity.ts b/src/cuestionario/entities/cuestionario.entity.ts index 535d2b2..ca73447 100644 --- a/src/cuestionario/entities/cuestionario.entity.ts +++ b/src/cuestionario/entities/cuestionario.entity.ts @@ -1,9 +1,15 @@ +import { + Entity, + PrimaryGeneratedColumn, + Column, + OneToMany, + ManyToOne, + JoinColumn, +} from 'typeorm'; import { CuestionarioSeccion } from 'src/cuestionario_seccion/entities/cuestionario_seccion.entity'; -import { Evento } from 'src/evento/evento.entity'; +import { Evento } from 'src/evento/entities/evento.entity'; +import { ParticipanteEvento } from 'src/participante_evento/entities/participante_evento.entity'; import { TipoCuestionario } from 'src/tipo_cuestionario/entities/tipo_cuestionario.entity'; -import { Entity, PrimaryGeneratedColumn, Column, OneToMany, ManyToMany, ManyToOne, JoinColumn } from 'typeorm'; - - @Entity('cuestionario') export class Cuestionario { @@ -13,6 +19,9 @@ export class Cuestionario { @Column({ type: 'text' }) nombre_form: string; + @Column({ type: 'text', nullable: true }) + banner: string; + @Column({ type: 'int' }) contador_secciones: number; @@ -22,29 +31,40 @@ export class Cuestionario { @Column({ type: 'boolean', default: true }) editable: boolean; - @Column({ type: 'datetime', nullable: true }) - fecha_fin?: Date; + @Column({ type: 'datetime' }) + fecha_inicio: Date; - @Column({ type: 'datetime', nullable: true }) - fecha_inicio?: Date; - - - @OneToMany(() => CuestionarioSeccion,(cuestionario_seccion) => cuestionario_seccion.id_cuestionario) - cuestionarioSeccion:CuestionarioSeccion[] - - @Column({ type: 'int', nullable: true }) - id_cuestionario_original: number; + @Column({ type: 'datetime' }) + fecha_fin: Date; @Column({ type: 'int' }) id_tipo_cuestionario: number; - @ManyToOne(()=> Cuestionario) - cuestionarioOriginal: Cuestionario; - - @Column({ type: 'int', nullable: true }) + @Column({ type: 'int' }) id_evento: number; - - @ManyToOne(() => Evento) + + // Relaciones + @ManyToOne(() => Cuestionario, { nullable: true }) + @JoinColumn({ name: 'id_cuestionario_original' }) + cuestionarioOriginal?: Cuestionario; + + @ManyToOne(() => Evento, (evento) => evento.cuestionarios, { nullable: true }) @JoinColumn({ name: 'id_evento' }) - evento: Evento; + evento?: Evento; + + @ManyToOne(() => TipoCuestionario, (tipo) => tipo.cuestionarios) + @JoinColumn({ name: 'id_tipo_cuestionario' }) + tipoCuestionario: TipoCuestionario; + + @OneToMany( + () => CuestionarioSeccion, + (cuestionarioSeccion) => cuestionarioSeccion.cuestionario, + ) + cuestionarioSeccion: CuestionarioSeccion[]; + + @OneToMany( + () => ParticipanteEvento, + (participanteEvento) => participanteEvento.cuestionario, + ) + inscripciones: ParticipanteEvento[]; } diff --git a/src/cuestionario_respondido/cuestionario_respondido.controller.spec.ts b/src/cuestionario_respondido/cuestionario_respondido.controller.spec.ts deleted file mode 100644 index c96795c..0000000 --- a/src/cuestionario_respondido/cuestionario_respondido.controller.spec.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { CuestionarioRespondidoController } from './cuestionario_respondido.controller'; -import { CuestionarioRespondidoService } from './cuestionario_respondido.service'; - -describe('CuestionarioRespondidoController', () => { - let controller: CuestionarioRespondidoController; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [CuestionarioRespondidoController], - providers: [CuestionarioRespondidoService], - }).compile(); - - controller = module.get(CuestionarioRespondidoController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); -}); diff --git a/src/cuestionario_respondido/cuestionario_respondido.controller.ts b/src/cuestionario_respondido/cuestionario_respondido.controller.ts index 3e39867..2b0a3b0 100644 --- a/src/cuestionario_respondido/cuestionario_respondido.controller.ts +++ b/src/cuestionario_respondido/cuestionario_respondido.controller.ts @@ -18,17 +18,7 @@ export class CuestionarioRespondidoController { } @Post('submit') - @CuestionarioRespondidoApiDocumentation.ApiSubmitRespuestas - @ApiBody({ - type: SubmitRespuestasDto, - examples: { - comunidad: CuestionarioRespondidoApiDocumentation.ApiSubmitRespuestasExamples.communityExample, - externos: CuestionarioRespondidoApiDocumentation.ApiSubmitRespuestasExamples.externalExample - } - }) - @CuestionarioRespondidoApiDocumentation.ApiSubmitResponse201 - @CuestionarioRespondidoApiDocumentation.ApiSubmitResponse400 - @CuestionarioRespondidoApiDocumentation.ApiSubmitResponse404 + @CuestionarioRespondidoApiDocumentation.ApiSubmit submitRespuestas(@Body() submitRespuestasDto: SubmitRespuestasDto) { return this.cuestionarioRespondidoService.submitRespuestas(submitRespuestasDto); } diff --git a/src/cuestionario_respondido/cuestionario_respondido.documentation.ts b/src/cuestionario_respondido/cuestionario_respondido.documentation.ts index 531e303..c50edb6 100644 --- a/src/cuestionario_respondido/cuestionario_respondido.documentation.ts +++ b/src/cuestionario_respondido/cuestionario_respondido.documentation.ts @@ -1,26 +1,81 @@ -import { ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { + ApiBody, + ApiOperation, + ApiParam, + ApiResponse, + ApiTags, +} from '@nestjs/swagger'; import { applyDecorators } from '@nestjs/common'; +import { SubmitRespuestasDto } from './dto/submit-respuestas.dto'; + +export const ejemploEnvioRespuestas = { + summary: 'Ejemplo de envío de respuestas a un formulario', + value: { + id_cuestionario: 1, + correo: 'miguel@acatlan.unam.mx', + fecha_envio: '2025-07-10T10:15:00', // opcional + + respuestas: [ + { + id_pregunta: 1, // ¿Eres parte de la comunidad de la FES Acatlán? + valor: 1, // Si (id_opcion: 1) + }, + { + id_pregunta: 2, // Numero de cuenta + valor: '123456789', // texto + }, + { + id_pregunta: 3, // Correo electrónico + valor: 'miguel@acatlan.unam.mx', + }, + { + id_pregunta: 4, // Nombre(s) + valor: 'Liliana', + }, + { + id_pregunta: 5, // Apellidos + valor: 'Alvarado Díaz', + }, + { + id_pregunta: 6, // Género + valor: 4, // Femenino (id_opcion: 4) + }, + { + id_pregunta: 7, // Institución de procedencia + valor: 'FES Acatlán', + }, + { + id_pregunta: 8, // Carrera + valor: 'Matemáticas Aplicadas y Computación', + }, + ], + }, +}; 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 ApiSubmit = applyDecorators( + ApiOperation({ + summary: 'Enviar respuestas a un cuestionario', + description: `Este endpoint permite a un participante enviar respuestas a un cuestionario previamente creado. +Se asocia el formulario con el participante mediante su correo electrónico. Si es la primera vez que responde, +se registra automáticamente. + +Las respuestas pueden ser de tipo: +- Abierta (texto) +- Cerrada (una opción) +- Múltiple (varias opciones) + +Además, si el cuestionario está ligado a un evento, se registra su participación en dicho evento y se le envía un correo con un código QR para validar asistencia.`, + }), + ApiBody({ + type: SubmitRespuestasDto, + examples: { + envio_respuestas: ejemploEnvioRespuestas, + }, + }), + ); static ApiSubmitResponse201 = ApiResponse({ status: 201, @@ -28,10 +83,13 @@ export class CuestionarioRespondidoApiDocumentation { schema: { properties: { success: { type: 'boolean', example: true }, - message: { type: 'string', example: 'Respuestas guardadas correctamente' }, - cuestionarioRespondidoId: { type: 'number', example: 1 } - } - } + message: { + type: 'string', + example: 'Respuestas guardadas correctamente', + }, + cuestionarioRespondidoId: { type: 'number', example: 1 }, + }, + }, }); static ApiSubmitResponse400 = ApiResponse({ @@ -40,10 +98,13 @@ export class CuestionarioRespondidoApiDocumentation { 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' } - } - } + 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({ @@ -52,10 +113,13 @@ export class CuestionarioRespondidoApiDocumentation { schema: { properties: { statusCode: { type: 'number', example: 404 }, - message: { type: 'string', example: 'Cuestionario con ID 999 no encontrado' }, - error: { type: 'string', example: 'Not Found' } - } - } + message: { + type: 'string', + example: 'Cuestionario con ID 999 no encontrado', + }, + error: { type: 'string', example: 'Not Found' }, + }, + }, }); static get ApiReporteRespuestas() { @@ -74,16 +138,17 @@ export class CuestionarioRespondidoApiDocumentation { - Cuestionario - Pregunta - Respuesta - ` + `, }), ApiResponse({ status: 200, - description: 'Reporte de respuestas descargado exitosamente como archivo CSV' + description: + 'Reporte de respuestas descargado exitosamente como archivo CSV', }), ApiResponse({ status: 404, - description: 'Cuestionario no encontrado' - }) + description: 'Cuestionario no encontrado', + }), ); } @@ -97,14 +162,16 @@ export class CuestionarioRespondidoApiDocumentation { { 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) + { id_pregunta: 106, valor: 7 }, // Opción "Prefiero no decirlo" (usando el ID de la opción) ], - fecha_envio: '2025-04-02T13:45:00' + fecha_envio: '2025-04-02T13:45:00', }, - description: 'Datos de un estudiante de la FES Acatlán con respuestas abiertas y opciones por ID.' + 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', + summary: + 'Ejemplo de respuesta para un participante externo con selección múltiple', value: { id_formulario: 1, correo: 'externo@ejemplo.com', @@ -115,11 +182,12 @@ export class CuestionarioRespondidoApiDocumentation { { 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 + { id_pregunta: 107, valor: 'UAM Azcapotzalco' }, // Institución de procedencia ], - fecha_envio: '2025-04-02T13:45:00' + fecha_envio: '2025-04-02T13:45:00', }, - description: 'Datos de un participante externo con respuestas abiertas, opciones por ID y selección múltiple.' - } + description: + 'Datos de un participante externo con respuestas abiertas, opciones por ID y selección múltiple.', + }, }; -} \ No newline at end of file +} diff --git a/src/cuestionario_respondido/cuestionario_respondido.module.ts b/src/cuestionario_respondido/cuestionario_respondido.module.ts index d0cdb61..b0ba224 100644 --- a/src/cuestionario_respondido/cuestionario_respondido.module.ts +++ b/src/cuestionario_respondido/cuestionario_respondido.module.ts @@ -5,7 +5,7 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { CuestionarioRespondido } from './entities/cuestionario_respondido.entity'; import { CuestionarioModule } from '../cuestionario/cuestionario.module'; import { ParticipanteModule } from '../participante/participante.module'; -import { Participante } from '../participante/participante.entity'; +import { Participante } from '../participante/entities/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'; @@ -24,14 +24,14 @@ import { ValidacionesModule } from '../validaciones/validaciones.module'; RespuestaParticipanteAbierta, RespuestaParticipanteCerrada, PreguntaOpcion, - Opcion + Opcion, ]), CuestionarioModule, ParticipanteModule, - ValidacionesModule + ValidacionesModule, ], controllers: [CuestionarioRespondidoController], providers: [CuestionarioRespondidoService], - exports: [TypeOrmModule] + exports: [TypeOrmModule], }) export class CuestionarioRespondidoModule {} diff --git a/src/cuestionario_respondido/cuestionario_respondido.service.spec.ts b/src/cuestionario_respondido/cuestionario_respondido.service.spec.ts deleted file mode 100644 index 477946b..0000000 --- a/src/cuestionario_respondido/cuestionario_respondido.service.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { CuestionarioRespondidoService } from './cuestionario_respondido.service'; - -describe('CuestionarioRespondidoService', () => { - let service: CuestionarioRespondidoService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [CuestionarioRespondidoService], - }).compile(); - - service = module.get(CuestionarioRespondidoService); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); -}); diff --git a/src/cuestionario_respondido/cuestionario_respondido.service.ts b/src/cuestionario_respondido/cuestionario_respondido.service.ts index cf248a9..e7677e0 100644 --- a/src/cuestionario_respondido/cuestionario_respondido.service.ts +++ b/src/cuestionario_respondido/cuestionario_respondido.service.ts @@ -1,11 +1,15 @@ -import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; +import { + BadRequestException, + Injectable, + NotFoundException, +} from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository, DataSource } from 'typeorm'; import { CreateCuestionarioRespondidoDto } from './dto/create-cuestionario_respondido.dto'; import { UpdateCuestionarioRespondidoDto } from './dto/update-cuestionario_respondido.dto'; import { SubmitRespuestasDto } from './dto/submit-respuestas.dto'; import { CuestionarioRespondido } from './entities/cuestionario_respondido.entity'; -import { Participante } from '../participante/participante.entity'; +import { Participante } from '../participante/entities/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'; @@ -15,6 +19,9 @@ import { Opcion } from '../opcion/entities/opcion.entity'; import { ValidadorRespuestasService } from '../validaciones/validador-respuestas.service'; import axios from 'axios'; import * as QRCode from 'qrcode'; +import { CuestionarioService } from 'src/cuestionario/cuestionario.service'; +import { generarHtmlCorreoAsistencia } from 'src/emails/registro-evento'; +import { TipoPreguntaEnum } from 'src/tipo_pregunta/entities/tipo_pregunta.entity'; @Injectable() export class CuestionarioRespondidoService { constructor( @@ -26,16 +33,12 @@ export class CuestionarioRespondidoService { private cuestionarioRepository: Repository, @InjectRepository(Pregunta) private preguntaRepository: Repository, - @InjectRepository(RespuestaParticipanteAbierta) - private respuestaAbiertaRepository: Repository, - @InjectRepository(RespuestaParticipanteCerrada) - private respuestaCerradaRepository: Repository, @InjectRepository(PreguntaOpcion) private preguntaOpcionRepository: Repository, - @InjectRepository(Opcion) - private opcionRepository: Repository, + private dataSource: DataSource, private validadorRespuestasService: ValidadorRespuestasService, + private cuestionarioService: CuestionarioService, ) {} create(createCuestionarioRespondidoDto: CreateCuestionarioRespondidoDto) { @@ -49,67 +52,89 @@ export class CuestionarioRespondidoService { 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`); - } + const cuestionario = await this.cuestionarioService.getCuestionarioOrFail( + submitDto.id_cuestionario, + ); // 2. Buscar o crear participante let participante = await this.participanteRepository.findOne({ - where: { correo: submitDto.correo } + 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); } + // Verificar si el participante ya ha respondido este cuestionario + const cuestionarioRespondidoExistente = await queryRunner.manager.findOne( + CuestionarioRespondido, + { + where: { + cuestionario: { id_cuestionario: submitDto.id_cuestionario }, + participante: { id_participante: participante.id_participante }, + }, + relations: ['cuestionario', 'participante'], + }, + ); + if (cuestionarioRespondidoExistente) { + throw new BadRequestException( + `El participante con correo ${submitDto.correo} ya ha respondido el cuestionario ${cuestionario.nombre_form}.`, + ); + } + // 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); + 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'] + relations: ['tipoPregunta'], }); if (!pregunta) { - throw new NotFoundException(`Pregunta con ID ${respuestaDto.id_pregunta} no encontrada`); + 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 - ); - + 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}`); + 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'; - + const esPreguntaTipoAbierta = + pregunta.tipoPregunta.tipo_pregunta === + TipoPreguntaEnum.AbiertaParrafo || + pregunta.tipoPregunta.tipo_pregunta === + TipoPreguntaEnum.AbiertaRespuestaCorta; + // Determinar tipo de pregunta y guardar respuesta adecuadamente if (esPreguntaAbierta || esPreguntaTipoAbierta) { // Respuesta abierta (texto) @@ -123,34 +148,38 @@ export class CuestionarioRespondidoService { 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]; - + // Asegurarse de que opcionIds sea un array de números + const opcionIds = Array.isArray(respuestaDto.valor) + ? respuestaDto.valor.map((v) => Number(v)) + : [Number(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(); - + const preguntaOpciones = await this.preguntaOpcionRepository.find({ + where: { + id_pregunta: pregunta.id_pregunta, + id_opcion: opcionId, + }, + }); + if (preguntaOpciones.length === 0) { - throw new BadRequestException(`La opción con ID ${opcionId} no está asociada a la pregunta ${respuestaDto.id_pregunta}`); + 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; - + respuestaCerrada.cuestionarioRespondido = + savedCuestionarioRespondido; + await queryRunner.manager.save(respuestaCerrada); } } @@ -160,100 +189,109 @@ export class CuestionarioRespondidoService { let datosQR: { id_participante: number; id_evento: number; + id_cuestionario: number; } | null = null; - + + console.log(cuestionario.evento); + // 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 + id_evento: cuestionario.evento.id_evento, + id_cuestionario: cuestionario.id_cuestionario, }; } - + // 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) { + if (cuestionario && cuestionario.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] + `SELECT * FROM participante_evento WHERE id_participante = ? AND id_cuestionario = ?`, + [participante.id_participante, cuestionario.id_cuestionario], ); - + // Si no existe, crear la relación - if (!participanteEventoExistente || participanteEventoExistente.length === 0) { + 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] + `INSERT INTO participante_evento (id_participante, id_cuestionario, fecha_asistencia) VALUES (?, ?, ?)`, + [ + participante.id_participante, + cuestionario.id_cuestionario, + null, // o puedes omitir esta columna y no incluirla en el query + ], ); } } catch (dbError) { - console.error('Error al registrar participante en evento:', dbError.message); + 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'; - + const urlApiCorreos = process.env.url_api_correos; + + if (!urlApiCorreos) { + throw new BadRequestException( + 'No se encontro servicio de correos configurado', + ); + } + await axios.post(`${urlApiCorreos}/mail/send`, { to: participante.correo, - subject: 'Asistencia QR', - fecha_recibido: "2025-03-10T12:00:00Z", - html: ` -

¡Gracias por registrarte!

-

Has completado exitosamente el formulario: ${cuestionario.nombre_form}

-

Deberás presentar este QR (celular o impreso) en cualquier stand para que valide tu asistencia y -recibas tu constancia.

-

Este es tu QR para registrar asistencia:

- Código QR -

Tus datos de registro:

-
    -
  • Evento: ${cuestionario.evento.nombre_evento || 'Evento'}
  • -
  • Correo: ${participante.correo}
  • -
  • Fecha de registro: ${new Date().toLocaleString()}
  • -
- `, + subject: '🎓 Código QR para validar asistencia', + fecha_recibido: '2025-03-10T12:00:00Z', + html: generarHtmlCorreoAsistencia({ + nombreForm: cuestionario.nombre_form, + nombreEvento: cuestionario.evento?.nombre_evento, + correo: participante.correo, + fechaRegistro: new Date().toLocaleString(), + }), adjuntos: [ { filename: 'qr.png', content: qrBuffer.toString('base64'), encoding: 'base64', - cid: 'qrCode' - } - ] + 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 + 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 @@ -270,15 +308,23 @@ recibas tu constancia.

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'] - }); + 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(` + const respuestasCerradas = await this.dataSource.query( + ` SELECT rpc.idRespuestaParticipanteCerrada, rpc.id_pregunta_opcion, @@ -292,29 +338,34 @@ recibas tu constancia.

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]); + `, + [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 - } - })); + 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 + respuestasCerradas: respuestasCerradasFormateadas, }; - }) + }), ); return resultados; @@ -322,24 +373,28 @@ recibas tu constancia.

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' - ] - }); + 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`); + 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(` + const respuestasCerradas = await this.dataSource.query( + ` SELECT rpc.idRespuestaParticipanteCerrada, rpc.id_pregunta_opcion, @@ -353,35 +408,46 @@ recibas tu constancia.

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]); + `, + [id], + ); - console.log('Respuestas cerradas SQL:', JSON.stringify(respuestasCerradas, null, 2)); + 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 - } - })); + 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 + respuestasCerradas: respuestasCerradasFormateadas, }; return resultado; } - update(id: number, updateCuestionarioRespondidoDto: UpdateCuestionarioRespondidoDto) { + update( + id: number, + updateCuestionarioRespondidoDto: UpdateCuestionarioRespondidoDto, + ) { return `This action updates a #${id} cuestionarioRespondido`; } @@ -390,6 +456,101 @@ recibas tu constancia.

} async generarReporteRespuestas(idCuestionario: number): Promise { + const cuestionario = await this.cuestionarioRepository.findOne({ + where: { id_cuestionario: idCuestionario }, + relations: ['evento'], + }); + + if (!cuestionario) { + throw new NotFoundException( + `Cuestionario con ID ${idCuestionario} no encontrado`, + ); + } + + const preguntas = await this.dataSource.query( + ` + SELECT + p.id_pregunta, + p.pregunta + FROM pregunta p + JOIN seccion_pregunta sp ON p.id_pregunta = sp.id_pregunta + JOIN seccion s ON sp.id_seccion = s.id_seccion + JOIN cuestionario_seccion cs ON s.id_seccion = cs.id_seccion + WHERE cs.id_cuestionario = ? + ORDER BY sp.posicion, p.id_pregunta + `, + [idCuestionario], + ); + + const respuestasAbiertas = await this.dataSource.query( + ` + SELECT + cr.id_cuestionario_respondido, + p.id_pregunta, + rpa.respuesta + FROM respuesta_participante_abierta rpa + JOIN pregunta p ON rpa.id_pregunta = p.id_pregunta + JOIN cuestionario_respondido cr ON rpa.id_cuestionario_respondido = cr.id_cuestionario_respondido + WHERE cr.id_cuestionario = ? + `, + [idCuestionario], + ); + + const respuestasCerradas = await this.dataSource.query( + ` + SELECT + cr.id_cuestionario_respondido, + p.id_pregunta, + o.opcion + FROM respuesta_participante_cerrada rpc + JOIN pregunta_opcion po ON rpc.id_pregunta_opcion = po.id_pregunta_opcion + JOIN pregunta p ON po.id_pregunta = p.id_pregunta + JOIN opcion o ON po.id_opcion = o.id_opcion + JOIN cuestionario_respondido cr ON rpc.id_cuestionario_respondido = cr.id_cuestionario_respondido + WHERE cr.id_cuestionario = ? + `, + [idCuestionario], + ); + + const mapaRespuestas = new Map>(); + + respuestasAbiertas.forEach((r) => { + if (!mapaRespuestas.has(r.id_cuestionario_respondido)) { + mapaRespuestas.set(r.id_cuestionario_respondido, new Map()); + } + mapaRespuestas + .get(r.id_cuestionario_respondido)! + .set(r.id_pregunta, r.respuesta); + }); + + respuestasCerradas.forEach((r) => { + if (!mapaRespuestas.has(r.id_cuestionario_respondido)) { + mapaRespuestas.set(r.id_cuestionario_respondido, new Map()); + } + const mapa = mapaRespuestas.get(r.id_cuestionario_respondido)!; + const anterior = mapa.get(r.id_pregunta) || ''; + mapa.set(r.id_pregunta, anterior ? `${anterior}, ${r.opcion}` : r.opcion); + }); + + // Generar encabezado CSV + const encabezados = preguntas.map( + (p) => `"${p.pregunta.replace(/"/g, '""')}"`, + ); + let csv = encabezados.join(',') + '\n'; + + // Generar filas por participante + for (const [, mapa] of mapaRespuestas) { + const fila = preguntas.map((p) => { + const valor = mapa.get(p.id_pregunta) || ''; + return `"${valor.replace(/"/g, '""')}"`; // escapamos comillas dobles + }); + csv += fila.join(',') + '\n'; + } + + return csv; + } + + /* async generarReporteRespuestas(idCuestionario: number): Promise { // Buscar el cuestionario para validar que existe const cuestionario = await this.cuestionarioRepository.findOne({ where: { id_cuestionario: idCuestionario }, @@ -397,11 +558,14 @@ recibas tu constancia.

}); if (!cuestionario) { - throw new NotFoundException(`Cuestionario con ID ${idCuestionario} no encontrado`); + throw new NotFoundException( + `Cuestionario con ID ${idCuestionario} no encontrado`, + ); } // Obtener todas las preguntas del cuestionario para los encabezados - const preguntas = await this.dataSource.query(` + const preguntas = await this.dataSource.query( + ` SELECT p.id_pregunta, p.pregunta, @@ -412,10 +576,13 @@ recibas tu constancia.

JOIN cuestionario_seccion cs ON s.id_seccion = cs.id_seccion WHERE cs.id_cuestionario = ? ORDER BY sp.posicion, p.id_pregunta - `, [idCuestionario]); + `, + [idCuestionario], + ); // Obtener los participantes que han respondido este cuestionario - const participantes = await this.dataSource.query(` + const participantes = await this.dataSource.query( + ` SELECT DISTINCT cr.id_cuestionario_respondido, cr.fecha_respuesta, @@ -427,10 +594,16 @@ recibas tu constancia.

LEFT JOIN participante_evento pe ON p.id_participante = pe.id_participante AND pe.id_evento = ? WHERE cr.id_cuestionario = ? ORDER BY cr.fecha_respuesta DESC - `, [cuestionario.evento ? cuestionario.evento.id_evento : null, idCuestionario]); + `, + [ + cuestionario.evento ? cuestionario.evento.id_evento : null, + idCuestionario, + ], + ); // Obtener todas las respuestas para este cuestionario - const respuestasAbiertas = await this.dataSource.query(` + const respuestasAbiertas = await this.dataSource.query( + ` SELECT cr.id_cuestionario_respondido, p.id_pregunta, @@ -439,9 +612,12 @@ recibas tu constancia.

JOIN pregunta p ON rpa.id_pregunta = p.id_pregunta JOIN cuestionario_respondido cr ON rpa.id_cuestionario_respondido = cr.id_cuestionario_respondido WHERE cr.id_cuestionario = ? - `, [idCuestionario]); + `, + [idCuestionario], + ); - const respuestasCerradas = await this.dataSource.query(` + const respuestasCerradas = await this.dataSource.query( + ` SELECT cr.id_cuestionario_respondido, p.id_pregunta, @@ -452,25 +628,30 @@ recibas tu constancia.

JOIN opcion o ON po.id_opcion = o.id_opcion JOIN cuestionario_respondido cr ON rpc.id_cuestionario_respondido = cr.id_cuestionario_respondido WHERE cr.id_cuestionario = ? - `, [idCuestionario]); + `, + [idCuestionario], + ); // Crear un mapa de respuestas por participante y pregunta const respuestasPorParticipante = new Map(); - + // Agregar respuestas abiertas al mapa - respuestasAbiertas.forEach(respuesta => { + respuestasAbiertas.forEach((respuesta) => { const key = `${respuesta.id_cuestionario_respondido}_${respuesta.id_pregunta}`; respuestasPorParticipante.set(key, respuesta.respuesta); }); - + // Agregar respuestas cerradas al mapa (posiblemente múltiples por pregunta) - respuestasCerradas.forEach(respuesta => { + respuestasCerradas.forEach((respuesta) => { const key = `${respuesta.id_cuestionario_respondido}_${respuesta.id_pregunta}`; const respuestaActual = respuestasPorParticipante.get(key); - + if (respuestaActual) { // Si ya hay una respuesta para esta pregunta, agregar esta opción - respuestasPorParticipante.set(key, `${respuestaActual}, ${respuesta.opcion}`); + respuestasPorParticipante.set( + key, + `${respuestaActual}, ${respuesta.opcion}`, + ); } else { respuestasPorParticipante.set(key, respuesta.opcion); } @@ -478,54 +659,62 @@ recibas tu constancia.

// Preparar los datos para el CSV const datosReporte: string[][] = []; - + // Agregar encabezados con datos del participante seguidos por las preguntas const encabezados = [ - 'ID Participante', - 'Correo', + 'ID Participante', + 'Correo', 'Fecha Respuesta', - 'Asistencia' + 'Asistencia', ]; - + // Agregar cada pregunta como un encabezado - preguntas.forEach(pregunta => { + preguntas.forEach((pregunta) => { encabezados.push(pregunta.pregunta); }); - + datosReporte.push(encabezados); - + // Crear una fila por cada participante - participantes.forEach(participante => { + participantes.forEach((participante) => { const fila = [ participante.id_participante.toString(), participante.correo_participante, new Date(participante.fecha_respuesta).toLocaleString(), - participante.fecha_asistencia ? 'Sí' : 'No' + participante.fecha_asistencia ? 'Sí' : 'No', ]; - + // Agregar cada respuesta en el orden de las preguntas - preguntas.forEach(pregunta => { + preguntas.forEach((pregunta) => { const key = `${participante.id_cuestionario_respondido}_${pregunta.id_pregunta}`; const respuesta = respuestasPorParticipante.get(key) || ''; fila.push(respuesta); }); - + datosReporte.push(fila); }); - + // 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'); - + 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; - } + } */ } diff --git a/src/cuestionario_respondido/dto/submit-respuestas.dto.ts b/src/cuestionario_respondido/dto/submit-respuestas.dto.ts index 4781f76..c62a56b 100644 --- a/src/cuestionario_respondido/dto/submit-respuestas.dto.ts +++ b/src/cuestionario_respondido/dto/submit-respuestas.dto.ts @@ -1,21 +1,21 @@ import { ApiProperty } from '@nestjs/swagger'; -import { - IsArray, - IsDateString, - IsNotEmpty, - IsNumber, - IsOptional, - IsString, - ValidateNested, +import { + IsArray, + IsDateString, + IsNotEmpty, + IsNumber, + IsOptional, + IsString, + ValidateNested, ValidateIf, - MaxLength + MaxLength, } from 'class-validator'; import { Type, Transform } from 'class-transformer'; export class RespuestaDto { @ApiProperty({ description: 'ID de la pregunta respondida', - example: 101 + example: 101, }) @IsNumber() id_pregunta: number; @@ -23,40 +23,47 @@ export class RespuestaDto { @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) + '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: 'string', + description: 'Texto para pregunta abierta (máximo 250 caracteres)', + }, { type: 'number', description: 'ID de opción para pregunta cerrada' }, - { - type: 'array', + { + type: 'array', items: { type: 'number' }, - description: 'Array de IDs de opciones para pregunta multiple' - } - ] + 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', }) - @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 + description: 'ID del cuestionario a responder', + example: 1, }) @IsNumber() - id_formulario: number; + id_cuestionario: number; @ApiProperty({ description: 'Correo electrónico del participante', - example: 'usuario@ejemplo.com' + example: 'usuario@ejemplo.com', }) @IsString() @IsNotEmpty() - @MaxLength(250, { message: 'El correo electrónico no puede exceder 250 caracteres' }) + @MaxLength(250, { + message: 'El correo electrónico no puede exceder 250 caracteres', + }) correo: string; @ApiProperty({ @@ -64,18 +71,18 @@ export class SubmitRespuestasDto { type: [RespuestaDto], examples: [ { - id_pregunta: 101, - valor: 'Texto para pregunta abierta' + id_pregunta: 101, + valor: 'Texto para pregunta abierta', }, { id_pregunta: 102, - valor: 5 // ID de la opción para pregunta cerrada simple + valor: 5, }, { id_pregunta: 103, - valor: [2, 7, 9] // Array de IDs de opciones para pregunta de selección múltiple - } - ] + valor: [2, 7, 9], + }, + ], }) @IsArray() @ValidateNested({ each: true }) @@ -84,9 +91,9 @@ export class SubmitRespuestasDto { @ApiProperty({ description: 'Fecha y hora del envío', - example: '2025-04-02T13:45:00' + example: '2025-04-02T13:45:00', }) @IsDateString() @IsOptional() fecha_envio?: string; -} \ No newline at end of file +} diff --git a/src/cuestionario_respondido/entities/cuestionario_respondido.entity.ts b/src/cuestionario_respondido/entities/cuestionario_respondido.entity.ts index 00a3b57..1e79bc0 100644 --- a/src/cuestionario_respondido/entities/cuestionario_respondido.entity.ts +++ b/src/cuestionario_respondido/entities/cuestionario_respondido.entity.ts @@ -3,7 +3,7 @@ import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, OneToMany, JoinColum 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 { Participante } from 'src/participante/entities/participante.entity'; import { Opcion } from 'src/opcion/entities/opcion.entity'; @Entity('cuestionario_respondido') diff --git a/src/cuestionario_seccion/entities/cuestionario_seccion.entity.ts b/src/cuestionario_seccion/entities/cuestionario_seccion.entity.ts index 9efcd9a..4b2f99a 100644 --- a/src/cuestionario_seccion/entities/cuestionario_seccion.entity.ts +++ b/src/cuestionario_seccion/entities/cuestionario_seccion.entity.ts @@ -10,7 +10,6 @@ export class CuestionarioSeccion { @Column({ type: 'tinyint' }) posicion: number; - @ManyToOne(() => Cuestionario, (cuestionario) => cuestionario.cuestionarioSeccion) cuestionario: Cuestionario; diff --git a/src/db/typeorm.config.ts b/src/db/typeorm.config.ts new file mode 100644 index 0000000..7bef1a2 --- /dev/null +++ b/src/db/typeorm.config.ts @@ -0,0 +1,37 @@ +import { ConfigService } from '@nestjs/config'; +import { TypeOrmModuleOptions } from '@nestjs/typeorm'; +import { RegistroAlumno } from '../alumnos/entities/registro-alumno.entity'; + +export const createMainDbConfig = async ( + configService: ConfigService, +): Promise => ({ + type: 'mariadb', + host: configService.get('DB_HOST'), + port: configService.get('DB_PORT'), + username: configService.get('DB_USERNAME'), + password: configService.get('DB_PASSWORD'), + database: configService.get('DB_DATABASE'), + entities: [__dirname + '/../**/*.entity{.ts,.js}'], + autoLoadEntities: true, + synchronize: true, + retryAttempts: 5, + retryDelay: 3000, + dropSchema: true, +}); + +export const createAlumnosDbConfig = async ( + configService: ConfigService, +): Promise => ({ + name: 'alumnosConnection', + type: 'mariadb', + host: configService.get('ALUMNOS_DB_HOST'), + port: configService.get('ALUMNOS_DB_PORT'), + username: configService.get('ALUMNOS_DB_USERNAME'), + password: configService.get('ALUMNOS_DB_PASSWORD'), + database: configService.get('ALUMNOS_DB_DATABASE'), + entities: [RegistroAlumno], + synchronize: false, + retryAttempts: 10, + retryDelay: 3000, + connectTimeout: 30000, +}); diff --git a/src/docs/swagger.config.ts b/src/docs/swagger.config.ts index 94993e1..93f50b9 100644 --- a/src/docs/swagger.config.ts +++ b/src/docs/swagger.config.ts @@ -18,11 +18,9 @@ export const swaggerConfig = new DocumentBuilder() 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') - .setVersion('1.0') + .addTag('Administradores') .addTag('Cuestionarios') .addTag('Secciones') .addTag('Preguntas') diff --git a/src/emails/registro-evento.ts b/src/emails/registro-evento.ts new file mode 100644 index 0000000..1c23818 --- /dev/null +++ b/src/emails/registro-evento.ts @@ -0,0 +1,44 @@ +export function generarHtmlCorreoAsistencia({ + nombreForm, + nombreEvento, + correo, + fechaRegistro, +}: { + nombreForm: string; + nombreEvento: string; + correo: string; + fechaRegistro: string; +}) { + return ` +
+

🎓 ¡Gracias por registrarte!

+ +

+ Has completado exitosamente el formulario: ${nombreForm} +

+ +

+ Presenta este código QR (en tu celular o impreso) en cualquier stand para validar tu asistencia y recibir tu constancia. +

+ +
+ Código QR +
+ +

📋 Tus datos de registro:

+
    +
  • Evento: ${nombreEvento || 'Evento'}
  • +
  • Correo: ${correo}
  • +
  • Fecha de registro: ${fechaRegistro}
  • +
+ +

+ Si tienes alguna duda, acude al módulo de información durante el evento. +

+ +

+ Este mensaje fue enviado automáticamente. Por favor, no respondas a este correo. +

+
+ `; +} diff --git a/src/evento/docs/evento.documentation.ts b/src/evento/docs/evento.documentation.ts new file mode 100644 index 0000000..f1a9b1b --- /dev/null +++ b/src/evento/docs/evento.documentation.ts @@ -0,0 +1,68 @@ +import { applyDecorators } from '@nestjs/common'; +import { + ApiTags, + ApiOperation, + ApiBody, + ApiParam, + ApiConsumes, +} from '@nestjs/swagger'; +import { CreateEventoDto } from '../dto/create-evento.dto'; +import { UpdateEventoDto } from '../dto/update.evento.dto'; +import { + actualizacionEventoNombreYFechas, + actualizacionEventoTipo, + ejemploEventoAcademico, + ejemploEventoCultural, + ejemploFeriaSexualidad, +} from './evento.examples'; + +export class EventoApiDocumentation { + static ApiController = ApiTags('Evento'); + + static ApiCreate = applyDecorators( + ApiOperation({ summary: 'Crear un nuevo evento' }), + ApiBody({ + type: CreateEventoDto, + examples: { + ejemplo1: ejemploFeriaSexualidad, + ejemplo2: ejemploEventoAcademico, + ejemplo3: ejemploEventoCultural, + }, + }), + ); + + static ApiUpdate = applyDecorators( + ApiOperation({ summary: 'Actualizar un evento existente por ID' }), + ApiBody({ + type: UpdateEventoDto, + examples: { + actualizacion1: actualizacionEventoNombreYFechas, + actualizacion2: actualizacionEventoTipo, + }, + }), + ); + + static ApiPostBanner = applyDecorators( + ApiOperation({ summary: 'Subir banner para un evento' }), + ApiParam({ + name: 'id', + type: Number, + description: 'ID del evento al que se asigna el banner', + }), + ApiConsumes('multipart/form-data'), + ApiBody({ + schema: { + type: 'object', + properties: { + banner: { + type: 'string', + format: 'binary', + description: + 'Archivo de imagen (jpg, png, webp) que será el banner del evento', + }, + }, + required: ['banner'], + }, + }), + ); +} diff --git a/src/evento/docs/evento.examples.ts b/src/evento/docs/evento.examples.ts new file mode 100644 index 0000000..a94cb2b --- /dev/null +++ b/src/evento/docs/evento.examples.ts @@ -0,0 +1,57 @@ +/* CREATE */ + +export const ejemploFeriaSexualidad = { + summary: 'Evento que simula una feria', + description: + 'Un evento de una feria de la sexualidad, con registro para la comunidad universitaria', + value: { + tipo_evento: 'Estudiantil', + nombre_evento: 'Feria de la Sexualidad 2026-3', + descripcion_evento: + '**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.', + fecha_inicio: '2025-07-10T09:00:00.000Z', + fecha_fin: '2025-07-11T17:00:00.000Z', + }, +}; + +export const ejemploEventoAcademico = { + summary: 'Evento académico', + description: 'Un evento tipo seminario académico de dos días', + value: { + tipo_evento: 'Académico', + nombre_evento: 'Seminario de Innovación Educativa', + fecha_inicio: '2025-07-10T09:00:00.000Z', + fecha_fin: '2025-07-11T17:00:00.000Z', + }, +}; + +export const ejemploEventoCultural = { + summary: 'Evento cultural', + description: 'Un evento cultural de un solo día', + value: { + tipo_evento: 'Cultural', + nombre_evento: 'Festival de Música y Arte', + fecha_inicio: '2025-08-05T15:00:00.000Z', + fecha_fin: '2025-08-05T22:00:00.000Z', + }, +}; + +/* UPDATE */ + +export const actualizacionEventoNombreYFechas = { + summary: 'Actualizar nombre y fechas', + description: 'Se cambia el nombre del evento y se ajustan las fechas', + value: { + nombre_evento: 'Seminario de Tecnología Educativa', + fecha_inicio: '2025-07-15T10:00:00.000Z', + fecha_fin: '2025-07-16T18:00:00.000Z', + }, +}; + +export const actualizacionEventoTipo = { + summary: 'Solo cambiar tipo de evento', + description: 'Se actualiza el tipo sin modificar lo demás', + value: { + tipo_evento: 'Deportivo', + }, +}; diff --git a/src/evento/dto/create-evento.dto.ts b/src/evento/dto/create-evento.dto.ts index 5fa9fd6..b5e6986 100644 --- a/src/evento/dto/create-evento.dto.ts +++ b/src/evento/dto/create-evento.dto.ts @@ -1,11 +1,26 @@ +import { IsString, IsNotEmpty, IsDate, IsOptional } from 'class-validator'; +import { Type } from 'class-transformer'; + export class CreateEventoDto { - tipo_evento: string; + @IsString() + @IsNotEmpty({ message: 'El tipo de evento es obligatorio.' }) + tipo_evento: string; - nombre_evento: string; + @IsString() + @IsNotEmpty({ message: 'El nombre del evento es obligatorio.' }) + nombre_evento: string; - fecha_inicio: Date; + @IsOptional() + @IsNotEmpty({ message: 'La descripción del evento es obligatoria.' }) + descripcion_evento: string; - fecha_fin: Date; + @Type(() => Date) + @IsDate({ message: 'La fecha de inicio debe ser una fecha válida.' }) + @IsNotEmpty({ message: 'La fecha de inicio es obligatoria.' }) + fecha_inicio: Date; - //agregar el id del administrador -} \ No newline at end of file + @Type(() => Date) + @IsDate({ message: 'La fecha de fin debe ser una fecha válida.' }) + @IsNotEmpty({ message: 'La fecha de fin es obligatoria.' }) + fecha_fin: Date; +} diff --git a/src/evento/dto/update.evento.dto.ts b/src/evento/dto/update.evento.dto.ts index f85579b..8f89f69 100644 --- a/src/evento/dto/update.evento.dto.ts +++ b/src/evento/dto/update.evento.dto.ts @@ -1,6 +1,23 @@ +import { IsOptional, IsString, IsNotEmpty, IsDate } from 'class-validator'; +import { Type } from 'class-transformer'; + export class UpdateEventoDto { - tipo_evento?: string - nombre_evento: string - fecha_inicio?: Date - fecha_fin?: Date -} \ No newline at end of file + @IsOptional() + @IsString() + @IsNotEmpty({ message: 'El tipo de evento no puede estar vacío si se proporciona' }) + tipo_evento?: string; + + @IsString() + @IsNotEmpty({ message: 'El nombre del evento es obligatorio' }) + nombre_evento: string; + + @IsOptional() + @Type(() => Date) + @IsDate({ message: 'La fecha de inicio debe ser una fecha válida' }) + fecha_inicio?: Date; + + @IsOptional() + @Type(() => Date) + @IsDate({ message: 'La fecha de fin debe ser una fecha válida' }) + fecha_fin?: Date; +} diff --git a/src/evento/entities/evento.entity.ts b/src/evento/entities/evento.entity.ts new file mode 100644 index 0000000..a86530e --- /dev/null +++ b/src/evento/entities/evento.entity.ts @@ -0,0 +1,37 @@ +import { Cuestionario } from 'src/cuestionario/entities/cuestionario.entity'; +import { + Column, + Entity, + OneToMany, + PrimaryGeneratedColumn, +} from 'typeorm'; + +@Entity() +export class Evento { + @PrimaryGeneratedColumn() + id_evento: number; + + @Column({ length: 50 }) + tipo_evento: string; + + @Column({ length: 200 }) + nombre_evento: string; + + @Column({ length: 500, nullable: true }) + descripcion_evento: string; + + @Column({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP' }) + fecha_inicio: Date; + + @Column({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP' }) + fecha_fin: Date; + + @Column({ nullable: true }) + asistencias: number; + + @Column({ nullable: true }) + banner: string; + + @OneToMany(() => Cuestionario, (cuestionario) => cuestionario.evento) + cuestionarios: Cuestionario[]; +} diff --git a/src/evento/evento.controller.ts b/src/evento/evento.controller.ts index 0586840..3592062 100644 --- a/src/evento/evento.controller.ts +++ b/src/evento/evento.controller.ts @@ -1,37 +1,101 @@ -import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post } from '@nestjs/common'; +import { + Body, + Controller, + Delete, + Get, + Param, + ParseIntPipe, + Patch, + Post, + UploadedFile, + UseInterceptors, +} from '@nestjs/common'; import { EventoService } from './evento.service'; -import { Evento } from './evento.entity'; +import { Evento } from './entities/evento.entity'; import { CreateEventoDto } from './dto/create-evento.dto'; import { UpdateEventoDto } from './dto/update.evento.dto'; +import { FileInterceptor } from '@nestjs/platform-express'; +import { diskStorage } from 'multer'; +import { extname } from 'path'; +import { EventoApiDocumentation } from './docs/evento.documentation'; @Controller('evento') +@EventoApiDocumentation.ApiController export class EventoController { - constructor(private eventoService: EventoService) {} + constructor(private eventoService: EventoService) {} - @Get() - getEventos(): Promise { - return this.eventoService.getEventos() + @Post() + @EventoApiDocumentation.ApiCreate + createEvento(@Body() newEvento: CreateEventoDto) { + return this.eventoService.createEvento(newEvento); + } + + @Get() + getEventos(): Promise { + return this.eventoService.getEventos(); + } + + @Get(':id/cuestionarios') + getCuestionarios(@Param('id', ParseIntPipe) id: number) { + return this.eventoService.getCuestionariosEvento(id); + } + + @Get('activos') + getEventosActivos(): Promise { + return this.eventoService.getEventosActivos(); + } + + @Post(':id/banner') + @UseInterceptors( + FileInterceptor('banner', { + storage: diskStorage({ + destination: './public/banners', // Asegúrate de que esta carpeta exista + filename: (req, file, cb) => { + const uniqueSuffix = + Date.now() + '-' + Math.round(Math.random() * 1e9); + const ext = extname(file.originalname); + cb(null, `banner-${uniqueSuffix}${ext}`); + }, + }), + fileFilter: (req, file, cb) => { + const allowedTypes = /jpeg|jpg|png|webp/; + const ext = extname(file.originalname).toLowerCase(); + if (allowedTypes.test(ext)) { + cb(null, true); + } else { + cb(new Error('Tipo de archivo no permitido'), false); + } + }, + }), + ) + @EventoApiDocumentation.ApiPostBanner + async subirBannerEvento( + @Param('id', ParseIntPipe) id: number, + @UploadedFile() file: Express.Multer.File, + ) { + if (!file) { + throw new Error('No se ha recibido un archivo válido'); } - @Get(':id') - getEvento(@Param('id', ParseIntPipe) id: number) { - return this.eventoService.getEvento(id) - } + return this.eventoService.asociarBanner(id, file.filename); + } - @Post() - createEvento(@Body() newEvento: CreateEventoDto) { - return this.eventoService.createEvento(newEvento) - } + @Get(':id') + getEvento(@Param('id', ParseIntPipe) id: number) { + return this.eventoService.getEvento(id); + } - @Delete(':id') - deleteEvento(@Param('id', ParseIntPipe) id: number) { - return this.eventoService.deleteEvento(id) - } - //@Param(':id', ParseIntPipe) id: number, @Body() tipoUser: UpdateTipoUserDto - @Patch() - updateEvento(@Param(':id', ParseIntPipe) id: number, @Body() evento: UpdateEventoDto) { - return this.eventoService.updateEvento(id, evento) - } + @Delete(':id') + deleteEvento(@Param('id', ParseIntPipe) id: number) { + return this.eventoService.deleteEvento(id); + } - + @Patch(':id') + @EventoApiDocumentation.ApiUpdate + updateEvento( + @Param('id', ParseIntPipe) id: number, + @Body() evento: UpdateEventoDto, + ) { + return this.eventoService.updateEvento(id, evento); + } } diff --git a/src/evento/evento.entity.ts b/src/evento/evento.entity.ts deleted file mode 100644 index 046a042..0000000 --- a/src/evento/evento.entity.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Participante } from "src/participante/participante.entity"; -import { ParticipanteEvento } from "src/participante_evento/participante_evento.entity"; -import { Column, Entity, ManyToMany, OneToMany, PrimaryGeneratedColumn } from "typeorm"; - -@Entity() -export class Evento { - @PrimaryGeneratedColumn() - id_evento: number - - @Column() - tipo_evento: string - - @Column() - nombre_evento: string - - @Column({type: 'datetime', default: () => 'CURRENT_TIMESTAMP' }) - fecha_inicio: Date - - @Column({type: 'datetime', default: () => 'CURRENT_TIMESTAMP' }) - fecha_fin: Date - - /* falta hacer la relacion - @Column() - id_administrador: number - - @ManyToOne(() => Administrador, (admin) => admin.eventos) - @JoinColumn({ name: "id_administrador" }) - administrador: Administrador; - - - - @OneToMany(() => Asistencia, (asistencia) => asistencia.evento) - asistencias: Asistencia[]; - */ - - - - @OneToMany(() => ParticipanteEvento, participanteEvento => participanteEvento.evento) - participanteEventos: ParticipanteEvento[]; - -} \ No newline at end of file diff --git a/src/evento/evento.module.ts b/src/evento/evento.module.ts index 58e904d..9335a40 100644 --- a/src/evento/evento.module.ts +++ b/src/evento/evento.module.ts @@ -2,7 +2,7 @@ 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 { Evento } from './entities/evento.entity'; @Module({ imports: [TypeOrmModule.forFeature([Evento])], diff --git a/src/evento/evento.service.ts b/src/evento/evento.service.ts index eaf5d9f..47d942f 100644 --- a/src/evento/evento.service.ts +++ b/src/evento/evento.service.ts @@ -1,73 +1,116 @@ -import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { + HttpException, + HttpStatus, + Injectable, + NotFoundException, +} from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; -import { Evento } from './evento.entity'; +import { MoreThan, Repository } from 'typeorm'; +import { Evento } from './entities/evento.entity'; import { CreateEventoDto } from './dto/create-evento.dto'; import { UpdateEventoDto } from './dto/update.evento.dto'; @Injectable() export class EventoService { - constructor( - @InjectRepository(Evento) private eventoRepository: Repository, - ) {} + constructor( + @InjectRepository(Evento) private eventoRepository: Repository, + ) {} - async createEvento(evento: CreateEventoDto) { - const eventoFound = await this.eventoRepository.findOne({ - where: { - nombre_evento: evento.nombre_evento, - tipo_evento: evento.tipo_evento - } - }) + async createEvento(evento: CreateEventoDto) { + const eventoFound = await this.eventoRepository.findOne({ + where: { + nombre_evento: evento.nombre_evento, + tipo_evento: evento.tipo_evento, + }, + }); - if (eventoFound) - return new HttpException('Evento already exists', HttpStatus.CONFLICT) - - const createEvento = this.eventoRepository.create(evento) + if (eventoFound) + throw new HttpException( + 'Ya existe un evento con el mismo nombre y tipo, puede agregar el periodo al nombre del evento para diferenciarlos', + HttpStatus.BAD_REQUEST, + ); - return this.eventoRepository.save(createEvento) + const createEvento = this.eventoRepository.create(evento); + + return this.eventoRepository.save(createEvento); + } + + // evento.service.ts + + async asociarBanner(id: number, filename: string) { + const evento = await this.getEventoOrFail(id); + + evento.banner = filename; + return this.eventoRepository.save(evento); + } + + getEventos() { + return this.eventoRepository.find(); + } + + async getCuestionariosEvento(id_evento: number) { + const evento = await this.eventoRepository.findOne({ + where: { id_evento }, + relations: ['cuestionarios'], // solo los cuestionarios, sin secciones + }); + + if (!evento) { + throw new NotFoundException(`Evento con ID ${id_evento} no encontrado.`); } - getEventos() { - return this.eventoRepository.find({ - relations: ['participantes'] - }) + return evento; + } + + async getEvento(id_evento: number) { + return await this.eventoRepository.findOne({ + where: { + id_evento, + }, + }); + } + + async getEventoOrFail(id_evento: number): Promise { + const eventoFound = await this.eventoRepository.findOne({ + where: { + id_evento, + }, + }); + + if (!eventoFound) { + throw new HttpException( + 'El evento buscado no existe', + HttpStatus.NOT_FOUND, + ); } - async getEvento(id_evento: number) { - const eventoFound = await this.eventoRepository.findOne({ - where: { - id_evento - }, - relations: ['participantes'] - }) + return eventoFound; + } - if (!eventoFound) - return new HttpException('Evento not found', HttpStatus.NOT_FOUND); + async deleteEvento(id_evento: number) { + await this.getEventoOrFail(id_evento); - return eventoFound + const result = await this.eventoRepository.delete(id_evento); + + if (result.affected === 0) { + throw new HttpException('Evento not found', HttpStatus.NOT_FOUND); } - async deleteEvento(id_evento: number) { - const result = await this.eventoRepository.delete({ id_evento }) + return { message: 'Evento eliminado exitosamente' }; + } - if (result.affected === 0) { - return new HttpException('Evento not found', HttpStatus.NOT_FOUND); - } + async updateEvento(id_evento: number, evento: UpdateEventoDto) { + const eventoFound = await this.getEventoOrFail(id_evento); - return result - } + const updateEvento = Object.assign(eventoFound, evento); + return this.eventoRepository.save(updateEvento); + } - async updateEvento(id_evento: number, evento: UpdateEventoDto) { - const eventoFound = await this.eventoRepository.findOne({ - where: { - id_evento - } - }) - - if (!eventoFound) - return new HttpException('Evento not found', HttpStatus.NOT_FOUND) - - const updateEvento = Object.assign(eventoFound, evento) - return this.eventoRepository.save(updateEvento) - } + async getEventosActivos() { + const now = new Date(); + return this.eventoRepository.find({ + where: { + fecha_fin: MoreThan(now), + }, + }); + } } diff --git a/src/main.ts b/src/main.ts index bdbcf72..1e616e4 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,23 +1,22 @@ 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 { ConfigService } from '@nestjs/config'; +import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; async function bootstrap() { const app = await NestFactory.create(AppModule); - + const configService = app.get(ConfigService); + // 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: '*', @@ -25,34 +24,30 @@ async function bootstrap() { allowedHeaders: ['Content-Type', 'Authorization'], }); - // Swagger setup - DocsModule.setupSwagger(app); + const config = new DocumentBuilder() + .setTitle('Formularios API') + .setDescription( + `Esta es la API del sistema de registros para eventos de la FES Acatlán. Permite gestionar eventos académicos y sus formularios asociados. - // 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'); - } +Flujo de creación: + +1. Primero, se debe crear un evento usando **POST /evento**. +2. Luego, se crea un formulario y se relaciona con el evento usando **POST /cuestionario**. +3. (Opcional) También se puede crear un formulario y un evento al mismo tiempo usando **POST /cuestionario/evento**, lo cual genera automáticamente ambos y los relaciona.`, + ) + .setVersion('1.0') + .addBearerAuth() + .addTag('Evento') + .addTag('Cuestionario') + .addTag('Cuestionario Respondido') + .build(); + + const document = SwaggerModule.createDocument(app, config); + SwaggerModule.setup('api-docs', app, document); // Start the server - await app.listen(process.env.PORT ?? 4200); - console.log('API en ejecución en http://localhost:4200'); + await app.listen(configService.get('API_PORT') || 4200); + console.log('\nAPI en ejecución en http://localhost:4200'); console.log('Swagger disponible en http://localhost:4200/api-docs'); } bootstrap(); diff --git a/src/opcion/entities/opcion.entity.ts b/src/opcion/entities/opcion.entity.ts index 3523e76..bfe2c0e 100644 --- a/src/opcion/entities/opcion.entity.ts +++ b/src/opcion/entities/opcion.entity.ts @@ -1,5 +1,5 @@ -import { PreguntaOpcion } from "src/pregunta_opcion/entities/pregunta_opcion.entity"; -import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm"; +import { PreguntaOpcion } from 'src/pregunta_opcion/entities/pregunta_opcion.entity'; +import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; @Entity() export class Opcion { @@ -11,6 +11,4 @@ export class Opcion { @OneToMany(() => PreguntaOpcion, (preguntaOpcion) => preguntaOpcion.opcion) preguntasOpciones: PreguntaOpcion[]; - - -} \ No newline at end of file +} diff --git a/src/participante/dto/create-participante.dto.ts b/src/participante/dto/create-participante.dto.ts index 871599b..328bf95 100644 --- a/src/participante/dto/create-participante.dto.ts +++ b/src/participante/dto/create-participante.dto.ts @@ -10,13 +10,4 @@ export class CreateParticipanteDto { @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; } \ No newline at end of file diff --git a/src/participante/entities/participante.entity.ts b/src/participante/entities/participante.entity.ts new file mode 100644 index 0000000..f45c999 --- /dev/null +++ b/src/participante/entities/participante.entity.ts @@ -0,0 +1,31 @@ +import { CuestionarioRespondido } from 'src/cuestionario_respondido/entities/cuestionario_respondido.entity'; +import { ParticipanteEvento } from 'src/participante_evento/entities/participante_evento.entity'; +import { + Column, + Entity, + OneToMany, + PrimaryGeneratedColumn, +} from 'typeorm'; + +@Entity() +export class Participante { + @PrimaryGeneratedColumn() + id_participante: number; + + @Column({ unique: true }) + correo: string; + + // Cuestionarios que ha respondido + @OneToMany( + () => CuestionarioRespondido, + (cuestionarioRespondido) => cuestionarioRespondido.participante, + ) + cuestionariosRespondidos: CuestionarioRespondido[]; + + // Cuestionarios a los que se ha inscrito (subeventos) + @OneToMany( + () => ParticipanteEvento, + (participanteEvento) => participanteEvento.participante, + ) + participanteEventos: ParticipanteEvento[]; +} diff --git a/src/participante/participante.controller.ts b/src/participante/participante.controller.ts index e25b387..8abd9a6 100644 --- a/src/participante/participante.controller.ts +++ b/src/participante/participante.controller.ts @@ -1,5 +1,5 @@ import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post } from '@nestjs/common'; -import { Participante } from './participante.entity'; +import { Participante } from './entities/participante.entity'; import { ParticipanteService } from './participante.service'; import { CreateParticipanteDto } from './dto/create-participante.dto'; import { UpdateParticipanteDto } from './dto/update.participante.dto'; diff --git a/src/participante/participante.entity.ts b/src/participante/participante.entity.ts deleted file mode 100644 index 3579682..0000000 --- a/src/participante/participante.entity.ts +++ /dev/null @@ -1,49 +0,0 @@ -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"; - -@Entity() -export class Participante { - @PrimaryGeneratedColumn() - id_participante: number - - @Column() - correo: string - - @Column() - id_tipo_user: number - - //Relacion con tipo usuario - @ManyToOne(() => TipoUser, tipoUser => tipoUser.participante) - @JoinColumn({ name: 'participante_id' }) //nombre de la relacion - 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(() => Asistencia, (asistencia) => asistencia.evento) - asistencias: Asistencia[]; - */ - - - - @OneToMany(() => CuestionarioRespondido, cuestionario => cuestionario.participante) - cuestionariosRespondidos: CuestionarioRespondido[]; - - @OneToMany(() => ParticipanteEvento, participanteEvento => participanteEvento.participante) - participanteEventos: ParticipanteEvento[]; - - -} \ No newline at end of file diff --git a/src/participante/participante.module.ts b/src/participante/participante.module.ts index 41a0dd0..945f59e 100644 --- a/src/participante/participante.module.ts +++ b/src/participante/participante.module.ts @@ -2,7 +2,7 @@ import { Module } from '@nestjs/common'; import { ParticipanteService } from './participante.service'; import { ParticipanteController } from './participante.controller'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { Participante } from './participante.entity'; +import { Participante } from './entities/participante.entity'; @Module({ imports: [TypeOrmModule.forFeature([Participante])], diff --git a/src/participante/participante.service.ts b/src/participante/participante.service.ts index c500033..52c0df6 100644 --- a/src/participante/participante.service.ts +++ b/src/participante/participante.service.ts @@ -1,124 +1,180 @@ -import { BadRequestException, ConflictException, Injectable, NotFoundException } from '@nestjs/common'; +import { + BadRequestException, + ConflictException, + Injectable, + NotFoundException, +} from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Participante } from './participante.entity'; +import { Participante } from './entities/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 - ) {} + constructor( + @InjectRepository(Participante) + private participanteRepository: Repository, + ) {} - /** - * 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 { - // Verificar si ya existe un participante con el mismo correo - const participanteFound = await this.participanteRepository.findOne({ - where: { - correo: participante.correo - } - }); + /** + * 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 { + // Verificar si ya existe un participante con el mismo correo + const participanteFound = await this.participanteRepository.findOne({ + where: { + correo: participante.correo, + }, + }); - if (participanteFound) { - throw new ConflictException(`Ya existe un participante con el correo ${participante.correo}`); - } - - const nuevoParticipante = this.participanteRepository.create(participante); - return this.participanteRepository.save(nuevoParticipante); + if (participanteFound) { + throw new ConflictException( + `Ya existe un participante con el correo ${participante.correo}`, + ); } - /** - * Obtiene todos los participantes - * @returns Lista de participantes con sus relaciones - */ - async getParticipantes(): Promise { - return this.participanteRepository.find({ - relations: ['tipo_user', 'participanteEventos'] - }); + const nuevoParticipante = this.participanteRepository.create(participante); + return this.participanteRepository.save(nuevoParticipante); + } + + /** + * Obtiene todos los participantes + * @returns Lista de participantes con sus relaciones + */ + async getParticipantes(): Promise { + return this.participanteRepository.find({ + relations: ['tipo_user', 'participanteEventos'], + }); + } + + /** + * 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 { + const participanteFound = await this.participanteRepository.findOne({ + where: { + id_participante, + }, + relations: ['tipo_user', 'participanteEventos'], + }); + + if (!participanteFound) { + throw new NotFoundException( + `Participante con ID ${id_participante} no encontrado`, + ); } - /** - * 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 { - const participanteFound = await this.participanteRepository.findOne({ - where: { - id_participante - }, - relations: ['tipo_user', 'participanteEventos'] - }); + return participanteFound; + } - if (!participanteFound) { - throw new NotFoundException(`Participante con ID ${id_participante} no encontrado`); - } - - return participanteFound; + async getParticipanteByCorreo(correo: string): Promise { + const participanteFound = await this.participanteRepository.findOne({ + where: { + correo, + }, + relations: ['tipo_user', 'participanteEventos'], + }); + return participanteFound; + } + + async getParticipanteByCorreoOrFail(correo: string): Promise { + const participanteFound = await this.participanteRepository.findOne({ + where: { + correo, + }, + relations: ['tipo_user', 'participanteEventos'], + }); + + if (!participanteFound) { + throw new NotFoundException( + `Participante con correo ${correo} no encontrado`, + ); } - /** - * 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 }); + return participanteFound; + } - if (result.affected === 0) { - throw new NotFoundException(`Participante con ID ${id_participante} no encontrado`); - } + /** + * 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, + }); - return { - success: true, - message: `Participante con ID ${id_participante} eliminado correctamente`, - affected: result.affected - }; + if (result.affected === 0) { + throw new NotFoundException( + `Participante con ID ${id_participante} no encontrado`, + ); } - /** - * 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 { - // Verificar si el participante existe - const participanteFound = await this.participanteRepository.findOne({ - where: { - id_participante - } - }); + return { + success: true, + message: `Participante con ID ${id_participante} eliminado correctamente`, + affected: result.affected, + }; + } - if (!participanteFound) { - throw new NotFoundException(`Participante con ID ${id_participante} no encontrado`); - } + /** + * 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 { + // Verificar si el participante existe + const participanteFound = await this.participanteRepository.findOne({ + where: { + id_participante, + }, + }); - // 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); + if (!participanteFound) { + throw new NotFoundException( + `Participante con ID ${id_participante} no encontrado`, + ); } + + // 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); + } } diff --git a/src/participante_evento/entities/participante_evento.entity.ts b/src/participante_evento/entities/participante_evento.entity.ts new file mode 100644 index 0000000..d0b5a57 --- /dev/null +++ b/src/participante_evento/entities/participante_evento.entity.ts @@ -0,0 +1,40 @@ +import { Cuestionario } from 'src/cuestionario/entities/cuestionario.entity'; +import { Evento } from 'src/evento/entities/evento.entity'; +import { Participante } from 'src/participante/entities/participante.entity'; +import { + Column, + Entity, + JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, +} from 'typeorm'; + +@Entity() +export class ParticipanteEvento { + @PrimaryGeneratedColumn() + id_participante_evento: number; + + @Column() + id_participante: number; + + @Column() + id_cuestionario: number; + + @Column({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP' }) + fecha_registro: Date; + + @Column({ type: 'datetime', nullable: true }) + fecha_asistencia: Date; + + @Column({ type: 'boolean', default: false }) + asistio: boolean; + + // Relaciones + @ManyToOne(() => Participante, (p) => p.participanteEventos) + @JoinColumn({ name: 'id_participante' }) + participante: Participante; + + @ManyToOne(() => Cuestionario, (c) => c.inscripciones) + @JoinColumn({ name: 'id_cuestionario' }) + cuestionario: Cuestionario; +} diff --git a/src/participante_evento/participante_evento.controller.ts b/src/participante_evento/participante_evento.controller.ts index 5b78ac4..5e6b26c 100644 --- a/src/participante_evento/participante_evento.controller.ts +++ b/src/participante_evento/participante_evento.controller.ts @@ -1,6 +1,15 @@ -import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post } from '@nestjs/common'; +import { + Body, + Controller, + Delete, + Get, + Param, + ParseIntPipe, + Patch, + Post, +} from '@nestjs/common'; import { ParticipanteEventoService } from './participante_evento.service'; -import { ParticipanteEvento } from './participante_evento.entity'; +import { ParticipanteEvento } from './entities/participante_evento.entity'; import { CreateParticipanteEventoDto } from './dto/create-participante_evento.dto'; import { UpdateParticipanteEventoDto } from './dto/update.participante_evento.dto'; import { ApiTags, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; @@ -8,179 +17,238 @@ import { ApiTags, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; @ApiTags('Participantes-Eventos') @Controller('participante-evento') export class ParticipanteEventoController { + constructor(private participanteEventoService: ParticipanteEventoService) {} - constructor(private participanteEventoService: ParticipanteEventoService) {} + @ApiOperation({ summary: 'Obtener participantes por evento' }) + @ApiParam({ name: 'id_cuestionario', 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/:id_cuestionario') + getParticipantesPorEvento( + @Param('id_cuestionario', ParseIntPipe) id_cuestionario: number, + ) { + return this.participanteEventoService.getParticipantesPorCuestionario( + id_cuestionario, + ); + } - @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 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 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 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 { - return this.participanteEventoService.getParticipantesEvento() - } + @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 { + 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' }) - @Post() - createParticipanteEvento(@Body() newParticipanteEvento: CreateParticipanteEventoDto) { - return this.participanteEventoService.createParticipanteEvento(newParticipanteEvento) - } + @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', + }) + @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: '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: '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) - } + @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, + ); + } } diff --git a/src/participante_evento/participante_evento.entity.ts b/src/participante_evento/participante_evento.entity.ts deleted file mode 100644 index 4f537d3..0000000 --- a/src/participante_evento/participante_evento.entity.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Evento } from "src/evento/evento.entity"; -import { Participante } from "src/participante/participante.entity"; -import { Column, Entity, JoinColumn, ManyToOne, PrimaryColumn, PrimaryGeneratedColumn } from "typeorm"; - -@Entity() -export class ParticipanteEvento { - @PrimaryColumn() - id_participante: number - @PrimaryColumn() - id_evento: number - - @Column() - fecha_inscripcion: Date - - @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(() => 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) - @JoinColumn({ name: "id_evento" }) - evento: Evento; - -} \ No newline at end of file diff --git a/src/participante_evento/participante_evento.module.ts b/src/participante_evento/participante_evento.module.ts index aa7ba57..3a957f4 100644 --- a/src/participante_evento/participante_evento.module.ts +++ b/src/participante_evento/participante_evento.module.ts @@ -2,13 +2,17 @@ import { Module } from '@nestjs/common'; import { ParticipanteEventoService } from './participante_evento.service'; 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'; +import { ParticipanteEvento } from './entities/participante_evento.entity'; +import { Evento } from 'src/evento/entities/evento.entity'; +import { Participante } from 'src/participante/entities/participante.entity'; +import { CuestionarioModule } from 'src/cuestionario/cuestionario.module'; @Module({ - imports: [TypeOrmModule.forFeature([ParticipanteEvento, Evento, Participante])], - controllers: [ParticipanteEventoController], + imports: [ + TypeOrmModule.forFeature([ParticipanteEvento, Evento, Participante]), + CuestionarioModule, + ], + controllers: [ParticipanteEventoController], providers: [ParticipanteEventoService], exports: [ParticipanteEventoService], }) diff --git a/src/participante_evento/participante_evento.service.ts b/src/participante_evento/participante_evento.service.ts index 77f9f5d..f7808c5 100644 --- a/src/participante_evento/participante_evento.service.ts +++ b/src/participante_evento/participante_evento.service.ts @@ -1,160 +1,179 @@ import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { ParticipanteEvento } from './participante_evento.entity'; +import { ParticipanteEvento } from './entities/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'; +import { Evento } from 'src/evento/entities/evento.entity'; +import { Participante } from 'src/participante/entities/participante.entity'; +import { CuestionarioService } from 'src/cuestionario/cuestionario.service'; @Injectable() export class ParticipanteEventoService { + constructor( + @InjectRepository(ParticipanteEvento) + private participanteEventoRepository: Repository, + @InjectRepository(Evento) private eventoRepository: Repository, + @InjectRepository(Participante) + private participanteRepository: Repository, + private readonly cuestionarioService: CuestionarioService, + ) {} - constructor( - @InjectRepository(ParticipanteEvento) private participanteEventoRepository: Repository, - @InjectRepository(Evento) private eventoRepository: Repository, - @InjectRepository(Participante) private participanteRepository: Repository - ) {} + async createParticipanteEvento( + participanteEvento: CreateParticipanteEventoDto, + ) { + const participante_eventoFound = + await this.participanteEventoRepository.findOne({ + where: { + id_participante: participanteEvento.id_participante, + id_cuestionario: participanteEvento.id_evento, + }, + }); - async createParticipanteEvento(participanteEvento: CreateParticipanteEventoDto) { - - const participante_eventoFound = await this.participanteEventoRepository.findOne({ - where: { - id_participante: participanteEvento.id_participante, - id_evento: participanteEvento.id_evento - } - }) - - if (participante_eventoFound) { - return new HttpException('Participante already exists in this event', HttpStatus.CONFLICT) - } - - return this.participanteEventoRepository.save(participanteEvento) + if (participante_eventoFound) { + return new HttpException( + 'Participante already exists in this event', + HttpStatus.CONFLICT, + ); } - getParticipantesEvento() { - return this.participanteEventoRepository.find({ - relations: ['evento', 'participante'] - }) + return this.participanteEventoRepository.save(participanteEvento); + } + + getParticipantesEvento() { + return this.participanteEventoRepository.find({ + relations: ['evento', 'participante'], + }); + } + + async getParticipanteEvento(id_participante: number) { + const participante_eventoFound = + await this.participanteEventoRepository.findOne({ + where: { + id_participante, + }, + relations: ['evento', 'participante'], + }); + + if (!participante_eventoFound) { + return new HttpException('Participante not found', HttpStatus.NOT_FOUND); } - async getParticipanteEvento(id_participante: number) { - const participante_eventoFound = await this.participanteEventoRepository.findOne({ - where: { - id_participante - }, - relations: ['evento', 'participante'] - }) + return participante_eventoFound; + } - if (!participante_eventoFound) { - return new HttpException('Participante not found', HttpStatus.NOT_FOUND); - } + async getParticipanteEventoEspecifico( + id_participante: number, + id_evento: number, + ) { + const participante_eventoFound = + await this.participanteEventoRepository.findOne({ + where: { + id_participante, + id_cuestionario: id_evento, + }, + relations: ['evento', 'participante'], + }); - return participante_eventoFound + if (!participante_eventoFound) { + return new HttpException('Registro no encontrado', HttpStatus.NOT_FOUND); } - async getParticipanteEventoEspecifico(id_participante: number, id_evento: number) { - const participante_eventoFound = await this.participanteEventoRepository.findOne({ - where: { - id_participante, - id_evento - }, - relations: ['evento', 'participante'] - }) + return participante_eventoFound; + } - if (!participante_eventoFound) { - return new HttpException('Registro no encontrado', HttpStatus.NOT_FOUND); - } + /** + * 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 getParticipantesPorCuestionario(id_cuestionario: number) { + const cuestionario = + await this.cuestionarioService.getCuestionarioOrFail(id_cuestionario); + console.log(cuestionario); - return participante_eventoFound + const participantesEvento = await this.participanteEventoRepository.find({ + where: { id_cuestionario }, + 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, + ); } - /** - * 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 } - }); + // Obtener todos los registros participante-evento para este participante + const eventosParticipante = await this.participanteEventoRepository.find({ + where: { id_participante }, + relations: ['evento'], + }); - if (!eventoExists) { - throw new HttpException(`Evento con ID ${id_evento} no encontrado`, HttpStatus.NOT_FOUND); - } + return eventosParticipante; + } - // Obtener todos los registros participante-evento para este evento - const participantesEvento = await this.participanteEventoRepository.find({ - where: { id_evento }, - relations: ['participante'] - }); + async deleteParticipanteEvento(id_participante: number) { + const result = await this.participanteEventoRepository.delete({ + id_participante, + }); - return participantesEvento; + if (result.affected === 0) { + return new HttpException('Participante not found', HttpStatus.NOT_FOUND); } - /** - * 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 } - }); + return result; + } - if (!participanteExists) { - throw new HttpException(`Participante con ID ${id_participante} no encontrado`, HttpStatus.NOT_FOUND); - } + async updateParticipanteEvento( + id_participante: number, + id_evento: number, + participante_evento: UpdateParticipanteEventoDto, + ) { + const participante_eventoFound = + await this.participanteEventoRepository.findOne({ + where: { + id_participante, + id_cuestionario: id_evento, + }, + }); - // Obtener todos los registros participante-evento para este participante - const eventosParticipante = await this.participanteEventoRepository.find({ - where: { id_participante }, - relations: ['evento'] - }); - - return eventosParticipante; + if (!participante_eventoFound) { + return new HttpException('Participante not found', HttpStatus.NOT_FOUND); } - async deleteParticipanteEvento(id_participante: number) { - const result = await this.participanteEventoRepository.delete({ id_participante }) + return this.participanteEventoRepository.save(participante_evento); + } - if (result.affected === 0) { - return new HttpException('Participante not found', HttpStatus.NOT_FOUND); - } + async registrarAsistencia(id_participante: number, id_cuestionario: number) { + const participante_eventoFound = + await this.participanteEventoRepository.findOne({ + where: { + id_participante, + cuestionario: { id_cuestionario }, + }, + }); - return result + if (!participante_eventoFound) { + return new HttpException('Registro no encontrado', HttpStatus.NOT_FOUND); } - async updateParticipanteEvento(id_participante: number, id_evento: number, participante_evento: UpdateParticipanteEventoDto) { - const participante_eventoFound = await this.participanteEventoRepository.findOne({ - where: { - id_participante, - id_evento - } - }) - - if (!participante_eventoFound) { - return new HttpException('Participante not found', HttpStatus.NOT_FOUND) - } - - 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); - } + participante_eventoFound.fecha_asistencia = new Date(); + participante_eventoFound.asistio = true; + return this.participanteEventoRepository.save(participante_eventoFound); + } } diff --git a/src/pregunta/dto/create-pregunta.dto.ts b/src/pregunta/dto/create-pregunta.dto.ts index 6508cb9..5c14fe5 100644 --- a/src/pregunta/dto/create-pregunta.dto.ts +++ b/src/pregunta/dto/create-pregunta.dto.ts @@ -1,12 +1,9 @@ import { Type } from 'class-transformer'; import { IsArray, IsBoolean, IsEnum, IsNotEmpty, IsNumber, IsOptional, IsString, ValidateNested } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; +import { TiposValidacion } from '../entities/pregunta.entity'; +import { TipoPreguntaEnum } from 'src/tipo_pregunta/entities/tipo_pregunta.entity'; -export enum TipoPreguntaEnum { - CERRADA = 'Cerrada', - ABIERTA = 'Abierta', - MULTIPLE = 'Multiple' -} export class OpcionDto { @ApiProperty({ @@ -49,19 +46,18 @@ export class CreatePreguntaDto { description: 'Tipo de pregunta', enum: Object.values(TipoPreguntaEnum), enumName: 'TipoPreguntaEnum', - example: TipoPreguntaEnum.CERRADA, examples: { 'cerrada': { summary: 'Pregunta de opción única', - value: TipoPreguntaEnum.CERRADA + value: TipoPreguntaEnum.Cerrada }, 'abierta': { summary: 'Pregunta de texto libre', - value: TipoPreguntaEnum.ABIERTA + value: TipoPreguntaEnum.AbiertaParrafo }, 'multiple': { summary: 'Pregunta de selección múltiple', - value: TipoPreguntaEnum.MULTIPLE + value: TipoPreguntaEnum.Multiple } } }) @@ -69,7 +65,7 @@ export class CreatePreguntaDto { @IsEnum(TipoPreguntaEnum, { message: 'El tipo debe ser uno de los siguientes valores: Cerrada, Abierta, Multiple' }) - tipo: string; + tipo: TipoPreguntaEnum; @ApiProperty({ description: 'Contador de opciones (se calcula automáticamente)', @@ -79,15 +75,6 @@ export class CreatePreguntaDto { @IsNumber() contador_opcion?: 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, @@ -96,7 +83,7 @@ export class CreatePreguntaDto { }) @IsOptional() @IsString() - validacion?: string; + validacion?: TiposValidacion; @ApiProperty({ description: 'Lista de opciones para preguntas de tipo Cerrada o Multiple', diff --git a/src/pregunta/entities/pregunta.entity.ts b/src/pregunta/entities/pregunta.entity.ts index b936a3f..15c46a0 100644 --- a/src/pregunta/entities/pregunta.entity.ts +++ b/src/pregunta/entities/pregunta.entity.ts @@ -1,8 +1,28 @@ +import { IsEnum, IsOptional } from 'class-validator'; import { PreguntaOpcion } from 'src/pregunta_opcion/entities/pregunta_opcion.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'; +import { + Entity, + PrimaryGeneratedColumn, + Column, + ManyToOne, + OneToMany, + JoinColumn, +} from 'typeorm'; +export enum TiposValidacion { + CORREO = 'correo', + CORREO_INSTITUCIONAL = 'correo_institucional', + TELEFONO = 'telefono', + NOMBRE = 'nombre', + ENTERO = 'entero', + DECIMAL = 'decimal', + COMUNIDAD_ALUMNO = 'comunidad_alumno', + CUENTA_ALUMNO = 'cuenta_alumno', + COMUNIDAD_TRABAJADOR = 'comunidad_trabajador', + CUENTA_TRABAJADOR = 'cuenta_trabajador', +} @Entity() export class Pregunta { @@ -18,22 +38,24 @@ export class Pregunta { @Column({ default: false }) obligatoria: boolean; - @ManyToOne(() => TipoPregunta, tipo => tipo.preguntas) + @ManyToOne(() => TipoPregunta, (tipo) => tipo.preguntas) @JoinColumn({ name: 'id_tipo_pregunta' }) tipoPregunta: TipoPregunta; - + @Column() id_tipo_pregunta: number; - @Column({ type: 'int', nullable: true }) - id_opcion_dependiente?: number; + @Column({ type: 'enum', enum: TiposValidacion, nullable: true }) + @IsOptional() + @IsEnum(TiposValidacion, { message: 'Validación no permitida' }) + validacion?: TiposValidacion; - @Column({ nullable: true }) - validacion?: string; - - @OneToMany(() => SeccionPregunta, seccionPregunta => seccionPregunta.pregunta) + @OneToMany( + () => SeccionPregunta, + (seccionPregunta) => seccionPregunta.pregunta, + ) seccionPreguntas: SeccionPregunta[]; - @OneToMany(() => PreguntaOpcion, preguntaOpcion => preguntaOpcion.pregunta) + @OneToMany(() => PreguntaOpcion, (preguntaOpcion) => preguntaOpcion.pregunta) opciones: PreguntaOpcion[]; -} \ No newline at end of file +} diff --git a/src/pregunta/pregunta.controller.spec.ts b/src/pregunta/pregunta.controller.spec.ts deleted file mode 100644 index f2fc905..0000000 --- a/src/pregunta/pregunta.controller.spec.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { PreguntaController } from './pregunta.controller'; -import { PreguntaService } from './pregunta.service'; - -describe('PreguntaController', () => { - let controller: PreguntaController; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [PreguntaController], - providers: [PreguntaService], - }).compile(); - - controller = module.get(PreguntaController); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); -}); diff --git a/src/pregunta/pregunta.documentation.ts b/src/pregunta/pregunta.documentation.ts index 0641201..d9103e0 100644 --- a/src/pregunta/pregunta.documentation.ts +++ b/src/pregunta/pregunta.documentation.ts @@ -1,6 +1,12 @@ -import { ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { + ApiBody, + ApiOperation, + ApiParam, + ApiResponse, + ApiTags, +} from '@nestjs/swagger'; import { applyDecorators } from '@nestjs/common'; -import { TipoPreguntaEnum } from './dto/create-pregunta.dto'; +import { TipoPreguntaEnum } from 'src/tipo_pregunta/entities/tipo_pregunta.entity'; export class PreguntaApiDocumentation { // Decoradores para toda la clase del controlador @@ -10,7 +16,7 @@ export class PreguntaApiDocumentation { static ApiCreate = applyDecorators( ApiOperation({ summary: 'Crear una nueva pregunta', - description: 'Crea una nueva pregunta para una sección' + description: 'Crea una nueva pregunta para una sección', }), ApiBody({ description: 'Datos de la pregunta a crear', @@ -20,29 +26,30 @@ export class PreguntaApiDocumentation { properties: { titulo: { type: 'string', example: '¿Eres parte de la comunidad?' }, obligatoria: { type: 'boolean', example: true }, - tipo: { - type: 'string', + tipo: { + type: 'string', enum: Object.values(TipoPreguntaEnum), - description: 'Tipo de pregunta: Cerrada (opción única), Abierta (texto libre), Multiple (varias opciones)', - example: TipoPreguntaEnum.CERRADA + 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' + 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' } - } - } - } - } + valor: { type: 'string', example: 'Si' }, + }, + }, + }, + }, }, examples: { cerrada: { @@ -50,41 +57,38 @@ export class PreguntaApiDocumentation { value: { titulo: '¿Eres parte de la comunidad de la FES Acatlán?', obligatoria: true, - tipo: TipoPreguntaEnum.CERRADA, + tipo: TipoPreguntaEnum.Cerrada, validacion: null, - opciones: [ - { valor: 'Sí' }, - { valor: 'No' } - ] - } + 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 - } + tipo: TipoPreguntaEnum.AbiertaParrafo, + 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, + tipo: TipoPreguntaEnum.Multiple, validacion: null, opciones: [ { valor: 'Rapidez' }, { valor: 'Atención al cliente' }, - { valor: 'Facilidad de uso' } - ] - } - } - } + { valor: 'Facilidad de uso' }, + ], + }, + }, + }, }), - ApiResponse({ - status: 201, + ApiResponse({ + status: 201, description: 'Pregunta creada correctamente', schema: { type: 'object', @@ -94,20 +98,19 @@ export class PreguntaApiDocumentation { 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' } - } - } + validacion: { type: 'string', example: 'correo' }, + }, + }, }), ApiResponse({ status: 400, description: 'Datos de la pregunta inválidos' }), - ApiResponse({ status: 500, description: 'Error interno del servidor' }) + 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' + description: 'Retorna una lista de todas las preguntas registradas', }), ApiResponse({ status: 200, @@ -118,31 +121,33 @@ export class PreguntaApiDocumentation { type: 'object', properties: { id_pregunta: { type: 'number', example: 1 }, - pregunta: { type: 'string', example: '¿Eres parte de la comunidad?' }, + 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' } - } - } - } + validacion: { type: 'string', example: 'correo' }, + }, + }, + }, }), - ApiResponse({ status: 500, description: 'Error interno del servidor' }) + 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' + description: 'Retorna una pregunta específica por su ID', }), ApiParam({ name: 'id', description: 'ID de la pregunta', required: true, type: 'number', - example: 1 + example: 1, }), ApiResponse({ status: 200, @@ -155,7 +160,6 @@ export class PreguntaApiDocumentation { 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', @@ -163,29 +167,29 @@ export class PreguntaApiDocumentation { type: 'object', properties: { id_opcion: { type: 'number', example: 1 }, - opcion: { type: 'string', example: 'Si' } - } - } - } - } - } + opcion: { type: 'string', example: 'Si' }, + }, + }, + }, + }, + }, }), ApiResponse({ status: 404, description: 'Pregunta no encontrada' }), - ApiResponse({ status: 500, description: 'Error interno del servidor' }) + 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' + description: 'Actualiza los datos de una pregunta existente', }), ApiParam({ name: 'id', description: 'ID de la pregunta a actualizar', required: true, type: 'number', - example: 1 + example: 1, }), ApiBody({ description: 'Datos a actualizar de la pregunta', @@ -193,9 +197,9 @@ export class PreguntaApiDocumentation { type: 'object', properties: { pregunta: { type: 'string', example: 'Pregunta actualizada' }, - obligatoria: { type: 'boolean', example: false } - } - } + obligatoria: { type: 'boolean', example: false }, + }, + }, }), ApiResponse({ status: 200, @@ -203,27 +207,30 @@ export class PreguntaApiDocumentation { schema: { type: 'object', properties: { - affected: { type: 'number', example: 1 } - } - } + affected: { type: 'number', example: 1 }, + }, + }, + }), + ApiResponse({ + status: 400, + description: 'Datos de actualización inválidos', }), - 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' }) + 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' + description: 'Elimina permanentemente una pregunta por su ID', }), ApiParam({ name: 'id', description: 'ID de la pregunta a eliminar', required: true, type: 'number', - example: 1 + example: 1, }), ApiResponse({ status: 200, @@ -231,11 +238,11 @@ export class PreguntaApiDocumentation { schema: { type: 'object', properties: { - affected: { type: 'number', example: 1 } - } - } + affected: { type: 'number', example: 1 }, + }, + }, }), ApiResponse({ status: 404, description: 'Pregunta no encontrada' }), - ApiResponse({ status: 500, description: 'Error interno del servidor' }) + ApiResponse({ status: 500, description: 'Error interno del servidor' }), ); -} \ No newline at end of file +} diff --git a/src/pregunta/pregunta.service.spec.ts b/src/pregunta/pregunta.service.spec.ts deleted file mode 100644 index 1a4b35c..0000000 --- a/src/pregunta/pregunta.service.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { PreguntaService } from './pregunta.service'; - -describe('PreguntaService', () => { - let service: PreguntaService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [PreguntaService], - }).compile(); - - service = module.get(PreguntaService); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); -}); diff --git a/src/pregunta/pregunta.service.ts b/src/pregunta/pregunta.service.ts index c339468..627a3df 100644 --- a/src/pregunta/pregunta.service.ts +++ b/src/pregunta/pregunta.service.ts @@ -42,7 +42,6 @@ export class PreguntaService { 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; @@ -158,7 +157,6 @@ export class PreguntaService { // 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 diff --git a/src/respuesta_participante_abierta/entities/respuesta_participante_abierta.entity.ts b/src/respuesta_participante_abierta/entities/respuesta_participante_abierta.entity.ts index 4a6262e..80c2ee8 100644 --- a/src/respuesta_participante_abierta/entities/respuesta_participante_abierta.entity.ts +++ b/src/respuesta_participante_abierta/entities/respuesta_participante_abierta.entity.ts @@ -1,7 +1,12 @@ import { CuestionarioRespondido } from 'src/cuestionario_respondido/entities/cuestionario_respondido.entity'; import { Pregunta } from 'src/pregunta/entities/pregunta.entity'; -import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm'; - +import { + Entity, + PrimaryGeneratedColumn, + Column, + ManyToOne, + JoinColumn, +} from 'typeorm'; @Entity('respuesta_participante_abierta') export class RespuestaParticipanteAbierta { @@ -13,8 +18,8 @@ export class RespuestaParticipanteAbierta { // Relación con CuestionarioRespondido @ManyToOne( - () => CuestionarioRespondido, - cuestionarioRespondido => cuestionarioRespondido.respuestasAbiertas + () => CuestionarioRespondido, + (cuestionarioRespondido) => cuestionarioRespondido.respuestasAbiertas, ) @JoinColumn({ name: 'id_cuestionario_respondido' }) cuestionarioRespondido: CuestionarioRespondido; @@ -22,6 +27,4 @@ export class RespuestaParticipanteAbierta { @ManyToOne(() => Pregunta) @JoinColumn({ name: 'id_pregunta' }) pregunta: Pregunta; - - -} \ No newline at end of file +} diff --git a/src/respuesta_participante_cerrada/entities/respuesta_participante_cerrada.entity.ts b/src/respuesta_participante_cerrada/entities/respuesta_participante_cerrada.entity.ts index 0d65cb0..6a9f9ac 100644 --- a/src/respuesta_participante_cerrada/entities/respuesta_participante_cerrada.entity.ts +++ b/src/respuesta_participante_cerrada/entities/respuesta_participante_cerrada.entity.ts @@ -1,20 +1,29 @@ import { CuestionarioRespondido } from 'src/cuestionario_respondido/entities/cuestionario_respondido.entity'; import { PreguntaOpcion } from 'src/pregunta_opcion/entities/pregunta_opcion.entity'; -import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm'; - +import { + Entity, + PrimaryGeneratedColumn, + Column, + ManyToOne, + JoinColumn, +} from 'typeorm'; @Entity('respuesta_participante_cerrada') export class RespuestaParticipanteCerrada { @PrimaryGeneratedColumn() idRespuestaParticipanteCerrada: number; - @ManyToOne(() => CuestionarioRespondido, cuestionarioRespondido => cuestionarioRespondido.respuestasCerradas) + @ManyToOne( + () => CuestionarioRespondido, + (cuestionarioRespondido) => cuestionarioRespondido.respuestasCerradas, + ) @JoinColumn({ name: 'id_cuestionario_respondido' }) cuestionarioRespondido: CuestionarioRespondido; - @ManyToOne(() => PreguntaOpcion, preguntaOpcion => preguntaOpcion.respuestaParticipanteCerrada) + @ManyToOne( + () => PreguntaOpcion, + (preguntaOpcion) => preguntaOpcion.respuestaParticipanteCerrada, + ) @JoinColumn({ name: 'id_pregunta_opcion' }) preguntaOpcion: PreguntaOpcion; - - -} \ No newline at end of file +} diff --git a/src/seccion/seccion.service.ts b/src/seccion/seccion.service.ts index 7915dd1..c890a07 100644 --- a/src/seccion/seccion.service.ts +++ b/src/seccion/seccion.service.ts @@ -25,7 +25,7 @@ export class SeccionService { private opcionRepository: Repository, @InjectRepository(PreguntaOpcion) private preguntaOpcionRepository: Repository, - private dataSource: DataSource + private dataSource: DataSource, ) {} async create(createSeccionDto: CreateSeccionDto) { @@ -39,58 +39,59 @@ export class SeccionService { 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`); + 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); } } @@ -98,7 +99,7 @@ export class SeccionService { } await queryRunner.commitTransaction(); - + return { success: true, seccion: savedSeccion, @@ -116,90 +117,91 @@ export class SeccionService { } async findOne(id: number) { - const seccion = await this.seccionRepository.findOne({ - where: { id_seccion: id } + const seccion = await this.seccionRepository.findOne({ + where: { id_seccion: id }, }); - + if (!seccion) { throw new NotFoundException(`Sección con ID ${id} no encontrada`); } - + return seccion; } async findWithPreguntas(id: number) { const seccion = await this.seccionRepository.findOne({ - where: { id_seccion: id } + where: { id_seccion: id }, }); - + if (!seccion) { throw new NotFoundException(`Sección con ID ${id} no encontrada`); } - + // Obtener las relaciones seccion-pregunta para esta sección const seccionPreguntas = await this.seccionPreguntaRepository.find({ where: { id_seccion: id }, - relations: ['pregunta'] + relations: ['pregunta'], }); - + // 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 } + 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' } + order: { posicion: 'ASC' }, }); - - const opciones = preguntaOpciones.map(po => po.opcion); - + + const opciones = preguntaOpciones.map((po) => po.opcion); + return { ...pregunta, posicion: sp.posicion, - opciones + opciones, }; - }) + }), ); - + // Ordenar las preguntas por posición preguntas.sort((a, b) => a.posicion - b.posicion); - + return { ...seccion, - preguntas + preguntas, }; } async update(id: number, updateSeccionDto: UpdateSeccionDto) { const seccion = await this.seccionRepository.findOne({ - where: { id_seccion: id } + 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.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 } + where: { id_seccion: id }, }); - + if (!seccion) { throw new NotFoundException(`Sección con ID ${id} no encontrada`); } - + return this.seccionRepository.remove(seccion); } } diff --git a/src/tipo_cuestionario/entities/tipo_cuestionario.entity.ts b/src/tipo_cuestionario/entities/tipo_cuestionario.entity.ts index 3a2ccf5..b00540a 100644 --- a/src/tipo_cuestionario/entities/tipo_cuestionario.entity.ts +++ b/src/tipo_cuestionario/entities/tipo_cuestionario.entity.ts @@ -1,14 +1,27 @@ import { Cuestionario } from 'src/cuestionario/entities/cuestionario.entity'; -import { Entity, PrimaryGeneratedColumn, Column, ManyToMany, ManyToOne } from 'typeorm'; +import { + Entity, + PrimaryGeneratedColumn, + Column, + OneToMany, +} from 'typeorm'; + +export enum TipoCuestionarioEnum { + REGISTRO = 'Registro', + EVALUACION = 'Evaluación', +} @Entity('tipo_cuestionario') export class TipoCuestionario { @PrimaryGeneratedColumn() id_tipo_cuestionario: number; - @Column({ type: 'varchar', length: 30 }) - tipo_cuestionario: string; + @Column({ type: 'enum', enum: TipoCuestionarioEnum }) + tipo_cuestionario: TipoCuestionarioEnum; - @ManyToOne(()=> Cuestionario,(cuestionario)=> cuestionario.id_tipo_cuestionario) - cuestionario:Cuestionario[]; + @OneToMany( + () => Cuestionario, + (cuestionario) => cuestionario.tipoCuestionario, + ) + cuestionarios: Cuestionario[]; } diff --git a/src/tipo_pregunta/entities/tipo_pregunta.entity.ts b/src/tipo_pregunta/entities/tipo_pregunta.entity.ts index e6bea46..805c5d8 100644 --- a/src/tipo_pregunta/entities/tipo_pregunta.entity.ts +++ b/src/tipo_pregunta/entities/tipo_pregunta.entity.ts @@ -1,14 +1,21 @@ import { Pregunta } from 'src/pregunta/entities/pregunta.entity'; import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm'; +export enum TipoPreguntaEnum { + AbiertaParrafo = 'Abierta (Parrafo)', + AbiertaRespuestaCorta = 'Abierta (Respuesta corta)', + Cerrada = 'Cerrada', + Multiple = 'Multiple', +} + @Entity() export class TipoPregunta { @PrimaryGeneratedColumn() id_tipo: number; - @Column() - tipo_pregunta: string; + @Column({ type: 'enum', enum: TipoPreguntaEnum }) + tipo_pregunta: TipoPreguntaEnum; - @OneToMany(() => Pregunta, pregunta => pregunta.tipoPregunta) + @OneToMany(() => Pregunta, (pregunta) => pregunta.tipoPregunta) preguntas: Pregunta[]; -} \ No newline at end of file +} diff --git a/src/tipo_pregunta/tipo_pregunta.service.ts b/src/tipo_pregunta/tipo_pregunta.service.ts index 37f97c0..5518852 100644 --- a/src/tipo_pregunta/tipo_pregunta.service.ts +++ b/src/tipo_pregunta/tipo_pregunta.service.ts @@ -3,7 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { CreateTipoPreguntaDto } from './dto/create-tipo_pregunta.dto'; import { UpdateTipoPreguntaDto } from './dto/update-tipo_pregunta.dto'; -import { TipoPregunta } from './entities/tipo_pregunta.entity'; +import { TipoPregunta, TipoPreguntaEnum } from './entities/tipo_pregunta.entity'; @Injectable() export class TipoPreguntaService implements OnModuleInit { @@ -17,20 +17,15 @@ export class TipoPreguntaService implements OnModuleInit { } private async seedTipoPreguntas() { - const defaultTipos = [ - { tipo_pregunta: 'Cerrada' }, - { tipo_pregunta: 'Abierta' }, - { tipo_pregunta: 'Multiple' }, - { tipo_pregunta: 'Texto' } - ]; + const enumValues = Object.values(TipoPreguntaEnum); - for (const tipo of defaultTipos) { + for (const tipo_pregunta of enumValues) { const existingTipo = await this.tipoPreguntaRepository.findOne({ - where: { tipo_pregunta: tipo.tipo_pregunta }, + where: { tipo_pregunta: tipo_pregunta }, }); if (!existingTipo) { - await this.tipoPreguntaRepository.save(tipo); + await this.tipoPreguntaRepository.save({ tipo_pregunta }); } } } @@ -45,7 +40,7 @@ export class TipoPreguntaService implements OnModuleInit { findOne(id: number) { return this.tipoPreguntaRepository.findOne({ - where: { id_tipo: id } + where: { id_tipo: id }, }); } diff --git a/src/tipo_user/entities/tipo_user.entity.ts b/src/tipo_user/entities/tipo_user.entity.ts new file mode 100644 index 0000000..53a3ed0 --- /dev/null +++ b/src/tipo_user/entities/tipo_user.entity.ts @@ -0,0 +1,15 @@ +import { Administrador } from 'src/administrador/entities/administrador.entity'; +import { Participante } from 'src/participante/entities/participante.entity'; +import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm'; + +@Entity() +export class TipoUser { + @PrimaryGeneratedColumn() + id_tipo_user: number; + + @Column() + tipo: string; + + @OneToMany(() => Administrador, (administrador) => administrador.tipoUser) + administrador: Administrador[]; +} diff --git a/src/tipo_user/tipo_user.controller.ts b/src/tipo_user/tipo_user.controller.ts index abed59a..a997c6a 100644 --- a/src/tipo_user/tipo_user.controller.ts +++ b/src/tipo_user/tipo_user.controller.ts @@ -1,6 +1,6 @@ import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post } from '@nestjs/common'; import { TipoUserService } from './tipo_user.service'; -import { TipoUser } from './tipo_user.entity'; +import { TipoUser } from './entities/tipo_user.entity'; import { CreateTipoUserDto } from './dto/create-tipo-user.dto'; import { UpdateTipoUserDto } from './dto/update.tipo_user.dto'; import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiBody } from '@nestjs/swagger'; diff --git a/src/tipo_user/tipo_user.entity.ts b/src/tipo_user/tipo_user.entity.ts deleted file mode 100644 index a20976e..0000000 --- a/src/tipo_user/tipo_user.entity.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Administrador } from "src/administrador/administrador.entity"; -import { Participante } from "src/participante/participante.entity"; -import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm"; - -@Entity() -export class TipoUser { - @PrimaryGeneratedColumn() - id_tipo_user: number; - - @Column() - tipo: string; - - @Column() - nombre: string; - - @Column({nullable: true}) - apellido_p: string; - - @Column({nullable: true}) - apellido_m: string; - - @OneToMany(() => Participante, participante => participante.tipo_user) - participante: Participante[] - - @OneToMany(() => Administrador, administrador => administrador.tipoUser) - administrador: Administrador[] - -} \ No newline at end of file diff --git a/src/tipo_user/tipo_user.module.ts b/src/tipo_user/tipo_user.module.ts index 2caf76b..a1fa91e 100644 --- a/src/tipo_user/tipo_user.module.ts +++ b/src/tipo_user/tipo_user.module.ts @@ -1,10 +1,10 @@ import { Module } from '@nestjs/common'; import { TipoUserService } from './tipo_user.service'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { TipoUser } from './tipo_user.entity'; +import { TipoUser } from './entities/tipo_user.entity'; import { TipoUserController } from './tipo_user.controller'; -import { Administrador } from 'src/administrador/administrador.entity'; -import { Participante } from 'src/participante/participante.entity'; +import { Administrador } from 'src/administrador/entities/administrador.entity'; +import { Participante } from 'src/participante/entities/participante.entity'; @Module({ imports: [TypeOrmModule.forFeature([TipoUser, Administrador, Participante])], diff --git a/src/tipo_user/tipo_user.service.ts b/src/tipo_user/tipo_user.service.ts index f86d4a4..8b372e8 100644 --- a/src/tipo_user/tipo_user.service.ts +++ b/src/tipo_user/tipo_user.service.ts @@ -1,22 +1,29 @@ -import { BadRequestException, HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { + BadRequestException, + HttpException, + HttpStatus, + Injectable, +} from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { TipoUser } from './tipo_user.entity'; +import { TipoUser } from './entities/tipo_user.entity'; import { Repository } from 'typeorm'; import { CreateTipoUserDto } from './dto/create-tipo-user.dto'; import { UpdateTipoUserDto } from './dto/update.tipo_user.dto'; -import { Participante } from 'src/participante/participante.entity'; -import { Administrador } from 'src/administrador/administrador.entity'; +import { Participante } from 'src/participante/entities/participante.entity'; +import { Administrador } from 'src/administrador/entities/administrador.entity'; @Injectable() export class TipoUserService { + constructor( + @InjectRepository(TipoUser) + private tipoUserRepository: Repository, + @InjectRepository(Participante) + private participanteRepository: Repository, + @InjectRepository(Administrador) + private administradorRepository: Repository, + ) {} - constructor( - @InjectRepository(TipoUser) private tipoUserRepository: Repository, - @InjectRepository(Participante) private participanteRepository: Repository, - @InjectRepository(Administrador) private administradorRepository: Repository, - ) {} - - /* + /* //Funcion para registrar usuario async registrarUsuario(dto: CreateTipoUserDto) { // Crear el registro en tipo_user @@ -56,71 +63,69 @@ export class TipoUserService { } } */ - - async createTipoUser(tipoUser: CreateTipoUserDto) { - // falta hacer la validacion - - const tipo_userFound = await this.tipoUserRepository.findOne({ - where: { - nombre: tipoUser.nombre, - tipo: tipoUser.tipo, - //id_tipo_user: - } - }) - - if(tipo_userFound) { - return new HttpException('Tipo User already exict', HttpStatus.NOT_FOUND); - } - const tipo_user = this.tipoUserRepository.create(tipoUser) - - return this.tipoUserRepository.save(tipo_user) + async createTipoUser(tipoUser: CreateTipoUserDto) { + // falta hacer la validacion + + const tipo_userFound = await this.tipoUserRepository.findOne({ + where: { + tipo: tipoUser.tipo, + //id_tipo_user: + }, + }); + + if (tipo_userFound) { + return new HttpException('Tipo User already exict', HttpStatus.NOT_FOUND); } - getTipoUsers() { - return this.tipoUserRepository.find({ - relations: ['participante', 'administrador'] - }) + const tipo_user = this.tipoUserRepository.create(tipoUser); + + return this.tipoUserRepository.save(tipo_user); + } + + getTipoUsers() { + return this.tipoUserRepository.find({ + relations: ['participante', 'administrador'], + }); + } + + async getTipoUser(id: number) { + const tipo_userFound = await this.tipoUserRepository.findOne({ + where: { + id_tipo_user: id, + }, + relations: ['participante', 'administrador'], + }); + + if (!tipo_userFound) { + return new HttpException('User not found', HttpStatus.NOT_FOUND); } - async getTipoUser(id: number) { - const tipo_userFound = await this.tipoUserRepository.findOne({ - where: { - id_tipo_user: id - }, - relations: ['participante', 'administrador'] - }); - - if(!tipo_userFound) { - return new HttpException('User not found', HttpStatus.NOT_FOUND); - } - - return tipo_userFound; + return tipo_userFound; + } + + async deleteTipoUser(id_tipo_user: number) { + const result = await this.tipoUserRepository.delete({ id_tipo_user }); + + if (result.affected === 0) { + return new HttpException('User not found', HttpStatus.NOT_FOUND); } - async deleteTipoUser(id_tipo_user: number) { - const result = await this.tipoUserRepository.delete({ id_tipo_user }) + return result; + } - if(result.affected === 0) { - return new HttpException('User not found', HttpStatus.NOT_FOUND); - } + async updateTipoUser(id_tipo_user: number, tipo_user: UpdateTipoUserDto) { + const tipo_userFound = await this.tipoUserRepository.findOne({ + where: { + id_tipo_user, + }, + }); - return result - } - - async updateTipoUser(id_tipo_user: number, tipo_user: UpdateTipoUserDto) { - const tipo_userFound = await this.tipoUserRepository.findOne({ - where: { - id_tipo_user - } - }) - - if(!tipo_userFound) { - return new HttpException('User not found', HttpStatus.NOT_FOUND) - } - - const updateTipoUser = Object.assign(tipo_userFound, tipo_user) - return this.tipoUserRepository.save(updateTipoUser) + if (!tipo_userFound) { + return new HttpException('User not found', HttpStatus.NOT_FOUND); } + const updateTipoUser = Object.assign(tipo_userFound, tipo_user); + return this.tipoUserRepository.save(updateTipoUser); + } } diff --git a/utils/get_formulario_feria.ts b/utils/get_formulario_feria.ts index dfffb3c..e84abf2 100644 --- a/utils/get_formulario_feria.ts +++ b/utils/get_formulario_feria.ts @@ -35,7 +35,6 @@ export const cuestionario_feria = { contador_opcion: 4, obligatoria: true, id_tipo_pregunta: 1, - id_opcion_dependiente: null, tipo_pregunta: { id_tipo: 1, tipo_pregunta: 'Cerrada', @@ -89,7 +88,6 @@ export const cuestionario_feria = { contador_opcion: 2, obligatoria: true, id_tipo_pregunta: 1, - id_opcion_dependiente: null, tipo_pregunta: { id_tipo: 1, tipo_pregunta: 'Cerrada', @@ -137,7 +135,6 @@ export const cuestionario_feria = { contador_opcion: 0, obligatoria: false, id_tipo_pregunta: 2, - id_opcion_dependiente: null, tipo_pregunta: { id_tipo: 2, tipo_pregunta: 'Abierta', @@ -154,7 +151,6 @@ export const cuestionario_feria = { contador_opcion: 3, obligatoria: false, id_tipo_pregunta: 3, // Puedes usar este ID para distinguir múltiples - id_opcion_dependiente: null, tipo_pregunta: { id_tipo: 3, tipo_pregunta: 'Multiple',