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/.gitignore b/.gitignore
index 4b56acf..d91926b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -54,3 +54,6 @@ pids
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
+
+# Uploaded files
+/uploads
\ 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 @@
-
-
-
+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.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+📌 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..bb96032 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16,7 +16,8 @@
"@nestjs/mapped-types": "*",
"@nestjs/passport": "^11.0.5",
"@nestjs/platform-express": "^11.0.1",
- "@nestjs/swagger": "^11.1.0",
+ "@nestjs/serve-static": "^5.0.3",
+ "@nestjs/swagger": "^11.2.0",
"@nestjs/typeorm": "^11.0.0",
"@types/bcrypt": "^5.0.2",
"@types/bcryptjs": "^2.4.6",
@@ -32,7 +33,8 @@
"qrcode": "^1.5.4",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
- "typeorm": "^0.3.21"
+ "typeorm": "^0.3.21",
+ "xlsx": "^0.18.5"
},
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",
@@ -44,6 +46,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",
@@ -2539,10 +2542,37 @@
"tslib": "^2.1.0"
}
},
+ "node_modules/@nestjs/serve-static": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/@nestjs/serve-static/-/serve-static-5.0.3.tgz",
+ "integrity": "sha512-0jFjTlSVSLrI+mot8lfm+h2laXtKzCvgsVStv9T1ZBZTDwS26gM5czIhIESmWAod0PfrbCDFiu9C1MglObL8VA==",
+ "license": "MIT",
+ "dependencies": {
+ "path-to-regexp": "8.2.0"
+ },
+ "peerDependencies": {
+ "@fastify/static": "^8.0.4",
+ "@nestjs/common": "^11.0.2",
+ "@nestjs/core": "^11.0.2",
+ "express": "^5.0.1",
+ "fastify": "^5.2.1"
+ },
+ "peerDependenciesMeta": {
+ "@fastify/static": {
+ "optional": true
+ },
+ "express": {
+ "optional": true
+ },
+ "fastify": {
+ "optional": true
+ }
+ }
+ },
"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 +2580,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 +3353,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",
@@ -4046,6 +4086,15 @@
"node": ">=0.4.0"
}
},
+ "node_modules/adler-32": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
+ "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@@ -4831,6 +4880,19 @@
],
"license": "CC-BY-4.0"
},
+ "node_modules/cfb": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
+ "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "adler-32": "~1.3.0",
+ "crc-32": "~1.2.0"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -5056,6 +5118,15 @@
"node": ">= 0.12.0"
}
},
+ "node_modules/codepage": {
+ "version": "1.15.0",
+ "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
+ "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/collect-v8-coverage": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz",
@@ -5261,6 +5332,18 @@
}
}
},
+ "node_modules/crc-32": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
+ "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
+ "license": "Apache-2.0",
+ "bin": {
+ "crc32": "bin/crc32.njs"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/create-jest": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz",
@@ -6658,6 +6741,15 @@
"node": ">= 0.6"
}
},
+ "node_modules/frac": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
+ "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/fresh": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
@@ -10497,6 +10589,18 @@
"node": ">= 0.6"
}
},
+ "node_modules/ssf": {
+ "version": "0.11.2",
+ "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
+ "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "frac": "~1.1.2"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/stack-utils": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
@@ -10835,9 +10939,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"
@@ -12087,6 +12191,24 @@
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
"license": "ISC"
},
+ "node_modules/wmf": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
+ "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/word": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
+ "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/word-wrap": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
@@ -12198,6 +12320,27 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/xlsx": {
+ "version": "0.18.5",
+ "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
+ "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "adler-32": "~1.3.0",
+ "cfb": "~1.2.1",
+ "codepage": "~1.15.0",
+ "crc-32": "~1.2.1",
+ "ssf": "~0.11.2",
+ "wmf": "~1.0.1",
+ "word": "~0.3.0"
+ },
+ "bin": {
+ "xlsx": "bin/xlsx.njs"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
diff --git a/package.json b/package.json
index 8c10e20..f56500c 100644
--- a/package.json
+++ b/package.json
@@ -27,7 +27,8 @@
"@nestjs/mapped-types": "*",
"@nestjs/passport": "^11.0.5",
"@nestjs/platform-express": "^11.0.1",
- "@nestjs/swagger": "^11.1.0",
+ "@nestjs/serve-static": "^5.0.3",
+ "@nestjs/swagger": "^11.2.0",
"@nestjs/typeorm": "^11.0.0",
"@types/bcrypt": "^5.0.2",
"@types/bcryptjs": "^2.4.6",
@@ -43,7 +44,8 @@
"qrcode": "^1.5.4",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
- "typeorm": "^0.3.21"
+ "typeorm": "^0.3.21",
+ "xlsx": "^0.18.5"
},
"devDependencies": {
"@eslint/eslintrc": "^3.2.0",
@@ -55,6 +57,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.controller.ts b/src/alumnos/alumnos.controller.ts
index 61b8a9c..44c406d 100644
--- a/src/alumnos/alumnos.controller.ts
+++ b/src/alumnos/alumnos.controller.ts
@@ -1,5 +1,5 @@
import { Controller, Get, Param, NotFoundException } from '@nestjs/common';
-import { AlumnosService } from './alumnos.service';
+import { AlumnosService, UsuarioData } from './alumnos.service';
import { ApiTags, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger';
import { AlumnoDto, ALUMNO_RESPONSES } from './alumnos.documentation';
@@ -14,7 +14,7 @@ export class AlumnosController {
@ApiResponse(ALUMNO_RESPONSES[200])
@ApiResponse(ALUMNO_RESPONSES[404])
@ApiResponse(ALUMNO_RESPONSES[500])
- async getAlumnoByCuenta(@Param('cuenta') cuenta: string): Promise {
+ async getAlumnoByCuenta(@Param('cuenta') cuenta: string): Promise {
const alumno = await this.alumnosService.findByCuenta(cuenta);
if (!alumno) {
throw new NotFoundException(`No se encontró alumno con número de cuenta ${cuenta}`);
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/alumnos/alumnos.service.ts b/src/alumnos/alumnos.service.ts
index bda130a..bef0f1a 100644
--- a/src/alumnos/alumnos.service.ts
+++ b/src/alumnos/alumnos.service.ts
@@ -3,6 +3,15 @@ import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { RegistroAlumno } from './entities/registro-alumno.entity';
+export type UsuarioData = {
+ cuenta: string | null;
+ nombre: string | null;
+ apellidos: string | null;
+ carrera: string | null;
+ genero: string | null;
+ rfc?: string | null; // Solo para trabajadores
+}
+
@Injectable()
export class AlumnosService {
constructor(
@@ -10,9 +19,17 @@ export class AlumnosService {
private registroAlumnoRepository: Repository,
) {}
- async findByCuenta(cuenta: string): Promise {
- return this.registroAlumnoRepository.findOne({
+ async findByCuenta(cuenta: string): Promise {
+ const alumno = await this.registroAlumnoRepository.findOne({
where: { id_ncuenta: parseInt(cuenta, 10) },
});
+
+ return {
+ cuenta: alumno?.id_ncuenta.toString() || null,
+ nombre: alumno?.nombre || null,
+ apellidos: alumno?.apellidos || null,
+ carrera: alumno?.carrera || null,
+ genero: alumno?.genero || null,
+ }
}
}
diff --git a/src/app.module.ts b/src/app.module.ts
index 42dc231..649d94d 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,54 @@ 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';
-
+import { TrabajadoresModule } from './trabajadores/trabajadores.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,
+ TrabajadoresModule,
],
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..2aaaa57 100644
--- a/src/cuestionario/cuestionario.controller.ts
+++ b/src/cuestionario/cuestionario.controller.ts
@@ -1,9 +1,26 @@
-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';
+import { CreateEventoWithCuestionarioDto } from './dto/create-evento-cuestionario.dto';
@Controller('cuestionario')
@CuestionarioApiDocumentation.ApiController
@@ -16,6 +33,50 @@ export class CuestionarioController {
return this.cuestionarioService.create(createCuestionarioDto);
}
+ @Post('withEvento')
+ @CuestionarioApiDocumentation.ApiCreateWithEvento
+ createCuestionarioEvento(
+ @Body() createCuestionarioDto: CreateEventoWithCuestionarioDto,
+ ) {
+ return this.cuestionarioService.createCuestionarioEvento(
+ createCuestionarioDto,
+ );
+ }
+
+ @Post(':id/banner')
+ @UseInterceptors(
+ FileInterceptor('banner', {
+ storage: diskStorage({
+ destination: './uploads/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 +88,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 +97,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..c3a8aa1 100644
--- a/src/cuestionario/cuestionario.service.ts
+++ b/src/cuestionario/cuestionario.service.ts
@@ -1,7 +1,14 @@
-import { Injectable, NotFoundException } from '@nestjs/common';
+import {
+ Injectable,
+ InternalServerErrorException,
+ 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,10 +17,13 @@ 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';
+import { CreateEventoWithCuestionarioDto } from './dto/create-evento-cuestionario.dto';
@Injectable()
export class CuestionarioService {
@@ -37,138 +47,116 @@ 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;
+ cuestionario.cupo_maximo = createCuestionarioDto.cupo_maximo;
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,10 +166,177 @@ export class CuestionarioService {
}
await queryRunner.commitTransaction();
-
+
return {
success: true,
- cuestionario: savedCuestionario,
+ message: 'Cuestionario creado exitosamente',
+ id_cuestionario: savedCuestionario.id_cuestionario,
+ id_evento: savedCuestionario.id_evento,
+ };
+ } catch (error) {
+ await queryRunner.rollbackTransaction();
+ throw new InternalServerErrorException(
+ 'Error al crear el cuestionario y evento',
+ error.message,
+ );
+ } finally {
+ await queryRunner.release();
+ }
+ }
+
+ async createCuestionarioEvento(
+ createCuestionarioDto: CreateEventoWithCuestionarioDto,
+ ) {
+ const { evento, cuestionario } = createCuestionarioDto;
+
+ const queryRunner = this.dataSource.createQueryRunner();
+ await queryRunner.connect();
+ await queryRunner.startTransaction();
+
+ try {
+ // Buscar si ya existe un evento con el nombre proporcionado
+ let eventoExistente = await this.eventoRepository.findOne({
+ where: { nombre_evento: evento.nombre_evento },
+ });
+
+ // Si el evento no existe, crearlo
+ if (!eventoExistente) {
+ eventoExistente = queryRunner.manager.create(Evento, evento);
+ await queryRunner.manager.save(eventoExistente);
+ console.log(`Creando nuevo evento: ${evento.nombre_evento}`);
+ }
+
+ // Crear el cuestionario asociado al evento
+ const cuestionarioEntity = queryRunner.manager.create(Cuestionario, {
+ nombre_form: cuestionario.nombre_form,
+ descripcion: cuestionario.descripcion,
+ fecha_inicio: new Date(cuestionario.fecha_inicio),
+ fecha_fin: new Date(cuestionario.fecha_fin),
+ id_tipo_cuestionario: cuestionario.id_tipo_cuestionario,
+ contador_secciones: cuestionario.secciones?.length || 0,
+ editable: true,
+ id_evento: eventoExistente.id_evento, // Asociar el evento creado
+ cupo_maximo: cuestionario.cupo_maximo,
+ });
+ const savedCuestionario =
+ await queryRunner.manager.save(cuestionarioEntity);
+
+ console.log(
+ `Cuestionario creado: ${savedCuestionario.nombre_form}, ID: ${savedCuestionario.id_cuestionario}`,
+ );
+
+ // Crear las secciones del cuestionario
+ if (
+ createCuestionarioDto.cuestionario.secciones &&
+ createCuestionarioDto.cuestionario.secciones.length > 0
+ ) {
+ for (
+ let i = 0;
+ i < createCuestionarioDto.cuestionario.secciones.length;
+ i++
+ ) {
+ const seccionDto = createCuestionarioDto.cuestionario.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,
+ message: 'Cuestionario y evento creados exitosamente',
+ id_cuestionario: savedCuestionario.id_cuestionario,
+ id_evento: eventoExistente.id_evento,
};
} catch (error) {
await queryRunner.rollbackTransaction();
@@ -191,106 +346,144 @@ 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,
+ },
}));
-
+
// Construir objeto de pregunta según el formato requerido
return {
id_seccion_pregunta: sp.id_seccion_pregunta,
@@ -301,18 +494,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 +513,50 @@ 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,
+ descripcion_evento: cuestionario.evento.descripcion_evento,
+ banner: cuestionario.evento.banner,
+ 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 +565,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 52%
rename from src/cuestionario/cuestionario.documentation.ts
rename to src/cuestionario/docs/cuestionario.documentation.ts
index 8caf16a..6d1b45a 100644
--- a/src/cuestionario/cuestionario.documentation.ts
+++ b/src/cuestionario/docs/cuestionario.documentation.ts
@@ -1,87 +1,82 @@
-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 { ejemploCreateEventoWithCuestionario, 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.)',
- type: FormularioDto,
- content: {
- 'application/json': {
- schema: { $ref: getSchemaPath(FormularioDto) },
- 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: ejemploCreateEventoWithCuestionario,
+ },
}),
- 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 +87,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 +124,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 +176,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 +207,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..1f06a18
--- /dev/null
+++ b/src/cuestionario/docs/cuestionario.examples.ts
@@ -0,0 +1,224 @@
+import { ejemploFeriaSexualidad } from 'src/evento/docs/evento.examples';
+import { TiposValidacion } from 'src/pregunta/entities/pregunta.entity';
+
+export const ejemploCreateEventoWithCuestionario = {
+ summary: 'Crear un evento con cuestionario asociado',
+ value: {
+ evento: ejemploFeriaSexualidad.value,
+ cuestionario: {
+ 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,
+ cupo_maximo: 100,
+ 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.APELLIDOS,
+ },
+ {
+ titulo: 'Género',
+ tipo: 'Cerrada',
+ obligatoria: true,
+ validacion: TiposValidacion.GENERO,
+ opciones: [
+ { valor: 'Masculino' },
+ { valor: 'Femenino' },
+ { valor: 'Prefiero no decirlo' },
+ ],
+ },
+ {
+ titulo: 'Institución de procedencia',
+ tipo: 'Abierta (Respuesta corta)',
+ obligatoria: true,
+ validacion: TiposValidacion.INSTITUCION,
+ },
+ {
+ titulo: 'Carrera',
+ tipo: 'Abierta (Respuesta corta)',
+ obligatoria: true,
+ validacion: TiposValidacion.CARRERA,
+ },
+ ],
+ },
+ ],
+ },
+ },
+};
+
+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,
+ cupo_maximo: 100,
+ 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.APELLIDOS,
+ },
+ {
+ titulo: 'Género',
+ tipo: 'Cerrada',
+ obligatoria: true,
+ validacion: TiposValidacion.GENERO,
+ opciones: [
+ { valor: 'Masculino' },
+ { valor: 'Femenino' },
+ { valor: 'Prefiero no decirlo' },
+ ],
+ },
+ {
+ titulo: 'Institución de procedencia',
+ tipo: 'Abierta (Respuesta corta)',
+ obligatoria: true,
+ validacion: TiposValidacion.INSTITUCION,
+ },
+ {
+ titulo: 'Carrera',
+ tipo: 'Abierta (Respuesta corta)',
+ obligatoria: true,
+ validacion: TiposValidacion.CARRERA,
+ },
+ ],
+ },
+ ],
+ },
+};
+
+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,
+ cupo_maximo: 100,
+ 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: 'RFC',
+ tipo: 'Abierta (Respuesta corta)',
+ obligatoria: false,
+ validacion: TiposValidacion.RFC,
+ },
+ {
+ titulo: 'Correo electrónico',
+ 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..f90ee44 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,40 @@ 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: 'Cupo máximo de participantes',
+ example: 100,
+ required: false,
+ })
+ @IsOptional()
+ @IsNumber()
+ cupo_maximo?: number;
+
@ApiProperty({
description: 'Nombre del evento asociado',
example: 'Feria de la Sexualidad',
- required: false
+ required: false,
})
@IsOptional()
@IsString()
@@ -59,7 +84,7 @@ export class CreateCuestionarioDto {
@ApiProperty({
description: 'Secciones del cuestionario',
type: [CreateSeccionDto],
- required: false
+ required: false,
})
@IsOptional()
@IsArray()
@@ -67,3 +92,8 @@ export class CreateCuestionarioDto {
@Type(() => CreateSeccionDto)
secciones?: CreateSeccionDto[];
}
+
+export class CreateCuestionarioEventoDto extends OmitType(
+ CreateCuestionarioDto,
+ ['id_evento'] as const,
+) {}
diff --git a/src/cuestionario/dto/create-evento-cuestionario.dto.ts b/src/cuestionario/dto/create-evento-cuestionario.dto.ts
new file mode 100644
index 0000000..2985fcf
--- /dev/null
+++ b/src/cuestionario/dto/create-evento-cuestionario.dto.ts
@@ -0,0 +1,7 @@
+import { CreateEventoDto } from 'src/evento/dto/create-evento.dto';
+import { CreateCuestionarioDto, CreateCuestionarioEventoDto } from './create-cuestionario.dto';
+
+export interface CreateEventoWithCuestionarioDto {
+ evento: CreateEventoDto;
+ cuestionario: CreateCuestionarioEventoDto;
+}
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..4521691 100644
--- a/src/cuestionario/entities/cuestionario.entity.ts
+++ b/src/cuestionario/entities/cuestionario.entity.ts
@@ -1,9 +1,16 @@
+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';
-
-
+import { CuestionarioRespondido } from 'src/cuestionario_respondido/entities/cuestionario_respondido.entity';
@Entity('cuestionario')
export class Cuestionario {
@@ -13,6 +20,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 +32,48 @@ 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: 'datetime' })
+ fecha_fin: Date;
@Column({ type: 'int', nullable: true })
- id_cuestionario_original: number;
+ cupo_maximo?: number;
@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, {
+ eager: true,
+ })
+ @JoinColumn({ name: 'id_tipo_cuestionario' })
+ tipoCuestionario: TipoCuestionario;
+
+ @OneToMany(
+ () => CuestionarioSeccion,
+ (cuestionarioSeccion) => cuestionarioSeccion.cuestionario,
+ )
+ cuestionarioSeccion: CuestionarioSeccion[];
+
+ @OneToMany(
+ () => ParticipanteEvento,
+ (participanteEvento) => participanteEvento.cuestionario,
+ )
+ inscripciones: ParticipanteEvento[];
+
+ @OneToMany(() => CuestionarioRespondido, (cr) => cr.cuestionario)
+ cuestionariosRespondidos: CuestionarioRespondido[];
}
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..ae9fe8e 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,112 @@ export class CuestionarioRespondidoService {
try {
// 1. Verificar que el cuestionario existe
- const cuestionario = await this.cuestionarioRepository.findOne({
- where: { id_cuestionario: submitDto.id_formulario },
- relations: ['evento']
- });
+ const cuestionario = await this.cuestionarioService.getCuestionarioOrFail(
+ submitDto.id_cuestionario,
+ );
- if (!cuestionario) {
- throw new NotFoundException(`Cuestionario con ID ${submitDto.id_formulario} no encontrado`);
+ //1.1 Validar que no se haya rebasado el cupo máximo
+ if (
+ cuestionario.cupo_maximo !== null &&
+ cuestionario.cupo_maximo !== undefined
+ ) {
+ const totalRespondidos =
+ await this.cuestionarioRespondidoRepository.count({
+ where: {
+ cuestionario: { id_cuestionario: submitDto.id_cuestionario },
+ },
+ });
+
+ if (totalRespondidos >= cuestionario.cupo_maximo) {
+ await queryRunner.rollbackTransaction();
+ return {
+ success: false,
+ message: `El cupo máximo de ${cuestionario.cupo_maximo} participantes ha sido alcanzado para el cuestionario ${cuestionario.nombre_form}.`,
+ };
+ }
}
// 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) {
+ await queryRunner.rollbackTransaction();
+ return {
+ success: false,
+ message: `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 +171,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 +212,113 @@ export class CuestionarioRespondidoService {
let datosQR: {
id_participante: number;
id_evento: number;
+ id_cuestionario: number;
} | null = null;
-
+
// Capturamos los datos para el QR
if (cuestionario.evento && cuestionario.evento.id_evento) {
datosQR = {
id_participante: participante.id_participante,
- id_evento: cuestionario.evento.id_evento
+ 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:
-
- Tus datos de registro:
-
- - Evento: ${cuestionario.evento.nombre_evento || 'Evento'}
- - Correo: ${participante.correo}
- - Fecha de registro: ${new Date().toLocaleString()}
-
- `,
+ subject: `Evento: ${cuestionario.evento?.nombre_evento}`,
+ 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(),
+ fechaInicioEvento: cuestionario.evento?.fecha_inicio
+ ? new Date(cuestionario.evento.fecha_inicio).toLocaleString()
+ : undefined,
+ fechaFinEvento: cuestionario.evento?.fecha_fin
+ ? new Date(cuestionario.evento.fecha_fin).toLocaleString()
+ : undefined,
+ }),
adjuntos: [
{
filename: 'qr.png',
content: qrBuffer.toString('base64'),
encoding: 'base64',
- cid: 'qrCode'
- }
- ]
+ 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 +335,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 +365,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 +400,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 +435,41 @@ 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]);
-
- console.log('Respuestas cerradas SQL:', JSON.stringify(respuestasCerradas, null, 2));
+ `,
+ [id],
+ );
// 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`;
}
@@ -389,6 +477,101 @@ recibas tu constancia.
return `This action removes a #${id} cuestionarioRespondido`;
}
+ /* 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({
@@ -397,11 +580,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 +598,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,
@@ -424,13 +613,19 @@ recibas tu constancia.
pe.fecha_asistencia
FROM cuestionario_respondido cr
JOIN participante p ON cr.id_participante = p.id_participante
- LEFT JOIN participante_evento pe ON p.id_participante = pe.id_participante AND pe.id_evento = ?
+ LEFT JOIN participante_evento pe ON p.id_participante = pe.id_participante AND pe.id_cuestionario = ?
WHERE cr.id_cuestionario = ?
ORDER BY cr.fecha_respuesta DESC
- `, [cuestionario.evento ? cuestionario.evento.id_evento : null, idCuestionario]);
+ `,
+ [
+ cuestionario.evento ? cuestionario.id_cuestionario : 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 +634,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 +650,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 +681,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..1ae25ac
--- /dev/null
+++ b/src/db/typeorm.config.ts
@@ -0,0 +1,78 @@
+import { ConfigService } from '@nestjs/config';
+import { TypeOrmModuleOptions } from '@nestjs/typeorm';
+import { RegistroAlumno } from '../alumnos/entities/registro-alumno.entity';
+import { RegistroTrabajador } from 'src/trabajadores/entities/trabajadore.entity';
+import { Administrador } from 'src/administrador/entities/administrador.entity';
+import { Asistencia } from 'src/asistencia/entities/asistencia.entity';
+import { Cuestionario } from 'src/cuestionario/entities/cuestionario.entity';
+import { CuestionarioRespondido } from 'src/cuestionario_respondido/entities/cuestionario_respondido.entity';
+import { CuestionarioSeccion } from 'src/cuestionario_seccion/entities/cuestionario_seccion.entity';
+import { Evento } from 'src/evento/entities/evento.entity';
+import { Opcion } from 'src/opcion/entities/opcion.entity';
+import { Participante } from 'src/participante/entities/participante.entity';
+import { ParticipanteEvento } from 'src/participante_evento/entities/participante_evento.entity';
+import { Pregunta } from 'src/pregunta/entities/pregunta.entity';
+import { PreguntaOpcion } from 'src/pregunta_opcion/entities/pregunta_opcion.entity';
+import { RespuestaParticipanteAbierta } from 'src/respuesta_participante_abierta/entities/respuesta_participante_abierta.entity';
+import { RespuestaParticipanteCerrada } from 'src/respuesta_participante_cerrada/entities/respuesta_participante_cerrada.entity';
+import { Seccion } from 'src/seccion/entities/seccion.entity';
+import { SeccionPregunta } from 'src/seccion_pregunta/entities/seccion_pregunta.entity';
+import { TipoCuestionario } from 'src/tipo_cuestionario/entities/tipo_cuestionario.entity';
+import { TipoPregunta } from 'src/tipo_pregunta/entities/tipo_pregunta.entity';
+import { TipoUser } from 'src/tipo_user/entities/tipo_user.entity';
+
+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: [
+ Administrador,
+ Asistencia,
+ Cuestionario,
+ CuestionarioRespondido,
+ CuestionarioSeccion,
+ Evento,
+ Opcion,
+ Participante,
+ ParticipanteEvento,
+ Pregunta,
+ PreguntaOpcion,
+ RespuestaParticipanteAbierta,
+ RespuestaParticipanteCerrada,
+ Seccion,
+ SeccionPregunta,
+ TipoCuestionario,
+ TipoPregunta,
+ TipoUser,
+ ],
+ retryAttempts: 5,
+ retryDelay: 3000,
+ connectTimeout: 30000,
+
+ synchronize: configService.get('SYNCHRONIZE') === 'true',
+ dropSchema: configService.get('DROP_SCHEMA') === '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, RegistroTrabajador],
+ retryAttempts: 10,
+ retryDelay: 3000,
+ connectTimeout: 30000,
+
+ synchronize: false,
+ dropSchema: false,
+});
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..c533d66
--- /dev/null
+++ b/src/emails/registro-evento.ts
@@ -0,0 +1,61 @@
+export function generarHtmlCorreoAsistencia({
+ nombreForm,
+ nombreEvento,
+ correo,
+ fechaRegistro,
+ fechaInicioEvento,
+ fechaFinEvento,
+}: {
+ nombreForm: string;
+ nombreEvento: string;
+ correo: string;
+ fechaRegistro: string;
+ fechaInicioEvento?: string;
+ fechaFinEvento?: string;
+}) {
+ const horarioEvento = fechaInicioEvento && fechaFinEvento
+ ? `
+
+
+ 🕒 Horario del evento:
+ ${fechaInicioEvento} al ${fechaFinEvento}
+
+
+ `
+ : '';
+
+ 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.
+
+
+
+

+
+
+
📋 Tus datos de registro:
+
+ - Evento: ${nombreEvento || 'Evento'}
+ - Correo: ${correo}
+ - Fecha de registro: ${fechaRegistro}
+
+
+ ${horarioEvento}
+
+
+ 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/outpus.dto.ts b/src/evento/dto/outpus.dto.ts
new file mode 100644
index 0000000..de1e392
--- /dev/null
+++ b/src/evento/dto/outpus.dto.ts
@@ -0,0 +1,68 @@
+import { Expose, Type } from 'class-transformer';
+import { TipoCuestionarioOutputDto } from 'src/tipo_cuestionario/dto/outputs.dto';
+
+export class CuestionarioConCupoDto {
+ @Expose()
+ id_cuestionario: number;
+
+ @Expose()
+ nombre_form: string;
+
+ @Expose()
+ descripcion?: string;
+
+ @Expose()
+ banner: string;
+
+ @Expose()
+ fecha_inicio: Date;
+
+ @Expose()
+ fecha_fin: Date;
+
+ @Expose()
+ cupo_maximo: number | null;
+
+ @Expose()
+ cupos_usados: number;
+
+ @Expose()
+ cupos_disponibles: number | null;
+
+ @Expose()
+ id_evento: number;
+
+ @Expose()
+ @Type(() => TipoCuestionarioOutputDto)
+ tipoCuestionario: TipoCuestionarioOutputDto;
+}
+
+export class EventoConCuestionariosDto {
+ @Expose()
+ id_evento: number;
+
+ @Expose()
+ tipo_evento: string;
+
+ @Expose()
+ nombre_evento: string;
+
+ @Expose()
+ descripcion_evento: string;
+
+ @Expose()
+ fecha_inicio: Date;
+
+ @Expose()
+ fecha_fin: Date;
+
+ @Expose()
+ asistencias: any;
+
+ @Expose()
+ banner: string | null;
+
+ @Expose()
+ @Type(() => CuestionarioConCupoDto) // <- Esto es lo que faltaba
+ cuestionarios: CuestionarioConCupoDto[];
+}
diff --git a/src/evento/dto/update.evento.dto.ts b/src/evento/dto/update.evento.dto.ts
index f85579b..d567e22 100644
--- a/src/evento/dto/update.evento.dto.ts
+++ b/src/evento/dto/update.evento.dto.ts
@@ -1,6 +1,27 @@
+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()
+ @IsOptional()
+ nombre_evento: string;
+
+ @IsString()
+ @IsOptional()
+ descripcion_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..f202559 100644
--- a/src/evento/evento.controller.ts
+++ b/src/evento/evento.controller.ts
@@ -1,37 +1,117 @@
-import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post } from '@nestjs/common';
+import {
+ Body,
+ Controller,
+ Delete,
+ Get,
+ Param,
+ ParseIntPipe,
+ Patch,
+ Post,
+ UnprocessableEntityException,
+ 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('activos/cuestionarios')
+ getCuestionariosActivos() {
+ return this.eventoService.getEventosActivosCuestionarios();
+ }
+
+ @Get('activos')
+ getEventosActivos(): Promise {
+ return this.eventoService.getEventosActivos();
+ }
+
+ @Get(':id/cuestionarios')
+ getCuestionarios(@Param('id', ParseIntPipe) id: number) {
+ return this.eventoService.getCuestionariosEvento(id);
+ }
+
+ @Get(':id_evento/cuestionario/:id_cuestionario')
+ getCuestionarioEvento(
+ @Param('id_evento', ParseIntPipe) id_evento: number,
+ @Param('id_cuestionario', ParseIntPipe) id_cuestionario: number,
+ ) {
+ return this.eventoService.getCuestionarioEvento(id_evento, id_cuestionario);
+ }
+
+ @Post(':id/banner')
+ @UseInterceptors(
+ FileInterceptor('banner', {
+ storage: diskStorage({
+ destination: './uploads/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 UnprocessableEntityException(
+ 'No se ha subido ningún archivo o el archivo no es 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..e184d45 100644
--- a/src/evento/evento.service.ts
+++ b/src/evento/evento.service.ts
@@ -1,73 +1,219 @@
-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';
+import { join } from 'path';
+import { existsSync, unlinkSync } from 'fs';
+import { plainToInstance } from 'class-transformer';
+import { EventoConCuestionariosDto } from './dto/outpus.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);
- getEventos() {
- return this.eventoRepository.find({
- relations: ['participantes']
- })
- }
+ return this.eventoRepository.save(createEvento);
+ }
- async getEvento(id_evento: number) {
- const eventoFound = await this.eventoRepository.findOne({
- where: {
- id_evento
- },
- relations: ['participantes']
- })
+ // evento.service.ts
- if (!eventoFound)
- return new HttpException('Evento not found', HttpStatus.NOT_FOUND);
+ async asociarBanner(id: number, filename: string) {
+ const evento = await this.getEventoOrFail(id);
- return eventoFound
- }
-
- async deleteEvento(id_evento: number) {
- const result = await this.eventoRepository.delete({ id_evento })
-
- if (result.affected === 0) {
- return new HttpException('Evento not found', HttpStatus.NOT_FOUND);
+ // Si ya tiene un banner anterior, eliminarlo
+ if (evento.banner) {
+ const bannerPath = join(
+ __dirname,
+ '..',
+ '..',
+ 'uploads',
+ 'banners',
+ evento.banner,
+ );
+ if (existsSync(bannerPath)) {
+ try {
+ unlinkSync(bannerPath);
+ } catch (err) {
+ console.error(`Error al eliminar el banner anterior: ${err.message}`);
}
-
- return result
+ }
}
- async updateEvento(id_evento: number, evento: UpdateEventoDto) {
- const eventoFound = await this.eventoRepository.findOne({
- where: {
- id_evento
- }
- })
+ // Asociar el nuevo banner
+ evento.banner = filename;
+ return this.eventoRepository.save(evento);
+ }
- if (!eventoFound)
- return new HttpException('Evento not found', HttpStatus.NOT_FOUND)
+ getEventos() {
+ return this.eventoRepository.find();
+ }
- const updateEvento = Object.assign(eventoFound, evento)
- return this.eventoRepository.save(updateEvento)
+ async getCuestionariosEvento(id_evento: number) {
+ const eventos = await this.eventoRepository.findOne({
+ where: {
+ id_evento: id_evento,
+ },
+ relations: ['cuestionarios', 'cuestionarios.cuestionariosRespondidos'],
+ });
+
+ if (!eventos) {
+ throw new NotFoundException(`Evento con ID ${id_evento} no encontrado.`);
}
+
+ const eventoConCupos = {
+ ...eventos,
+ cuestionarios: eventos.cuestionarios.map((cuest) => {
+ const cupoMaximo = cuest.cupo_maximo ?? null;
+ const usados = cuest.cuestionariosRespondidos?.length || 0;
+ const disponibles =
+ cupoMaximo !== null ? Math.max(0, cupoMaximo - usados) : null;
+
+ return {
+ ...cuest,
+ cupos_usados: usados,
+ cupos_disponibles: disponibles,
+ };
+ }),
+ };
+
+ return plainToInstance(EventoConCuestionariosDto, eventoConCupos, {
+ excludeExtraneousValues: true,
+ });
+ }
+
+ async getEvento(id_evento: number) {
+ return await this.eventoRepository.findOne({
+ where: {
+ id_evento,
+ },
+ });
+ }
+
+ async getEventosActivosCuestionarios() {
+ const eventos = await this.eventoRepository.find({
+ where: {
+ fecha_fin: MoreThan(new Date()),
+ },
+ relations: ['cuestionarios', 'cuestionarios.cuestionariosRespondidos'],
+ });
+
+ const eventosConCupos = eventos.map((evento) => {
+ return {
+ ...evento,
+ cuestionarios: evento.cuestionarios.map((cuest) => {
+ const cupoMaximo = cuest.cupo_maximo ?? null;
+ const usados = cuest.cuestionariosRespondidos?.length || 0;
+ const disponibles =
+ cupoMaximo !== null ? Math.max(0, cupoMaximo - usados) : null;
+
+ return {
+ ...cuest,
+ cupos_usados: usados,
+ cupos_disponibles: disponibles,
+ };
+ }),
+ };
+ });
+
+ return plainToInstance(EventoConCuestionariosDto, eventosConCupos, {
+ excludeExtraneousValues: true,
+ });
+ }
+
+ 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,
+ );
+ }
+
+ return eventoFound;
+ }
+
+ async deleteEvento(id_evento: number) {
+ await this.getEventoOrFail(id_evento);
+
+ const result = await this.eventoRepository.delete(id_evento);
+
+ if (result.affected === 0) {
+ throw new HttpException('Evento not found', HttpStatus.NOT_FOUND);
+ }
+
+ return { message: 'Evento eliminado exitosamente' };
+ }
+
+ async updateEvento(id_evento: number, evento: UpdateEventoDto) {
+ const eventoFound = await this.getEventoOrFail(id_evento);
+
+ 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),
+ },
+ });
+ }
+
+ async getCuestionarioEvento(id_evento: number, id_cuestionario: number) {
+ const evento = await this.eventoRepository.findOne({
+ where: { id_evento },
+ relations: ['cuestionarios'],
+ });
+
+ if (!evento) {
+ throw new NotFoundException(`Evento con ID ${id_evento} no encontrado.`);
+ }
+
+ const cuestionario = evento.cuestionarios.find(
+ (c) => c.id_cuestionario === id_cuestionario,
+ );
+
+ if (!cuestionario) {
+ throw new NotFoundException(
+ `Cuestionario no asignado al evento ${evento.nombre_evento}.`,
+ );
+ }
+
+ // Return evento info, but only with the searched cuestionario
+ return {
+ ...evento,
+ cuestionarios: undefined,
+ cuestionario: cuestionario,
+ };
+ }
}
diff --git a/src/main.ts b/src/main.ts
index bdbcf72..b72cbda 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,23 +1,28 @@
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';
+import { NestExpressApplication } from '@nestjs/platform-express';
+import { join } from 'path';
async function bootstrap() {
- const app = await NestFactory.create(AppModule);
-
+ 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
- })
+ }),
);
-
+
+ app.useStaticAssets(join(__dirname, '..', 'uploads'), {
+ index: false,
+ prefix: '/',
+ });
+
// CORS configuration
app.enableCors({
origin: '*',
@@ -25,34 +30,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..2b1d71e 100644
--- a/src/participante_evento/participante_evento.service.ts
+++ b/src/participante_evento/participante_evento.service.ts
@@ -1,160 +1,178 @@
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: ['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);
- 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..4ba1d32 100644
--- a/src/pregunta/entities/pregunta.entity.ts
+++ b/src/pregunta/entities/pregunta.entity.ts
@@ -1,8 +1,33 @@
+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',
+ APELLIDOS = 'apellidos',
+ ENTERO = 'entero',
+ DECIMAL = 'decimal',
+ COMUNIDAD_ALUMNO = 'comunidad_alumno',
+ CUENTA_ALUMNO = 'cuenta_alumno',
+ COMUNIDAD_TRABAJADOR = 'comunidad_trabajador',
+ CUENTA_TRABAJADOR = 'cuenta_trabajador',
+ GENERO = 'genero',
+ CARRERA = 'carrera',
+ INSTITUCION = 'institucion',
+ RFC = 'rfc',
+}
@Entity()
export class Pregunta {
@@ -18,22 +43,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/dto/outputs.dto.ts b/src/tipo_cuestionario/dto/outputs.dto.ts
new file mode 100644
index 0000000..1f225ee
--- /dev/null
+++ b/src/tipo_cuestionario/dto/outputs.dto.ts
@@ -0,0 +1,9 @@
+import { Expose } from 'class-transformer';
+
+export class TipoCuestionarioOutputDto {
+ @Expose()
+ id_tipo_cuestionario: number;
+
+ @Expose()
+ tipo_cuestionario: string;
+}
\ No newline at end of file
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_cuestionario/tipo_cuestionario.module.ts b/src/tipo_cuestionario/tipo_cuestionario.module.ts
index 00cf32e..a89696d 100644
--- a/src/tipo_cuestionario/tipo_cuestionario.module.ts
+++ b/src/tipo_cuestionario/tipo_cuestionario.module.ts
@@ -1,8 +1,11 @@
import { Module } from '@nestjs/common';
import { TipoCuestionarioService } from './tipo_cuestionario.service';
import { TipoCuestionarioController } from './tipo_cuestionario.controller';
+import { TypeOrmModule } from '@nestjs/typeorm';
+import { TipoCuestionario } from './entities/tipo_cuestionario.entity';
@Module({
+ imports: [TypeOrmModule.forFeature([TipoCuestionario])],
controllers: [TipoCuestionarioController],
providers: [TipoCuestionarioService],
})
diff --git a/src/tipo_cuestionario/tipo_cuestionario.service.ts b/src/tipo_cuestionario/tipo_cuestionario.service.ts
index ba3da92..c15a3a5 100644
--- a/src/tipo_cuestionario/tipo_cuestionario.service.ts
+++ b/src/tipo_cuestionario/tipo_cuestionario.service.ts
@@ -1,9 +1,40 @@
import { Injectable } from '@nestjs/common';
import { CreateTipoCuestionarioDto } from './dto/create-tipo_cuestionario.dto';
import { UpdateTipoCuestionarioDto } from './dto/update-tipo_cuestionario.dto';
+import { InjectRepository } from '@nestjs/typeorm';
+import {
+ TipoCuestionario,
+ TipoCuestionarioEnum,
+} from './entities/tipo_cuestionario.entity';
+import { Repository } from 'typeorm';
@Injectable()
export class TipoCuestionarioService {
+ constructor(
+ @InjectRepository(TipoCuestionario)
+ private tipoCuestionarioRepository: Repository,
+ ) {}
+
+ async onModuleInit() {
+ await this.seedTipoCuestionarios();
+ }
+
+ private async seedTipoCuestionarios() {
+ const enumValues = Object.values(TipoCuestionarioEnum);
+
+ for (const tipoCuestionario of enumValues) {
+ const existingTipo = await this.tipoCuestionarioRepository.findOne({
+ where: { tipo_cuestionario: tipoCuestionario },
+ });
+
+ if (!existingTipo) {
+ await this.tipoCuestionarioRepository.save({
+ tipo_cuestionario: tipoCuestionario,
+ });
+ }
+ }
+ }
+
create(createTipoCuestionarioDto: CreateTipoCuestionarioDto) {
return 'This action adds a new tipoCuestionario';
}
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/src/trabajadores/docs/trabajadores.docs.ts b/src/trabajadores/docs/trabajadores.docs.ts
new file mode 100644
index 0000000..967720b
--- /dev/null
+++ b/src/trabajadores/docs/trabajadores.docs.ts
@@ -0,0 +1,30 @@
+import { applyDecorators } from '@nestjs/common';
+import {
+ ApiTags,
+ ApiOperation,
+ ApiConsumes,
+ ApiBody,
+} from '@nestjs/swagger';
+
+export class TrabajadorApiDocumentation {
+ static ApiController = ApiTags('Trabajador');
+
+ static ApiCargarArchivo = applyDecorators(
+ ApiOperation({ summary: 'Cargar archivo Excel con trabajadores' }),
+ ApiConsumes('multipart/form-data'),
+ ApiBody({
+ description: 'Archivo Excel (.xls o .xlsx) con columnas: rfc, num_empleado, nombre, adscripcion, correo, AP1, AP2, Nombres, sexo',
+ schema: {
+ type: 'object',
+ properties: {
+ file: {
+ type: 'string',
+ format: 'binary',
+ description: 'Archivo Excel con trabajadores a registrar',
+ },
+ },
+ required: ['file'],
+ },
+ }),
+ );
+}
diff --git a/src/trabajadores/dto/create-trabajadore.dto.ts b/src/trabajadores/dto/create-trabajadore.dto.ts
new file mode 100644
index 0000000..1219a60
--- /dev/null
+++ b/src/trabajadores/dto/create-trabajadore.dto.ts
@@ -0,0 +1 @@
+export class CreateTrabajadoreDto {}
diff --git a/src/trabajadores/dto/update-trabajadore.dto.ts b/src/trabajadores/dto/update-trabajadore.dto.ts
new file mode 100644
index 0000000..dd69e6d
--- /dev/null
+++ b/src/trabajadores/dto/update-trabajadore.dto.ts
@@ -0,0 +1,4 @@
+import { PartialType } from '@nestjs/swagger';
+import { CreateTrabajadoreDto } from './create-trabajadore.dto';
+
+export class UpdateTrabajadoreDto extends PartialType(CreateTrabajadoreDto) {}
diff --git a/src/trabajadores/entities/trabajadore.entity.ts b/src/trabajadores/entities/trabajadore.entity.ts
new file mode 100644
index 0000000..4b4c415
--- /dev/null
+++ b/src/trabajadores/entities/trabajadore.entity.ts
@@ -0,0 +1,25 @@
+import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
+
+@Entity('registro_trabajador')
+export class RegistroTrabajador {
+ @PrimaryGeneratedColumn()
+ id_trabajador: number;
+
+ @Column({ type: 'int', unique: true, nullable: true })
+ num_trabajador: number;
+
+ @Column({ type: 'varchar', length: 13, unique: true })
+ rfc: string;
+
+ @Column({ type: 'varchar', length: 100 })
+ nombre: string;
+
+ @Column({ type: 'varchar', length: 100 })
+ apellidos: string;
+
+ @Column({ type: 'char', length: 1 })
+ genero: string;
+
+ @Column({ type: 'varchar', length: 100 })
+ carrera: string;
+}
diff --git a/src/trabajadores/trabajadores.controller.ts b/src/trabajadores/trabajadores.controller.ts
new file mode 100644
index 0000000..a10fe84
--- /dev/null
+++ b/src/trabajadores/trabajadores.controller.ts
@@ -0,0 +1,69 @@
+import {
+ Controller,
+ Get,
+ Post,
+ Body,
+ Patch,
+ Param,
+ Delete,
+ UseInterceptors,
+ UploadedFile,
+ NotFoundException,
+} from '@nestjs/common';
+import { TrabajadoresService } from './trabajadores.service';
+import { CreateTrabajadoreDto } from './dto/create-trabajadore.dto';
+import { UpdateTrabajadoreDto } from './dto/update-trabajadore.dto';
+import { FileInterceptor } from '@nestjs/platform-express';
+import { diskStorage } from 'multer';
+import { extname } from 'path';
+import { TrabajadorApiDocumentation } from './docs/trabajadores.docs';
+
+@Controller('trabajadores')
+@TrabajadorApiDocumentation.ApiController
+export class TrabajadoresController {
+ constructor(private readonly trabajadoresService: TrabajadoresService) {}
+
+ @Post('cargar')
+ @TrabajadorApiDocumentation.ApiCargarArchivo
+ @UseInterceptors(
+ FileInterceptor('file', {
+ storage: diskStorage({
+ destination: './uploads',
+ filename: (_, file, callback) => {
+ const uniqueName = `${Date.now()}${extname(file.originalname)}`;
+ callback(null, uniqueName);
+ },
+ }),
+ }),
+ )
+ async cargarArchivo(@UploadedFile() file: Express.Multer.File) {
+ return this.trabajadoresService.procesarArchivo(file.path);
+ }
+
+ @Post()
+ create(@Body() createTrabajadoreDto: CreateTrabajadoreDto) {
+ return this.trabajadoresService.create(createTrabajadoreDto);
+ }
+
+ @Get(':rfc')
+ findByRfc(@Param('rfc') rfc: string) {
+ const trabajador = this.trabajadoresService.findByRfc(rfc);
+ if (!trabajador) {
+ throw new NotFoundException(`No se encontró trabajador con RFC ${rfc}`);
+ }
+ return trabajador;
+ }
+
+ @Patch(':id')
+ update(
+ @Param('id') id: string,
+ @Body() updateTrabajadoreDto: UpdateTrabajadoreDto,
+ ) {
+ return this.trabajadoresService.update(+id, updateTrabajadoreDto);
+ }
+
+ @Delete(':id')
+ remove(@Param('id') id: string) {
+ return this.trabajadoresService.remove(+id);
+ }
+}
diff --git a/src/trabajadores/trabajadores.module.ts b/src/trabajadores/trabajadores.module.ts
new file mode 100644
index 0000000..d9f8855
--- /dev/null
+++ b/src/trabajadores/trabajadores.module.ts
@@ -0,0 +1,15 @@
+import { Module } from '@nestjs/common';
+import { TrabajadoresService } from './trabajadores.service';
+import { TrabajadoresController } from './trabajadores.controller';
+import { TypeOrmModule } from '@nestjs/typeorm';
+import { RegistroTrabajador } from './entities/trabajadore.entity';
+
+@Module({
+ imports: [
+ TypeOrmModule.forFeature([RegistroTrabajador], 'alumnosConnection'),
+ ],
+ controllers: [TrabajadoresController],
+ providers: [TrabajadoresService],
+ exports: [TrabajadoresService],
+})
+export class TrabajadoresModule {}
diff --git a/src/trabajadores/trabajadores.service.ts b/src/trabajadores/trabajadores.service.ts
new file mode 100644
index 0000000..4aa8355
--- /dev/null
+++ b/src/trabajadores/trabajadores.service.ts
@@ -0,0 +1,108 @@
+import { Injectable } from '@nestjs/common';
+import { CreateTrabajadoreDto } from './dto/create-trabajadore.dto';
+import { UpdateTrabajadoreDto } from './dto/update-trabajadore.dto';
+import { InjectRepository } from '@nestjs/typeorm';
+import { RegistroTrabajador } from './entities/trabajadore.entity';
+import { Repository } from 'typeorm';
+import * as fs from 'fs';
+import * as XLSX from 'xlsx';
+import { UsuarioData } from 'src/alumnos/alumnos.service';
+
+@Injectable()
+export class TrabajadoresService {
+ constructor(
+ @InjectRepository(RegistroTrabajador, 'alumnosConnection')
+ private readonly trabajadorRepo: Repository,
+ ) {}
+
+ async procesarArchivo(
+ filePath: string,
+ ): Promise<{ insertados: number; omitidos: number }> {
+ const workbook = XLSX.readFile(filePath);
+ const sheetName = workbook.SheetNames[0];
+ const data: any[] = XLSX.utils.sheet_to_json(workbook.Sheets[sheetName]);
+
+ let insertados = 0;
+ let omitidos = 0;
+
+ for (const row of data) {
+ const rfc = (row['rfc'] || '').trim();
+
+ if (!rfc) {
+ omitidos++;
+ continue; // RFC es obligatorio
+ }
+
+ const numTrabajadorRaw = row['num_empleado'];
+ const numTrabajador = Number.isFinite(Number(numTrabajadorRaw))
+ ? Number(numTrabajadorRaw)
+ : undefined;
+
+ const where: any[] = [{ rfc }];
+ if (numTrabajador !== undefined) {
+ where.push({ num_trabajador: numTrabajador });
+ }
+
+ const existe = await this.trabajadorRepo.findOne({ where });
+
+ if (existe) {
+ omitidos++;
+ continue;
+ }
+
+ const nuevo = this.trabajadorRepo.create({
+ rfc,
+ num_trabajador: numTrabajador,
+ nombre: (row['Nombres'] || '').trim(),
+ apellidos:
+ `${(row['AP1'] || '').trim()} ${(row['AP2'] || '').trim()}`.trim(),
+ genero: (row['sexo'] || '').trim().substring(0, 1).toUpperCase(),
+ carrera: (row['adscripcion'] || '').trim(),
+ });
+
+ await this.trabajadorRepo.save(nuevo);
+ insertados++;
+ }
+
+ fs.unlinkSync(filePath); // Limpia archivo temporal
+
+ return { insertados, omitidos };
+ }
+
+ async findByRfc(rfc: string): Promise {
+ const trabajador = await this.trabajadorRepo.findOne({
+ where: { rfc: rfc.trim().toUpperCase() },
+ });
+
+ return {
+ cuenta: trabajador?.num_trabajador?.toString() || null,
+ nombre: trabajador?.nombre || null,
+ apellidos: trabajador
+ ? `${trabajador.apellidos}`.trim()
+ : null,
+ carrera: trabajador?.carrera || null,
+ genero: trabajador?.genero || null,
+ rfc: trabajador?.rfc || null, // Incluye RFC en el retorno
+ }
+ }
+
+ create(createTrabajadoreDto: CreateTrabajadoreDto) {
+ return 'This action adds a new trabajadore';
+ }
+
+ findAll() {
+ return `This action returns all trabajadores`;
+ }
+
+ findOne(id: number) {
+ return `This action returns a #${id} trabajadore`;
+ }
+
+ update(id: number, updateTrabajadoreDto: UpdateTrabajadoreDto) {
+ return `This action updates a #${id} trabajadore`;
+ }
+
+ remove(id: number) {
+ return `This action removes a #${id} trabajadore`;
+ }
+}
diff --git a/uploads/banners/banner-1750269883742-220110679.jpeg b/uploads/banners/banner-1750269883742-220110679.jpeg
new file mode 100644
index 0000000..b99380a
Binary files /dev/null and b/uploads/banners/banner-1750269883742-220110679.jpeg differ
diff --git a/utils/crear_formulario_feria.ts b/utils/crear_formulario_feria.ts
deleted file mode 100644
index 0b2d77d..0000000
--- a/utils/crear_formulario_feria.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-export const feriaSexualidad = {
- nombre_form: 'Registro para la Feria de la Sexualidad - FES Acatlán',
- descripcion:
- 'La Feria de la Sexualidad es un espacio seguro e informativo dirigido a toda la comunidad universitaria. Este registro nos ayudará a organizar mejor las actividades, talleres y charlas, así como conocer tus intereses. Por favor, completa el siguiente formulario para confirmar tu participación.',
- fecha_inicio: '2025-03-07T00:00:01',
- fecha_fin: '2025-03-18T23:59:59',
- id_tipo_cuestionario: 1,
- evento: 'Feria de la Sexualidad',
- 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,
- },
- {
- titulo: 'Correo electrónico',
- obligatoria: true,
- tipo: 'Abierta',
- limite: 250,
- validacion: 'correo',
- },
- {
- titulo: 'Numero de cuenta',
- obligatoria: true,
- tipo: 'Abierta',
- limite: 250,
- validacion: 'cuenta_alumno',
- },
- {
- titulo: 'Nombre(s)',
- obligatoria: true,
- tipo: 'Abierta',
- limite: 250,
- validacion: 'nombre',
- },
- {
- titulo: 'Apellidos',
- obligatoria: true,
- tipo: 'Abierta',
- limite: 250,
- validacion: 'nombre',
- },
- {
- titulo: 'Género',
- obligatoria: true,
- tipo: 'Cerrada',
- opciones: [
- { valor: 'Masculino' },
- { valor: 'Femenino' },
- { valor: 'Prefiero no decirlo' },
- ],
- },
- {
- titulo: 'Institución de procedencia',
- tipo: 'Abierta',
- obligatoria: true,
- limite: 250,
- },
- ],
- },
- ],
- };
\ No newline at end of file
diff --git a/utils/get_formulario_feria.ts b/utils/get_formulario_feria.ts
deleted file mode 100644
index dfffb3c..0000000
--- a/utils/get_formulario_feria.ts
+++ /dev/null
@@ -1,197 +0,0 @@
-export const cuestionario_feria = {
- tipo_cuestionario: {
- id_tipo_cuestionario: 1,
- tipo_cuestionario: 'Encuesta',
- },
- cuestionario: {
- id_cuestionario: 10,
- nombre_form:
- 'Registro para la Feria de la Sexualidad - Comunidad FES Acatlán',
- contador_secciones: 2,
- descripcion:
- 'La Feria de la Sexualidad es un espacio seguro e informativo dirigido a toda la comunidad universitaria. Este registro nos ayudará a organizar mejor las actividades, talleres y charlas, así como conocer tus intereses. Por favor, completa el siguiente formulario para confirmar tu participación.',
- editable: true,
- fecha_inicio: '2025-03-26T00:00:00',
- fecha_fin: '2025-03-31T23:59:59',
- id_cuestionario_original: null,
- id_tipo_cuestionario: 1,
- secciones: [
- {
- id_cuestionario_seccion: 1,
- posicion: 1,
- seccion: {
- id_seccion: 100,
- contador_pregunta: 2,
- descripcion: 'Preguntas generales',
- titulo: 'General',
- },
- preguntas: [
- {
- id_seccion_pregunta: 1000,
- posicion: 1,
- pregunta: {
- id_pregunta: 10000,
- pregunta: '¿Cómo calificaría nuestro servicio?',
- contador_opcion: 4,
- obligatoria: true,
- id_tipo_pregunta: 1,
- id_opcion_dependiente: null,
- tipo_pregunta: {
- id_tipo: 1,
- tipo_pregunta: 'Cerrada',
- },
- opciones: [
- {
- id_pregunta_opcion: 50000,
- posicion: 1,
- id_opcion: 90000,
- opcion: {
- id_opcion: 90000,
- opcion: 'Excelente',
- },
- },
- {
- id_pregunta_opcion: 50001,
- posicion: 2,
- id_opcion: 90001,
- opcion: {
- id_opcion: 90001,
- opcion: 'Bueno',
- },
- },
- {
- id_pregunta_opcion: 50002,
- posicion: 3,
- id_opcion: 90002,
- opcion: {
- id_opcion: 90002,
- opcion: 'Regular',
- },
- },
- {
- id_pregunta_opcion: 50003,
- posicion: 4,
- id_opcion: 90003,
- opcion: {
- id_opcion: 90003,
- opcion: 'Malo',
- },
- },
- ],
- },
- },
- {
- id_seccion_pregunta: 1001,
- posicion: 2,
- pregunta: {
- id_pregunta: 10001,
- pregunta: '¿Recomendaría nuestro servicio?',
- contador_opcion: 2,
- obligatoria: true,
- id_tipo_pregunta: 1,
- id_opcion_dependiente: null,
- tipo_pregunta: {
- id_tipo: 1,
- tipo_pregunta: 'Cerrada',
- },
- opciones: [
- {
- id_pregunta_opcion: 50004,
- posicion: 1,
- id_opcion: 90004,
- opcion: {
- id_opcion: 90004,
- opcion: 'Sí',
- },
- },
- {
- id_pregunta_opcion: 50005,
- posicion: 2,
- id_opcion: 90005,
- opcion: {
- id_opcion: 90005,
- opcion: 'No',
- },
- },
- ],
- },
- },
- ],
- },
- {
- id_cuestionario_seccion: 2,
- posicion: 2,
- seccion: {
- id_seccion: 101,
- contador_pregunta: 1,
- descripcion: 'Preguntas adicionales para conocer más detalles',
- titulo: 'Adicionales',
- },
- preguntas: [
- {
- id_seccion_pregunta: 1002,
- posicion: 1,
- pregunta: {
- id_pregunta: 10002,
- pregunta: '¿Qué mejorarías en nuestro servicio?',
- contador_opcion: 0,
- obligatoria: false,
- id_tipo_pregunta: 2,
- id_opcion_dependiente: null,
- tipo_pregunta: {
- id_tipo: 2,
- tipo_pregunta: 'Abierta',
- },
- opciones: [],
- },
- },
- {
- id_seccion_pregunta: 1003,
- posicion: 3,
- pregunta: {
- id_pregunta: 10003,
- pregunta: '¿Qué aspectos del servicio fueron de tu agrado?',
- 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',
- },
- opciones: [
- {
- id_pregunta_opcion: 50006,
- posicion: 1,
- id_opcion: 90006,
- opcion: {
- id_opcion: 90006,
- opcion: 'Rapidez',
- },
- },
- {
- id_pregunta_opcion: 50007,
- posicion: 2,
- id_opcion: 90007,
- opcion: {
- id_opcion: 90007,
- opcion: 'Atención al cliente',
- },
- },
- {
- id_pregunta_opcion: 50008,
- posicion: 3,
- id_opcion: 90008,
- opcion: {
- id_opcion: 90008,
- opcion: 'Facilidad de uso',
- },
- },
- ],
- },
- },
- ],
- },
- ],
- },
- };
\ No newline at end of file
diff --git a/utils/load-form.js b/utils/load-form.js
deleted file mode 100644
index 9489b53..0000000
--- a/utils/load-form.js
+++ /dev/null
@@ -1,46 +0,0 @@
-// Script para cargar un formulario desde un archivo JSON
-
-const axios = require('axios');
-const fs = require('fs');
-const path = require('path');
-
-const API_URL = 'http://localhost:4200';
-
-async function loadFormFromFile(filePath) {
- try {
- // Leer el archivo JSON
- const formData = require(filePath);
-
- console.log('Enviando datos del formulario al servidor...');
- console.log('Datos:', JSON.stringify(formData, null, 2));
-
- // Enviar los datos al endpoint
- const response = await axios.post(`${API_URL}/cuestionario`, formData);
-
- console.log('Formulario creado exitosamente:');
- console.log(JSON.stringify(response.data, null, 2));
-
- return response.data;
- } catch (error) {
- console.error('Error al crear el formulario:');
- if (error.response) {
- console.error('Status:', error.response.status);
- console.error('Data:', JSON.stringify(error.response.data, null, 2));
- } else {
- console.error(error.message);
- }
- throw error;
- }
-}
-
-// Verificar si se proporcionó un argumento de archivo
-const args = process.argv.slice(2);
-const filePath = args[0] || './crear_formulario_feria';
-
-// Ejecutar la función
-loadFormFromFile(filePath)
- .then(() => console.log('Proceso completado'))
- .catch(() => {
- console.log('Proceso fallido');
- process.exit(1);
- });
\ No newline at end of file
diff --git a/utils/mandar_formulario.json b/utils/mandar_formulario.json
deleted file mode 100644
index e69de29..0000000
diff --git a/utils/respuesta_1.json b/utils/respuesta_1.json
deleted file mode 100644
index e69de29..0000000
diff --git a/utils/respuesta_2.json b/utils/respuesta_2.json
deleted file mode 100644
index e69de29..0000000
diff --git a/utils/respuesta_formulario_feria.ts b/utils/respuesta_formulario_feria.ts
deleted file mode 100644
index 0a2b5fe..0000000
--- a/utils/respuesta_formulario_feria.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-const comunidad = {
- id_formulario: 1,
- respuestas: [
- { id_pregunta: 101, valor: 'Si' }, // ¿Eres parte de la comunidad de la FES Acatlán?
- { id_pregunta: 102, valor: '123456789' }, // Numero de cuenta
- { id_pregunta: 106, valor: 'Prefiero no decirlo' },
- ],
- fecha_envio: '2025-04-02T13:45:00',
- };
-
- const externos = {
- id_formulario: 1,
- respuestas: [
- { id_pregunta: 101, valor: 'No' }, // ¿Eres parte de la comunidad de la FES Acatlán?
- { id_pregunta: 103, valor: 'Juan Carlos' }, // Nombre(s)
- { id_pregunta: 104, valor: 'Ramírez López' }, // Apellidos
- { id_pregunta: 106, valor: 'Prefiero no decirlo' }, // genero
- { id_pregunta: 107, valor: 'UAM Azcapotzalco' }, // Institución de procedencia
- ],
- fecha_envio: '2025-04-02T13:45:00',
- };
\ No newline at end of file
diff --git a/utils/test-crear-formulario.ts b/utils/test-crear-formulario.ts
deleted file mode 100644
index 62b9ada..0000000
--- a/utils/test-crear-formulario.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import { feriaSexualidad } from './crear_formulario_feria';
-import axios from 'axios';
-
-// Asumiendo que el servidor está corriendo en localhost:4200
-const API_URL = 'http://localhost:4200';
-
-async function crearFormulario() {
- try {
- const response = await axios.post(`${API_URL}/cuestionario`, feriaSexualidad);
- console.log('Formulario creado exitosamente:');
- console.log(JSON.stringify(response.data, null, 2));
- return response.data;
- } catch (error) {
- console.error('Error al crear el formulario:');
- if (error.response) {
- console.error('Status:', error.response.status);
- console.error('Data:', error.response.data);
- } else {
- console.error(error.message);
- }
- throw error;
- }
-}
-
-// Ejecutar la función
-crearFormulario()
- .then(() => console.log('Proceso completado'))
- .catch(() => console.log('Proceso fallido'));
\ No newline at end of file