Compare commits
78 Commits
mike
...
events-division
| Author | SHA1 | Date | |
|---|---|---|---|
| 14ae95c00e | |||
| bc5ddb29c7 | |||
| 94002a3e50 | |||
| 0a80d892b3 | |||
| 641514b8f8 | |||
| 7c06ed0b2c | |||
| 1732065ced | |||
| 551cc3e3b2 | |||
| e20a381ba6 | |||
| 8fe2b6cc72 | |||
| a64e4ea925 | |||
| 74de90eda8 | |||
| ae0313cd1f | |||
| 3d90c51742 | |||
| 282f921e43 | |||
| 6c85847bf4 | |||
| 7d8c9e9ef4 | |||
| d188d8bea3 | |||
| 5f302df2a7 | |||
| 63e1d3183e | |||
| d652f9f012 | |||
| 0dddfce887 | |||
| e6abd86d38 | |||
| 210cc139d9 | |||
| 4e472e9e33 | |||
| 14d191790f | |||
| f496c2d8be | |||
| 8a837eda8d | |||
| aae1e5ede3 | |||
| 0c6f5336a9 | |||
| 2a807acb87 | |||
| a8645d28e8 | |||
| f7f6d8296c | |||
| f747e146e8 | |||
| 10ba9642bd | |||
| a2e4f62135 | |||
| 283b8df276 | |||
| a48f2cb41c | |||
| 564cbd4f69 | |||
| ed7cadd6e0 | |||
| d436ca6bbc | |||
| 489965681f | |||
| 297e9913bb | |||
| daca1aa0b0 | |||
| 60127068aa | |||
| 766a380f98 | |||
| d6d2c1c0d4 | |||
| 9997faadf9 | |||
| e3b2d3cb9c | |||
| c8667dc142 | |||
| 0805cffbab | |||
| 973eb625aa | |||
| f9cdf6a593 | |||
| abbbb206cf | |||
| 91cc3f4e10 | |||
| f1ef5c0b3c | |||
| 61b2a50c65 | |||
| 6ce45d5c1a | |||
| 8d1a9d3d53 | |||
| 24e07ea2e5 | |||
| 0c8981c81d | |||
| fdeb4e557f | |||
| 4e992c5b6e | |||
| e5c6f611f4 | |||
| d34fa32672 | |||
| 521adac6bf | |||
| bc07c307cf | |||
| c8056c5957 | |||
| 8208ff0292 | |||
| 934b4e4ffe | |||
| 1e58a64cde | |||
| 5ba3f0f2a8 | |||
| f50c9f36c8 | |||
| fa6a73ea1a | |||
| 8d191fbb23 | |||
| 1e13a5fffd | |||
| 7c3aba670e | |||
| c4d4fed43d |
@@ -0,0 +1,10 @@
|
||||
PORT=4200
|
||||
|
||||
db_type=mysql
|
||||
db_host=mariadb # <-- uso del nombre del servicio, NO IP
|
||||
db_port=3306
|
||||
db_username=root # <-- te conectarás con root
|
||||
db_password=mypass # <-- la misma que MARIADB_ROOT_PASSWORD
|
||||
db_database=formularios_test
|
||||
|
||||
url_api_correos=http://host.docker.internal:4100 #http://localhost:4100
|
||||
@@ -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
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
}
|
||||
+4
-2
@@ -1,10 +1,12 @@
|
||||
# Etapa de desarrollo para ejecutar en watch mode
|
||||
FROM node:20-alpine
|
||||
FROM node:22-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
# Instalar explícitamente las dependencias de autenticación
|
||||
RUN npm i
|
||||
|
||||
COPY . .
|
||||
|
||||
@@ -12,7 +14,7 @@ COPY . .
|
||||
RUN npm run build
|
||||
|
||||
# Puerto expuesto
|
||||
EXPOSE 4200
|
||||
EXPOSE 4204
|
||||
|
||||
# Comando para iniciar en modo desarrollo
|
||||
CMD ["npm", "run", "start:dev"]
|
||||
|
||||
@@ -1,99 +1,26 @@
|
||||
<p align="center">
|
||||
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="120" alt="Nest Logo" /></a>
|
||||
</p>
|
||||
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.
|
||||
|
||||
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
|
||||
<p align="center">
|
||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
|
||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
|
||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
|
||||
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
|
||||
<a href="https://coveralls.io/github/nestjs/nest?branch=master" target="_blank"><img src="https://coveralls.io/repos/github/nestjs/nest/badge.svg?branch=master#9" alt="Coverage" /></a>
|
||||
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
|
||||
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
|
||||
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
|
||||
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg" alt="Donate us"/></a>
|
||||
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
|
||||
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow" alt="Follow us on Twitter"></a>
|
||||
</p>
|
||||
<!--[](https://opencollective.com/nest#backer)
|
||||
[](https://opencollective.com/nest#sponsor)-->
|
||||
📌 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).
|
||||
|
||||
+16
-1
@@ -11,6 +11,7 @@ services:
|
||||
MARIADB_DATABASE: formularios_test
|
||||
volumes:
|
||||
- mariadb_data:/var/lib/mysql
|
||||
- ./init-scripts:/docker-entrypoint-initdb.d # Base de datos simulada para escolares
|
||||
restart: unless-stopped
|
||||
|
||||
api:
|
||||
@@ -24,10 +25,24 @@ services:
|
||||
- .:/app
|
||||
- /app/node_modules
|
||||
ports:
|
||||
- "4200:4200"
|
||||
- "4204:4204"
|
||||
depends_on:
|
||||
- mariadb
|
||||
restart: unless-stopped
|
||||
|
||||
|
||||
|
||||
# front:
|
||||
# build: ../formularios_front
|
||||
# container_name: formularios_front
|
||||
# restart: always
|
||||
# depends_on:
|
||||
# - api
|
||||
# env_file:
|
||||
# - ../formularios_front/.env
|
||||
# ports:
|
||||
# - "4202:3000"
|
||||
|
||||
|
||||
volumes:
|
||||
mariadb_data:
|
||||
|
||||
+24
-35
@@ -1,35 +1,24 @@
|
||||
// @ts-check
|
||||
import eslint from '@eslint/js';
|
||||
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
|
||||
import globals from 'globals';
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
export default tseslint.config(
|
||||
{
|
||||
ignores: ['eslint.config.mjs'],
|
||||
},
|
||||
eslint.configs.recommended,
|
||||
...tseslint.configs.recommendedTypeChecked,
|
||||
eslintPluginPrettierRecommended,
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest,
|
||||
},
|
||||
ecmaVersion: 5,
|
||||
sourceType: 'module',
|
||||
parserOptions: {
|
||||
projectService: true,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-floating-promises': 'warn',
|
||||
'@typescript-eslint/no-unsafe-argument': 'warn'
|
||||
},
|
||||
},
|
||||
);
|
||||
module.exports = {
|
||||
// parser: '@typescript-eslint/parser',
|
||||
// parserOptions: {
|
||||
// project: 'tsconfig.json',
|
||||
// sourceType: 'module',
|
||||
// },
|
||||
// plugins: ['@typescript-eslint/eslint-plugin'],
|
||||
// extends: [
|
||||
// 'plugin:@typescript-eslint/recommended',
|
||||
// 'plugin:prettier/recommended',
|
||||
// ],
|
||||
// root: true,
|
||||
// env: {
|
||||
// node: true,
|
||||
// jest: true,
|
||||
// },
|
||||
// ignorePatterns: ['.eslintrc.js'],
|
||||
// rules: {
|
||||
// '@typescript-eslint/interface-name-prefix': 'off',
|
||||
// '@typescript-eslint/explicit-function-return-type': 'off',
|
||||
// '@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
// '@typescript-eslint/no-explicit-any': 'off',
|
||||
// },
|
||||
};
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
-- Crear la base de datos para el registro de alumnos
|
||||
CREATE DATABASE IF NOT EXISTS datos_feria_registro;
|
||||
USE datos_feria_registro;
|
||||
|
||||
-- Crear la tabla registro_alumno
|
||||
CREATE TABLE IF NOT EXISTS registro_alumno (
|
||||
id_ncuenta int(9) unsigned zerofill NOT NULL,
|
||||
nombre varchar(100) NOT NULL,
|
||||
apellidos varchar(120) NOT NULL,
|
||||
carrera varchar(100) NOT NULL,
|
||||
genero char(1) NOT NULL,
|
||||
PRIMARY KEY (id_ncuenta)
|
||||
);
|
||||
|
||||
-- Insertar algunos datos de ejemplo
|
||||
INSERT INTO registro_alumno (id_ncuenta, nombre, apellidos, carrera, genero) VALUES
|
||||
(000123456, 'Juan', 'Pérez López', 'Ingeniería en Computación', 'M'),
|
||||
(000789012, 'María', 'González Ramírez', 'Licenciatura en Administración', 'F'),
|
||||
(000456789, 'Carlos', 'Rodríguez Martínez', 'Ingeniería Civil', 'M'),
|
||||
(000345678, 'Ana', 'Sánchez Jiménez', 'Medicina', 'F'),
|
||||
(000901234, 'Pedro', 'Fernández Gómez', 'Arquitectura', 'M'),
|
||||
(316092940, 'Eithan', 'Fernández Gómez', 'Arquitectura', 'M');
|
||||
@@ -0,0 +1,14 @@
|
||||
INSERT INTO tipo_evento (tipo_evento) VALUES
|
||||
('Académico'),
|
||||
('Taller / Capacitación'),
|
||||
('Conferencia / Charla'),
|
||||
('Panel / Mesa redonda'),
|
||||
('Feria / Expo'),
|
||||
('Networking / Vinculación'),
|
||||
('Entrevista / Sesión 1:1'),
|
||||
('Cultural / Artístico'),
|
||||
('Deportivo / Recreativo'),
|
||||
('Institucional / Ceremonial'),
|
||||
('Social / Comunitario'),
|
||||
('Aniversario'),
|
||||
('Otro');
|
||||
Generated
+386
-22
@@ -12,19 +12,29 @@
|
||||
"@nestjs/common": "^11.0.12",
|
||||
"@nestjs/config": "^4.0.2",
|
||||
"@nestjs/core": "^11.0.1",
|
||||
"@nestjs/jwt": "^11.0.0",
|
||||
"@nestjs/mapped-types": "*",
|
||||
"@nestjs/passport": "^11.0.5",
|
||||
"@nestjs/platform-express": "^11.0.1",
|
||||
"@nestjs/swagger": "^11.1.0",
|
||||
"@nestjs/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",
|
||||
"@types/passport-jwt": "^4.0.1",
|
||||
"axios": "^1.8.4",
|
||||
"bcryptjs": "^3.0.2",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.1",
|
||||
"dotenv": "^16.4.7",
|
||||
"mysql2": "^3.14.0",
|
||||
"passport": "^0.7.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"qrcode": "^1.5.4",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
"typeorm": "^0.3.21"
|
||||
"typeorm": "^0.3.21",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
@@ -36,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",
|
||||
@@ -2369,6 +2380,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/jwt": {
|
||||
"version": "11.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-11.0.0.tgz",
|
||||
"integrity": "sha512-v7YRsW3Xi8HNTsO+jeHSEEqelX37TVWgwt+BcxtkG/OfXJEOs6GZdbdza200d6KqId1pJQZ6UPj1F0M6E+mxaA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/jsonwebtoken": "9.0.7",
|
||||
"jsonwebtoken": "9.0.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/mapped-types": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.1.0.tgz",
|
||||
@@ -2389,6 +2413,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/passport": {
|
||||
"version": "11.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-11.0.5.tgz",
|
||||
"integrity": "sha512-ulQX6mbjlws92PIM15Naes4F4p2JoxGnIJuUsdXQPT+Oo2sqQmENEZXM7eYuimocfHnKlcfZOuyzbA33LwUlOQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "^10.0.0 || ^11.0.0",
|
||||
"passport": "^0.5.0 || ^0.6.0 || ^0.7.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/platform-express": {
|
||||
"version": "11.0.12",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.0.12.tgz",
|
||||
@@ -2508,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",
|
||||
@@ -2519,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",
|
||||
@@ -3109,11 +3170,25 @@
|
||||
"@babel/types": "^7.20.7"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/bcrypt": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz",
|
||||
"integrity": "sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/bcryptjs": {
|
||||
"version": "2.4.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz",
|
||||
"integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/body-parser": {
|
||||
"version": "1.19.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
|
||||
"integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/connect": "*",
|
||||
@@ -3124,7 +3199,6 @@
|
||||
"version": "3.4.38",
|
||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
|
||||
"integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
@@ -3170,7 +3244,6 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.1.tgz",
|
||||
"integrity": "sha512-UZUw8vjpWFXuDnjFTh7/5c2TWDlQqeXHi6hcN7F2XSVT5P+WmUnnbFS3KA6Jnc6IsEqI2qCVu2bK0R0J4A8ZQQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/body-parser": "*",
|
||||
@@ -3182,7 +3255,6 @@
|
||||
"version": "5.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz",
|
||||
"integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
@@ -3212,7 +3284,6 @@
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz",
|
||||
"integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/istanbul-lib-coverage": {
|
||||
@@ -3260,6 +3331,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/jsonwebtoken": {
|
||||
"version": "9.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.7.tgz",
|
||||
"integrity": "sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/methods": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz",
|
||||
@@ -3271,19 +3351,56 @@
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
|
||||
"integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
|
||||
"dev": true,
|
||||
"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",
|
||||
"integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/passport": {
|
||||
"version": "1.0.17",
|
||||
"resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.17.tgz",
|
||||
"integrity": "sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/express": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/passport-jwt": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/passport-jwt/-/passport-jwt-4.0.1.tgz",
|
||||
"integrity": "sha512-Y0Ykz6nWP4jpxgEUYq8NoVZeCQPo1ZndJLfapI249g1jHChvRfZRO/LS3tqu26YgAS/laI1qx98sYGz0IalRXQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/jsonwebtoken": "*",
|
||||
"@types/passport-strategy": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/passport-strategy": {
|
||||
"version": "0.2.38",
|
||||
"resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.38.tgz",
|
||||
"integrity": "sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/express": "*",
|
||||
"@types/passport": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/qrcode": {
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.5.tgz",
|
||||
@@ -3298,21 +3415,18 @@
|
||||
"version": "6.9.18",
|
||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz",
|
||||
"integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/range-parser": {
|
||||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
|
||||
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/send": {
|
||||
"version": "0.17.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz",
|
||||
"integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/mime": "^1",
|
||||
@@ -3323,7 +3437,6 @@
|
||||
"version": "1.15.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz",
|
||||
"integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/http-errors": "*",
|
||||
@@ -3973,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",
|
||||
@@ -4394,6 +4516,15 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bcryptjs": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.2.tgz",
|
||||
"integrity": "sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog==",
|
||||
"license": "BSD-3-Clause",
|
||||
"bin": {
|
||||
"bcrypt": "bin/bcrypt"
|
||||
}
|
||||
},
|
||||
"node_modules/bin-version": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/bin-version/-/bin-version-6.0.0.tgz",
|
||||
@@ -4619,6 +4750,12 @@
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-equal-constant-time": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/buffer-from": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||
@@ -4743,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",
|
||||
@@ -4968,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",
|
||||
@@ -5173,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",
|
||||
@@ -5463,6 +5634,15 @@
|
||||
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ecdsa-sig-formatter": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
|
||||
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/ee-first": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||
@@ -6561,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",
|
||||
@@ -8132,6 +8321,49 @@
|
||||
"graceful-fs": "^4.1.6"
|
||||
}
|
||||
},
|
||||
"node_modules/jsonwebtoken": {
|
||||
"version": "9.0.2",
|
||||
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
|
||||
"integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jws": "^3.2.2",
|
||||
"lodash.includes": "^4.3.0",
|
||||
"lodash.isboolean": "^3.0.3",
|
||||
"lodash.isinteger": "^4.0.4",
|
||||
"lodash.isnumber": "^3.0.3",
|
||||
"lodash.isplainobject": "^4.0.6",
|
||||
"lodash.isstring": "^4.0.1",
|
||||
"lodash.once": "^4.0.0",
|
||||
"ms": "^2.1.1",
|
||||
"semver": "^7.5.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12",
|
||||
"npm": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/jwa": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
|
||||
"integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-equal-constant-time": "1.0.1",
|
||||
"ecdsa-sig-formatter": "1.0.11",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/jws": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
|
||||
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jwa": "^1.4.1",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/keyv": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||
@@ -8231,6 +8463,42 @@
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.includes": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isboolean": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
|
||||
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isinteger": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
|
||||
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isnumber": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
|
||||
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isplainobject": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
||||
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isstring": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
|
||||
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.memoize": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
|
||||
@@ -8245,6 +8513,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.once": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
|
||||
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/log-symbols": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
|
||||
@@ -8996,6 +9270,42 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/passport": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz",
|
||||
"integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"passport-strategy": "1.x.x",
|
||||
"pause": "0.0.1",
|
||||
"utils-merge": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/jaredhanson"
|
||||
}
|
||||
},
|
||||
"node_modules/passport-jwt": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz",
|
||||
"integrity": "sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"passport-strategy": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/passport-strategy": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz",
|
||||
"integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==",
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/path-exists": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||
@@ -9077,6 +9387,11 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/pause": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz",
|
||||
"integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg=="
|
||||
},
|
||||
"node_modules/peek-readable": {
|
||||
"version": "5.4.2",
|
||||
"resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.4.2.tgz",
|
||||
@@ -9918,7 +10233,6 @@
|
||||
"version": "7.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
|
||||
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
@@ -10275,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",
|
||||
@@ -10613,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"
|
||||
@@ -11478,7 +11804,6 @@
|
||||
"version": "6.20.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
|
||||
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/universalify": {
|
||||
@@ -11866,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",
|
||||
@@ -11977,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",
|
||||
|
||||
+14
-3
@@ -8,7 +8,7 @@
|
||||
"scripts": {
|
||||
"build": "nest build",
|
||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||
"start": "nest start",
|
||||
"start": "source ~/.nvm/nvm.sh && nvm use 22.10.0 && npm install && nest start",
|
||||
"start:dev": "nest start --watch",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
"start:prod": "node dist/main",
|
||||
@@ -23,19 +23,29 @@
|
||||
"@nestjs/common": "^11.0.12",
|
||||
"@nestjs/config": "^4.0.2",
|
||||
"@nestjs/core": "^11.0.1",
|
||||
"@nestjs/jwt": "^11.0.0",
|
||||
"@nestjs/mapped-types": "*",
|
||||
"@nestjs/passport": "^11.0.5",
|
||||
"@nestjs/platform-express": "^11.0.1",
|
||||
"@nestjs/swagger": "^11.1.0",
|
||||
"@nestjs/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",
|
||||
"@types/passport-jwt": "^4.0.1",
|
||||
"axios": "^1.8.4",
|
||||
"bcryptjs": "^3.0.2",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.1",
|
||||
"dotenv": "^16.4.7",
|
||||
"mysql2": "^3.14.0",
|
||||
"passport": "^0.7.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"qrcode": "^1.5.4",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
"typeorm": "^0.3.21"
|
||||
"typeorm": "^0.3.21",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
@@ -47,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",
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"Nombre": "Sistema de Registro Eventos",
|
||||
"ip": "192.168.0.10",
|
||||
"email": "registro-eventos@acatlan.unam.mx",
|
||||
"email_password": "c4Gl-06k9%h",
|
||||
"password": "E5cCF5b-92d",
|
||||
"clientId": "963087784448-i4pg060bkumkhk75r4po5esaglij6dq2.apps.googleusercontent.com",
|
||||
"refreshToken": "1//04-ba9LCW4EQ9CgYIARAAGAQSNwF-L9IrOlLXH2Piku5W37ujhF4N3sBMMJqTAvXhjzKiYy7ckuOoA6sIcBZpu-d-E7-no286kzU",
|
||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzaXN0ZW1hSWQiOjIsIm5vbWJyZSI6IlNpc3RlbWEgZGUgUmVnaXN0cm8gRXZlbnRvcyIsImlwIjoiMTkyLjE2OC4wLjEwIiwiaWF0IjoxNzUxMTU1OTU0LCJleHAiOjE3NTEyNDIzNTR9.9JzBnjlkw9SqTAlpN2UfsWwLS7HzeIyosfRCNA3Lj0Y%"
|
||||
}
|
||||
@@ -1,57 +1,101 @@
|
||||
import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post } 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 { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiBody } from '@nestjs/swagger';
|
||||
import { LoginAdministradorDto } from './dto/login-administrador.dto';
|
||||
import { ChangePasswordDto } from './dto/change-password.dto';
|
||||
import { ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||
import { AdministradorApiDocumentation } from './docs/administrador.documentation';
|
||||
import { JwtValidationGuard } from 'src/auth/guards/jwt-validation.guard';
|
||||
import { Roles } from 'src/auth/decorators/roles.decorator';
|
||||
import { RolesGuard } from 'src/auth/guards/roles.guard';
|
||||
|
||||
@ApiTags('Administradores') // Agrupa los endpoints en Swagger
|
||||
@AdministradorApiDocumentation.ApiController
|
||||
@ApiBearerAuth()
|
||||
@Controller('administrador')
|
||||
export class AdministradorController {
|
||||
constructor(private administradorService: AdministradorService) {}
|
||||
|
||||
constructor(private administradorService: AdministradorService) {}
|
||||
@Get()
|
||||
@UseGuards(RolesGuard)
|
||||
@UseGuards(JwtValidationGuard)
|
||||
@Roles('administrador')
|
||||
@AdministradorApiDocumentation.ApiGetAll
|
||||
getAdministradores(): Promise<Administrador[]> {
|
||||
return this.administradorService.getAdministradores();
|
||||
}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'Obtener todos los administradores' })
|
||||
@ApiResponse({ status: 200, description: 'Lista de administradores obtenida correctamente.' })
|
||||
getAdministradores(): Promise<Administrador[]> {
|
||||
return this.administradorService.getAdministradores();
|
||||
}
|
||||
@Get(':id')
|
||||
@UseGuards(RolesGuard)
|
||||
@UseGuards(JwtValidationGuard)
|
||||
@Roles('administrador')
|
||||
@AdministradorApiDocumentation.ApiGetOne
|
||||
getAdministrador(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.administradorService.getAdminByIdOrFail(id);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: 'Obtener un administrador por ID' })
|
||||
@ApiParam({ name: 'id', description: 'ID del administrador', example: 1 })
|
||||
@ApiResponse({ status: 200, description: 'Administrador obtenido correctamente.' })
|
||||
@ApiResponse({ status: 404, description: 'Administrador no encontrado.' })
|
||||
getAdministrador(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.administradorService.getAdministrador(id)
|
||||
}
|
||||
@Post()
|
||||
@Roles('administrador')
|
||||
@AdministradorApiDocumentation.ApiCreate
|
||||
createAdministrador(@Body() newAdministrador: CreateAdministradorDto) {
|
||||
return this.administradorService.createAdministrador(newAdministrador);
|
||||
}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ summary: 'Registrar un nuevo administrador' })
|
||||
@ApiBody({
|
||||
description: 'Datos del administrador a registrar',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id_tipo_user: { type: 'integer', example: 1 }
|
||||
}
|
||||
}
|
||||
})
|
||||
@ApiResponse({ status: 201, description: 'Administrador registrado exitosamente.' })
|
||||
@ApiResponse({ status: 400, description: 'Datos inválidos.' })
|
||||
createAdministrador(@Body() newAdministrador: CreateAdministradorDto) {
|
||||
return this.administradorService.createAdministrador(newAdministrador)
|
||||
}
|
||||
@Post('login')
|
||||
@AdministradorApiDocumentation.ApiLogin
|
||||
login(@Body() loginData: LoginAdministradorDto) {
|
||||
return this.administradorService.login(
|
||||
loginData.nombre_usuario,
|
||||
loginData.password,
|
||||
);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
deleteAdministrador(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.administradorService.deleteAdministrador(id)
|
||||
}
|
||||
@Patch(':id/change-password')
|
||||
@UseGuards(RolesGuard)
|
||||
@UseGuards(JwtValidationGuard)
|
||||
@Roles('administrador')
|
||||
@AdministradorApiDocumentation.ApiChangePassword
|
||||
changePassword(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Body() changePasswordDto: ChangePasswordDto,
|
||||
) {
|
||||
return this.administradorService.changePassword(
|
||||
id,
|
||||
changePasswordDto.currentPassword,
|
||||
changePasswordDto.newPassword,
|
||||
);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
updateAdministrador(@Param('id', ParseIntPipe) id: number, @Body() administrador: UpdateAdministradorDto) {
|
||||
return this.administradorService.updateAdministrador(id, administrador)
|
||||
}
|
||||
@Delete(':id')
|
||||
@UseGuards(RolesGuard)
|
||||
@UseGuards(JwtValidationGuard)
|
||||
@Roles('administrador')
|
||||
@AdministradorApiDocumentation.ApiRemove
|
||||
deleteAdministrador(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.administradorService.deleteAdministrador(id);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@UseGuards(RolesGuard)
|
||||
@UseGuards(JwtValidationGuard)
|
||||
@Roles('administrador')
|
||||
@AdministradorApiDocumentation.ApiUpdate
|
||||
updateAdministrador(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Body() administrador: UpdateAdministradorDto,
|
||||
) {
|
||||
return this.administradorService.updateAdministrador(id, administrador);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +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
|
||||
|
||||
/*
|
||||
@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[];
|
||||
*/
|
||||
}
|
||||
@@ -2,10 +2,14 @@ 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({
|
||||
imports: [TypeOrmModule.forFeature([Administrador])],
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([Administrador]),
|
||||
AuthModule
|
||||
],
|
||||
controllers: [AdministradorController],
|
||||
providers: [AdministradorService],
|
||||
exports: [AdministradorService],
|
||||
|
||||
@@ -1,79 +1,218 @@
|
||||
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';
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
|
||||
@Injectable()
|
||||
export class AdministradorService {
|
||||
|
||||
constructor(
|
||||
@InjectRepository(Administrador) private administradorRepository: Repository<Administrador>
|
||||
) {}
|
||||
constructor(
|
||||
@InjectRepository(Administrador)
|
||||
private administradorRepository: Repository<Administrador>,
|
||||
private jwtService: JwtService,
|
||||
) {}
|
||||
|
||||
async createAdministrador(administrador: CreateAdministradorDto) {
|
||||
async createAdministrador(administrador: CreateAdministradorDto) {
|
||||
// Verificar si ya existe un administrador con el mismo correo
|
||||
const administradorFound = await this.administradorRepository.findOne({
|
||||
where: {
|
||||
correo: administrador.correo,
|
||||
},
|
||||
});
|
||||
|
||||
//revisar el where
|
||||
const administradorFound = await this.administradorRepository.findOne({
|
||||
where: {
|
||||
id_admnistrador: administrador.id_tipo_user
|
||||
}
|
||||
})
|
||||
|
||||
if (administradorFound) {
|
||||
return new HttpException('Administrador already exists', HttpStatus.CONFLICT)
|
||||
}
|
||||
|
||||
|
||||
//Falta regresar el return
|
||||
//return this.administradorRepository.save(administradorFound)
|
||||
if (administradorFound) {
|
||||
throw new HttpException(
|
||||
'El correo ya está registrado',
|
||||
HttpStatus.CONFLICT,
|
||||
);
|
||||
}
|
||||
|
||||
getAdministradores() {
|
||||
return this.administradorRepository.find({
|
||||
relations: ['tipoUser']
|
||||
})
|
||||
// Encriptar la contraseña
|
||||
const hashedPassword = await this.hashPassword(administrador.password);
|
||||
|
||||
// Crear y guardar el nuevo administrador
|
||||
const newAdministrador = this.administradorRepository.create({
|
||||
nombre_usuario: administrador.nombre_usuario,
|
||||
correo: administrador.correo,
|
||||
password: hashedPassword,
|
||||
tipoUser: { id_tipo_user: administrador.id_tipo_user }, // Asignar el tipo de usuario
|
||||
});
|
||||
|
||||
const savedAdministrador =
|
||||
await this.administradorRepository.save(newAdministrador);
|
||||
|
||||
// Excluir la contraseña de la respuesta
|
||||
const { password, ...result } = savedAdministrador;
|
||||
return result;
|
||||
}
|
||||
|
||||
async login(nombre_usuario: string, password: string) {
|
||||
// Buscar administrador por nombre de usuario
|
||||
const administrador = await this.administradorRepository.findOne({
|
||||
where: { nombre_usuario },
|
||||
relations: ['tipoUser'],
|
||||
});
|
||||
|
||||
if (!administrador) {
|
||||
throw new HttpException(
|
||||
'Credenciales inválidas',
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
);
|
||||
}
|
||||
|
||||
async getAdministrador(id_admnistrador: number) {
|
||||
const administradorFound = await this.administradorRepository.findOne({
|
||||
where: {
|
||||
id_admnistrador
|
||||
},
|
||||
relations: ['tipoUser']
|
||||
})
|
||||
|
||||
if (!Administrador) {
|
||||
return new HttpException('User not found', HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
return administradorFound
|
||||
// Verificar contraseña
|
||||
const isPasswordValid = await bcrypt.compare(
|
||||
password,
|
||||
administrador.password,
|
||||
);
|
||||
if (!isPasswordValid) {
|
||||
throw new HttpException(
|
||||
'Credenciales inválidas',
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
);
|
||||
}
|
||||
|
||||
async deleteAdministrador(id_admnistrador: number) {
|
||||
const result = await this.administradorRepository.delete({ id_admnistrador })
|
||||
// Generar token JWT
|
||||
const payload = {
|
||||
sub: administrador.id_administrador,
|
||||
correo: administrador.correo,
|
||||
};
|
||||
|
||||
if (result.affected === 0) {
|
||||
return new HttpException('User not found', HttpStatus.NOT_FOUND);
|
||||
}
|
||||
return {
|
||||
token: this.jwtService.sign(payload),
|
||||
tipo_usuario: administrador.tipoUser.tipo,
|
||||
};
|
||||
}
|
||||
|
||||
return result
|
||||
async changePassword(
|
||||
id_administrador: number,
|
||||
currentPassword: string,
|
||||
newPassword: string,
|
||||
) {
|
||||
// Buscar administrador
|
||||
const administrador = await this.administradorRepository.findOne({
|
||||
where: { id_administrador },
|
||||
});
|
||||
|
||||
if (!administrador) {
|
||||
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
|
||||
}
|
||||
})
|
||||
|
||||
if (!administradorFound) {
|
||||
return new HttpException('User not found', HttpStatus.NOT_FOUND)
|
||||
}
|
||||
|
||||
const updateAdministrador = Object.assign(administradorFound, administrador)
|
||||
return this.administradorRepository.save(updateAdministrador)
|
||||
// Verificar contraseña actual
|
||||
const isPasswordValid = await bcrypt.compare(
|
||||
currentPassword,
|
||||
administrador.password,
|
||||
);
|
||||
if (!isPasswordValid) {
|
||||
throw new HttpException(
|
||||
'Contraseña actual incorrecta',
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
// Hashear y actualizar nueva contraseña
|
||||
administrador.password = await this.hashPassword(newPassword);
|
||||
await this.administradorRepository.save(administrador);
|
||||
|
||||
return { message: 'Contraseña actualizada correctamente' };
|
||||
}
|
||||
|
||||
private async hashPassword(password: string): Promise<string> {
|
||||
const salt = await bcrypt.genSalt();
|
||||
return bcrypt.hash(password, salt);
|
||||
}
|
||||
|
||||
getAdministradores() {
|
||||
return this.administradorRepository.find({
|
||||
relations: ['tipoUser'],
|
||||
select: {
|
||||
nombre_usuario: true,
|
||||
id_administrador: true,
|
||||
correo: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getAdminByIdOrFail(id_administrador: number) {
|
||||
const administradorFound = await this.administradorRepository.findOne({
|
||||
where: {
|
||||
id_administrador,
|
||||
},
|
||||
});
|
||||
|
||||
if (!administradorFound) {
|
||||
throw new HttpException(
|
||||
'Administrador no encontrado',
|
||||
HttpStatus.NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
return administradorFound;
|
||||
}
|
||||
|
||||
async getAdminById(id_administrador: number) {
|
||||
const administradorFound = await this.administradorRepository.findOne({
|
||||
where: {
|
||||
id_administrador,
|
||||
},
|
||||
relations: ['tipoUser'],
|
||||
});
|
||||
|
||||
return administradorFound;
|
||||
}
|
||||
|
||||
async deleteAdministrador(id_administrador: number) {
|
||||
const result = await this.administradorRepository.delete({
|
||||
id_administrador,
|
||||
});
|
||||
|
||||
if (result.affected === 0) {
|
||||
throw new HttpException(
|
||||
'Administrador no encontrado',
|
||||
HttpStatus.NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async updateAdministrador(
|
||||
id_administrador: number,
|
||||
administrador: UpdateAdministradorDto,
|
||||
) {
|
||||
const administradorFound = await this.administradorRepository.findOne({
|
||||
where: {
|
||||
id_administrador,
|
||||
},
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,346 @@
|
||||
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 = 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',
|
||||
}),
|
||||
ApiBody({
|
||||
description: 'Datos del administrador a registrar',
|
||||
schema: {
|
||||
type: 'object',
|
||||
required: ['correo', 'password', 'id_tipo_user'],
|
||||
properties: {
|
||||
nombre_usuario: {
|
||||
type: 'string',
|
||||
example: 'mike',
|
||||
description: 'Nombre de usuario del administrador',
|
||||
},
|
||||
correo: {
|
||||
type: 'string',
|
||||
format: 'email',
|
||||
example: 'admin@ejemplo.com',
|
||||
description: 'Correo electrónico del administrador',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
format: 'password',
|
||||
example: 'password',
|
||||
description: 'Contraseña del administrador (mínimo 6 caracteres)',
|
||||
},
|
||||
id_tipo_user: {
|
||||
type: 'integer',
|
||||
example: 1,
|
||||
description: 'ID del tipo de usuario administrador',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
ApiResponse({
|
||||
status: 201,
|
||||
description: 'Administrador registrado exitosamente',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id_admnistrador: { type: 'number', example: 1 },
|
||||
correo: { type: 'string', example: 'admin@ejemplo.com' },
|
||||
id_tipo_user: { type: 'number', example: 1 },
|
||||
},
|
||||
},
|
||||
}),
|
||||
ApiResponse({
|
||||
status: 400,
|
||||
description: 'Datos del administrador inválidos',
|
||||
}),
|
||||
ApiResponse({ status: 409, description: 'El correo ya está registrado' }),
|
||||
);
|
||||
|
||||
// Documentación para iniciar sesión
|
||||
static ApiLogin = applyDecorators(
|
||||
ApiOperation({
|
||||
summary: 'Iniciar sesión como administrador',
|
||||
description:
|
||||
'Autentica un administrador con correo y contraseña, y devuelve un token JWT',
|
||||
}),
|
||||
ApiBody({
|
||||
description: 'Credenciales de inicio de sesión',
|
||||
schema: {
|
||||
type: 'object',
|
||||
required: ['correo', 'password'],
|
||||
properties: {
|
||||
correo: {
|
||||
type: 'string',
|
||||
format: 'email',
|
||||
example: 'admin@ejemplo.com',
|
||||
description: 'Correo electrónico del administrador',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
format: 'password',
|
||||
example: 'password',
|
||||
description: 'Contraseña del administrador',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
ApiResponse({
|
||||
status: 200,
|
||||
description: 'Inicio de sesión exitoso',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
access_token: {
|
||||
type: 'string',
|
||||
example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
|
||||
description: 'Token JWT para autenticación',
|
||||
},
|
||||
id_administrador: {
|
||||
type: 'number',
|
||||
example: 1,
|
||||
description: 'ID del administrador autenticado',
|
||||
},
|
||||
correo: {
|
||||
type: 'string',
|
||||
example: 'admin@ejemplo.com',
|
||||
description: 'Correo del administrador autenticado',
|
||||
},
|
||||
tipo_user: {
|
||||
type: 'number',
|
||||
example: 1,
|
||||
description: 'Tipo de usuario del administrador',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
ApiResponse({ status: 401, description: 'Credenciales inválidas' }),
|
||||
);
|
||||
|
||||
// Documentación para obtener todos los administradores
|
||||
static ApiGetAll = applyDecorators(
|
||||
ApiOperation({
|
||||
summary: 'Obtener todos los administradores',
|
||||
description:
|
||||
'Retorna una lista de todos los administradores registrados (requiere autenticación)',
|
||||
}),
|
||||
ApiResponse({
|
||||
status: 200,
|
||||
description: 'Lista de administradores obtenida correctamente',
|
||||
schema: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id_admnistrador: { type: 'number', example: 1 },
|
||||
correo: { type: 'string', example: 'admin@ejemplo.com' },
|
||||
id_tipo_user: { type: 'number', example: 1 },
|
||||
tipoUser: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id_tipo_user: { type: 'number', example: 1 },
|
||||
tipo_user: {
|
||||
type: 'string',
|
||||
example: 'Administrador General',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
ApiResponse({ status: 401, description: 'No autorizado' }),
|
||||
);
|
||||
|
||||
// Documentación para obtener un administrador por ID
|
||||
static ApiGetOne = applyDecorators(
|
||||
ApiOperation({
|
||||
summary: 'Obtener un administrador por ID',
|
||||
description:
|
||||
'Retorna los datos de un administrador específico según su ID (requiere autenticación)',
|
||||
}),
|
||||
ApiParam({
|
||||
name: 'id',
|
||||
description: 'ID del administrador',
|
||||
type: 'number',
|
||||
example: 1,
|
||||
}),
|
||||
ApiResponse({
|
||||
status: 200,
|
||||
description: 'Administrador obtenido correctamente',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id_admnistrador: { type: 'number', example: 1 },
|
||||
correo: { type: 'string', example: 'admin@ejemplo.com' },
|
||||
id_tipo_user: { type: 'number', example: 1 },
|
||||
tipoUser: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id_tipo_user: { type: 'number', example: 1 },
|
||||
tipo_user: { type: 'string', example: 'Administrador General' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
ApiResponse({ status: 404, description: 'Administrador no encontrado' }),
|
||||
ApiResponse({ status: 401, description: 'No autorizado' }),
|
||||
);
|
||||
|
||||
// Documentación para actualizar un administrador
|
||||
static ApiUpdate = applyDecorators(
|
||||
ApiOperation({
|
||||
summary: 'Actualizar datos de un administrador',
|
||||
description:
|
||||
'Actualiza la información de un administrador existente (requiere autenticación)',
|
||||
}),
|
||||
ApiParam({
|
||||
name: 'id',
|
||||
description: 'ID del administrador a actualizar',
|
||||
type: 'number',
|
||||
example: 1,
|
||||
}),
|
||||
ApiBody({
|
||||
description: 'Datos a actualizar del administrador',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
correo: {
|
||||
type: 'string',
|
||||
format: 'email',
|
||||
example: 'nuevo_admin@ejemplo.com',
|
||||
description: 'Nuevo correo electrónico del administrador',
|
||||
},
|
||||
password: {
|
||||
type: 'string',
|
||||
format: 'password',
|
||||
example: 'NuevaPassword123',
|
||||
description:
|
||||
'Nueva contraseña del administrador (mínimo 6 caracteres)',
|
||||
},
|
||||
id_tipo_user: {
|
||||
type: 'integer',
|
||||
example: 2,
|
||||
description: 'Nuevo tipo de usuario administrador',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
ApiResponse({
|
||||
status: 200,
|
||||
description: 'Administrador actualizado correctamente',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id_admnistrador: { type: 'number', example: 1 },
|
||||
correo: { type: 'string', example: 'nuevo_admin@ejemplo.com' },
|
||||
id_tipo_user: { type: 'number', example: 2 },
|
||||
},
|
||||
},
|
||||
}),
|
||||
ApiResponse({
|
||||
status: 400,
|
||||
description: 'Datos de actualización inválidos',
|
||||
}),
|
||||
ApiResponse({ status: 404, description: 'Administrador no encontrado' }),
|
||||
ApiResponse({ status: 401, description: 'No autorizado' }),
|
||||
);
|
||||
|
||||
// Documentación para cambiar contraseña
|
||||
static ApiChangePassword = applyDecorators(
|
||||
ApiOperation({
|
||||
summary: 'Cambiar contraseña de un administrador',
|
||||
description:
|
||||
'Permite a un administrador cambiar su contraseña verificando primero la contraseña actual (requiere autenticación)',
|
||||
}),
|
||||
ApiParam({
|
||||
name: 'id',
|
||||
description: 'ID del administrador',
|
||||
type: 'number',
|
||||
example: 1,
|
||||
}),
|
||||
ApiBody({
|
||||
description: 'Datos para cambio de contraseña',
|
||||
schema: {
|
||||
type: 'object',
|
||||
required: ['currentPassword', 'newPassword'],
|
||||
properties: {
|
||||
currentPassword: {
|
||||
type: 'string',
|
||||
format: 'password',
|
||||
example: 'Password123',
|
||||
description: 'Contraseña actual del administrador',
|
||||
},
|
||||
newPassword: {
|
||||
type: 'string',
|
||||
format: 'password',
|
||||
example: 'NuevaPassword123',
|
||||
description:
|
||||
'Nueva contraseña del administrador (mínimo 6 caracteres)',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
ApiResponse({
|
||||
status: 200,
|
||||
description: 'Contraseña actualizada correctamente',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
message: {
|
||||
type: 'string',
|
||||
example: 'Contraseña actualizada correctamente',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
ApiResponse({ status: 400, description: 'Contraseña actual incorrecta' }),
|
||||
ApiResponse({ status: 404, description: 'Administrador no encontrado' }),
|
||||
ApiResponse({ status: 401, description: 'No autorizado' }),
|
||||
);
|
||||
|
||||
// Documentación para eliminar un administrador
|
||||
static ApiRemove = applyDecorators(
|
||||
ApiOperation({
|
||||
summary: 'Eliminar un administrador',
|
||||
description:
|
||||
'Elimina permanentemente un administrador por su ID (requiere autenticación)',
|
||||
}),
|
||||
ApiParam({
|
||||
name: 'id',
|
||||
description: 'ID del administrador a eliminar',
|
||||
type: 'number',
|
||||
example: 1,
|
||||
}),
|
||||
ApiResponse({
|
||||
status: 200,
|
||||
description: 'Administrador eliminado correctamente',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
affected: { type: 'number', example: 1 },
|
||||
},
|
||||
},
|
||||
}),
|
||||
ApiResponse({ status: 404, description: 'Administrador no encontrado' }),
|
||||
ApiResponse({ status: 401, description: 'No autorizado' }),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { IsNotEmpty, MinLength } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class ChangePasswordDto {
|
||||
@ApiProperty({ description: 'Contraseña actual del administrador' })
|
||||
@IsNotEmpty({ message: 'La contraseña actual es requerida' })
|
||||
currentPassword: string;
|
||||
|
||||
@ApiProperty({ description: 'Nueva contraseña del administrador' })
|
||||
@MinLength(6, { message: 'La nueva contraseña debe tener al menos 6 caracteres' })
|
||||
@IsNotEmpty({ message: 'La nueva contraseña es requerida' })
|
||||
newPassword: string;
|
||||
}
|
||||
@@ -1,3 +1,32 @@
|
||||
import { IsEmail, IsNotEmpty, IsNumber, MinLength } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class CreateAdministradorDto {
|
||||
id_tipo_user: number
|
||||
}
|
||||
@ApiProperty({
|
||||
example: 'admin@example.com',
|
||||
description: 'Correo del administrador',
|
||||
})
|
||||
@IsEmail({}, { message: 'El correo no es válido' })
|
||||
@IsNotEmpty({ message: 'El correo es requerido' })
|
||||
correo: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: 'mike',
|
||||
description: 'Nombre de usuario del administrador',
|
||||
})
|
||||
@IsNotEmpty({ message: 'El nombre de usuario es requerido' })
|
||||
nombre_usuario: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: 'Password123',
|
||||
description: 'Contraseña del administrador',
|
||||
})
|
||||
@MinLength(6, { message: 'La contraseña debe tener al menos 6 caracteres' })
|
||||
@IsNotEmpty({ message: 'La contraseña es requerida' })
|
||||
password: string;
|
||||
|
||||
@ApiProperty({ example: 1, description: 'ID del tipo de usuario' })
|
||||
@IsNumber({}, { message: 'El tipo de usuario debe ser un número' })
|
||||
@IsNotEmpty({ message: 'El tipo de usuario es requerido' })
|
||||
id_tipo_user: number;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { IsEmail, IsNotEmpty } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class LoginAdministradorDto {
|
||||
@ApiProperty({
|
||||
example: 'mike',
|
||||
description: 'Nombre de usuario del administrador',
|
||||
})
|
||||
@IsNotEmpty({ message: 'El nombre de usuario es requerido' })
|
||||
nombre_usuario: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: 'Password123',
|
||||
description: 'Contraseña del administrador',
|
||||
})
|
||||
@IsNotEmpty({ message: 'La contraseña es requerida' })
|
||||
password: string;
|
||||
}
|
||||
@@ -1,3 +1,19 @@
|
||||
import { IsEmail, IsNumber, IsOptional, MinLength } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class UpdateAdministradorDto {
|
||||
id_tipo_user?: number
|
||||
@ApiProperty({ example: 'admin@example.com', description: 'Correo del administrador', required: false })
|
||||
@IsEmail({}, { message: 'El correo no es válido' })
|
||||
@IsOptional()
|
||||
correo?: string;
|
||||
|
||||
@ApiProperty({ example: 'Password123', description: 'Contraseña del administrador', required: false })
|
||||
@MinLength(6, { message: 'La contraseña debe tener al menos 6 caracteres' })
|
||||
@IsOptional()
|
||||
password?: string;
|
||||
|
||||
@ApiProperty({ example: 1, description: 'ID del tipo de usuario', required: false })
|
||||
@IsNumber({}, { message: 'El tipo de usuario debe ser un número' })
|
||||
@IsOptional()
|
||||
id_tipo_user?: number;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { TipoUser } from 'src/tipo_user/entities/tipo_user.entity';
|
||||
import {
|
||||
Column,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
@Entity('administrador')
|
||||
export class Administrador {
|
||||
@PrimaryGeneratedColumn()
|
||||
id_administrador: number;
|
||||
|
||||
@Column({ unique: true })
|
||||
nombre_usuario: string;
|
||||
|
||||
@Column({ unique: true })
|
||||
correo: string;
|
||||
|
||||
@Column()
|
||||
password: string;
|
||||
|
||||
@Column({ type: 'int' })
|
||||
id_tipo_user: number;
|
||||
|
||||
@ManyToOne(() => TipoUser, (tipoUser) => tipoUser.administrador)
|
||||
@JoinColumn({ name: 'id_tipo_user' })
|
||||
tipoUser: TipoUser;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Controller, Get, Param, NotFoundException } from '@nestjs/common';
|
||||
import { AlumnosService, UsuarioData } from './alumnos.service';
|
||||
import { ApiTags, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger';
|
||||
import { AlumnoDto, ALUMNO_RESPONSES } from './alumnos.documentation';
|
||||
|
||||
@ApiTags('alumnos')
|
||||
@Controller('alumnos')
|
||||
export class AlumnosController {
|
||||
constructor(private readonly alumnosService: AlumnosService) {}
|
||||
|
||||
@Get(':cuenta')
|
||||
@ApiOperation({ summary: 'Obtener datos de un alumno por su número de cuenta' })
|
||||
@ApiParam({ name: 'cuenta', description: 'Número de cuenta del alumno' })
|
||||
@ApiResponse(ALUMNO_RESPONSES[200])
|
||||
@ApiResponse(ALUMNO_RESPONSES[404])
|
||||
@ApiResponse(ALUMNO_RESPONSES[500])
|
||||
async getAlumnoByCuenta(@Param('cuenta') cuenta: string): Promise<UsuarioData | null> {
|
||||
const alumno = await this.alumnosService.findByCuenta(cuenta);
|
||||
if (!alumno) {
|
||||
throw new NotFoundException(`No se encontró alumno con número de cuenta ${cuenta}`);
|
||||
}
|
||||
return alumno;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class AlumnoDto {
|
||||
@ApiProperty({
|
||||
example: 123456789,
|
||||
description: 'Número de cuenta del alumno (9 dígitos)',
|
||||
})
|
||||
id_ncuenta: number;
|
||||
|
||||
@ApiProperty({
|
||||
example: 'Juan',
|
||||
description: 'Nombre del alumno',
|
||||
})
|
||||
nombre: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: 'Pérez López',
|
||||
description: 'Apellidos del alumno',
|
||||
})
|
||||
apellidos: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: 'Ingeniería en Computación',
|
||||
description: 'Carrera que cursa el alumno',
|
||||
})
|
||||
carrera: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: 'M',
|
||||
description: 'Género del alumno (M: Masculino, F: Femenino)',
|
||||
enum: ['M', 'F'],
|
||||
})
|
||||
genero: string;
|
||||
}
|
||||
|
||||
export const ALUMNO_RESPONSES = {
|
||||
200: {
|
||||
description: 'Datos del alumno obtenidos correctamente',
|
||||
type: AlumnoDto,
|
||||
},
|
||||
404: {
|
||||
description: 'Alumno no encontrado',
|
||||
},
|
||||
500: {
|
||||
description: 'Error interno del servidor',
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { AlumnosController } from './alumnos.controller';
|
||||
import { AlumnosService } from './alumnos.service';
|
||||
import { RegistroAlumno } from './entities/registro-alumno.entity';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([RegistroAlumno], 'alumnosConnection')],
|
||||
controllers: [AlumnosController],
|
||||
providers: [AlumnosService],
|
||||
exports: [AlumnosService],
|
||||
})
|
||||
export class AlumnosModule {}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
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(
|
||||
@InjectRepository(RegistroAlumno, 'alumnosConnection')
|
||||
private registroAlumnoRepository: Repository<RegistroAlumno>,
|
||||
) {}
|
||||
|
||||
async findByCuenta(cuenta: string): Promise<UsuarioData | null> {
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Entity, Column, PrimaryColumn } from 'typeorm';
|
||||
|
||||
@Entity('registro_alumno')
|
||||
export class RegistroAlumno {
|
||||
@PrimaryColumn({ type: 'int', unsigned: true, zerofill: true, width: 9 })
|
||||
id_ncuenta: number;
|
||||
|
||||
@Column({ type: 'varchar', length: 100 })
|
||||
nombre: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 120 })
|
||||
apellidos: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 100 })
|
||||
carrera: string;
|
||||
|
||||
@Column({ type: 'char', length: 1 })
|
||||
genero: string;
|
||||
}
|
||||
+42
-50
@@ -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,69 +19,56 @@ 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 { TrabajadoresModule } from './trabajadores/trabajadores.module';
|
||||
import { TipoEventoModule } from './tipo_evento/tipo_evento.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot(),
|
||||
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: true,
|
||||
dropSchema: true, // 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
|
||||
ConfigModule.forRoot({ isGlobal: true }),
|
||||
TypeOrmModule.forRootAsync({
|
||||
imports: [ConfigModule],
|
||||
useFactory: createMainDbConfig,
|
||||
inject: [ConfigService],
|
||||
}),
|
||||
|
||||
TypeOrmModule.forRootAsync({
|
||||
imports: [ConfigModule],
|
||||
name: 'alumnosConnection',
|
||||
useFactory: createAlumnosDbConfig,
|
||||
inject: [ConfigService],
|
||||
}),
|
||||
CuestionarioModule,
|
||||
TipoCuestionarioModule,
|
||||
CuestionarioSeccionModule,
|
||||
SeccionModule,
|
||||
SeccionPreguntaModule,
|
||||
PreguntaModule,
|
||||
TipoPreguntaModule,
|
||||
OpcionModule, PreguntaOpcionModule,
|
||||
RespuestaParticipanteCerradaModule,
|
||||
CuestionarioRespondidoModule,
|
||||
RespuestaParticipanteAbiertaModule,
|
||||
EventoModule,
|
||||
TipoUserModule,
|
||||
ParticipanteModule,
|
||||
QrModule,
|
||||
AdministradorModule,
|
||||
AlumnosModule,
|
||||
AsistenciaModule,
|
||||
AuthModule,
|
||||
CuestionarioModule,
|
||||
CuestionarioRespondidoModule,
|
||||
CuestionarioSeccionModule,
|
||||
EventoModule,
|
||||
OpcionModule,
|
||||
ParticipanteEventoModule,
|
||||
ParticipanteModule,
|
||||
PreguntaModule,
|
||||
PreguntaOpcionModule,
|
||||
RespuestaParticipanteAbiertaModule,
|
||||
RespuestaParticipanteCerradaModule,
|
||||
SeccionModule,
|
||||
SeccionPreguntaModule,
|
||||
TipoCuestionarioModule,
|
||||
TipoEventoModule,
|
||||
TipoPreguntaModule,
|
||||
TipoUserModule,
|
||||
ValidacionesModule,
|
||||
QrModule,
|
||||
TrabajadoresModule,
|
||||
],
|
||||
|
||||
controllers: [AppController],
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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;
|
||||
*/
|
||||
}
|
||||
@@ -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])],
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { PassportModule } from '@nestjs/passport';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
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.registerAsync({
|
||||
imports: [ConfigModule],
|
||||
inject: [ConfigService],
|
||||
useFactory: (configService: ConfigService) => ({
|
||||
secret: configService.get<string>('JWT_SECRET', 'tu_clave_secreta'),
|
||||
signOptions: {
|
||||
expiresIn: '24h',
|
||||
},
|
||||
}),
|
||||
}),
|
||||
TypeOrmModule.forFeature([Administrador]),
|
||||
],
|
||||
providers: [JwtStrategy],
|
||||
exports: [PassportModule, JwtModule],
|
||||
})
|
||||
export class AuthModule {}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
|
||||
export const ROLES_KEY = 'roles';
|
||||
export const Roles = (...roles: ('administrador' | 'staff')[]) =>
|
||||
SetMetadata(ROLES_KEY, roles);
|
||||
@@ -0,0 +1,52 @@
|
||||
import {
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
Injectable,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { AdministradorService } from 'src/administrador/administrador.service';
|
||||
|
||||
@Injectable()
|
||||
export class JwtValidationGuard implements CanActivate {
|
||||
constructor(
|
||||
private jwtService: JwtService,
|
||||
private administradorService: AdministradorService,
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const authHeader = request.headers.authorization;
|
||||
|
||||
if (!authHeader) {
|
||||
throw new UnauthorizedException('Token no proporcionado');
|
||||
}
|
||||
|
||||
const token = authHeader.split(' ')[1];
|
||||
|
||||
try {
|
||||
const decodedToken = this.jwtService.verify(token, {
|
||||
secret: process.env.JWT_SECRET,
|
||||
});
|
||||
|
||||
const administrador = await this.administradorService.getAdminById(
|
||||
decodedToken.sub,
|
||||
);
|
||||
|
||||
if (!administrador) {
|
||||
throw new UnauthorizedException('Usuario no encontrado');
|
||||
}
|
||||
|
||||
request.user = administrador;
|
||||
} catch (error) {
|
||||
if (error.name === 'TokenExpiredError') {
|
||||
throw new UnauthorizedException(
|
||||
`La sesión ha expirado, inicia sesión nuevamente.`,
|
||||
);
|
||||
}
|
||||
throw new UnauthorizedException('Token inválido');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import {
|
||||
Injectable,
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
ForbiddenException,
|
||||
} from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { ROLES_KEY } from '../decorators/roles.decorator';
|
||||
|
||||
@Injectable()
|
||||
export class RolesGuard implements CanActivate {
|
||||
constructor(private reflector: Reflector) {}
|
||||
|
||||
canActivate(context: ExecutionContext): boolean {
|
||||
const requiredRoles = this.reflector.get<string[]>(
|
||||
ROLES_KEY,
|
||||
context.getHandler(),
|
||||
);
|
||||
if (!requiredRoles) return true; // Si no hay roles definidos, permitir acceso.
|
||||
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const user = request.user; // Usuario ya validado en JwtValidationGuard
|
||||
|
||||
if (!user) {
|
||||
throw new ForbiddenException('Usuario no autenticado');
|
||||
}
|
||||
|
||||
// Obtener el tipo de usuario
|
||||
const tipo_usuario = user.tipoUser.tipo;
|
||||
|
||||
if (!tipo_usuario) {
|
||||
throw new ForbiddenException('No se pudo determinar el tipo de usuario');
|
||||
}
|
||||
|
||||
// Validar si el usuario tiene uno de los roles requeridos
|
||||
const hasRole = requiredRoles.includes(tipo_usuario);
|
||||
|
||||
if (!hasRole) {
|
||||
throw new ForbiddenException(
|
||||
'No tienes permisos para acceder a este recurso',
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
|
||||
@Injectable()
|
||||
export class JwtAuthGuard extends AuthGuard('jwt') {}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||
import { Repository } from 'typeorm';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Administrador } from '../administrador/entities/administrador.entity';
|
||||
|
||||
@Injectable()
|
||||
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
constructor(
|
||||
@InjectRepository(Administrador)
|
||||
private administradorRepository: Repository<Administrador>,
|
||||
) {
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
ignoreExpiration: false,
|
||||
secretOrKey: process.env.JWT_SECRET || 'tu_clave_secreta',
|
||||
});
|
||||
}
|
||||
|
||||
async validate(payload: any) {
|
||||
const { sub: id_administrador } = payload;
|
||||
|
||||
const administrador = await this.administradorRepository.findOne({
|
||||
where: { id_administrador },
|
||||
select: {
|
||||
id_administrador: true,
|
||||
correo: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!administrador) {
|
||||
throw new UnauthorizedException('Token inválido');
|
||||
}
|
||||
|
||||
return administrador;
|
||||
}
|
||||
}
|
||||
@@ -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>(CuestionarioController);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(controller).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -1,9 +1,23 @@
|
||||
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 { 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,18 +30,68 @@ 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() {
|
||||
return this.cuestionarioService.findAll();
|
||||
}
|
||||
|
||||
@Get('recientes')
|
||||
@CuestionarioApiDocumentation.ApiGetAll
|
||||
findAllRecientes() {
|
||||
return this.cuestionarioService.findAllRecientes();
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@CuestionarioApiDocumentation.ApiGetOne
|
||||
findOne(@Param('id') id: string) {
|
||||
return this.cuestionarioService.findOne(+id);
|
||||
}
|
||||
|
||||
|
||||
@Get(':id/formulario')
|
||||
@CuestionarioApiDocumentation.ApiGetFormulario
|
||||
findFormulario(@Param('id') id: string) {
|
||||
@@ -36,7 +100,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);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ 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/entities/evento.entity';
|
||||
import { EventoService } from '../evento/evento.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -23,11 +25,12 @@ import { TipoCuestionario } from '../tipo_cuestionario/entities/tipo_cuestionari
|
||||
Opcion,
|
||||
PreguntaOpcion,
|
||||
TipoPregunta,
|
||||
TipoCuestionario
|
||||
TipoCuestionario,
|
||||
Evento
|
||||
])
|
||||
],
|
||||
controllers: [CuestionarioController],
|
||||
providers: [CuestionarioService],
|
||||
providers: [CuestionarioService, EventoService],
|
||||
exports: [TypeOrmModule, CuestionarioService]
|
||||
})
|
||||
export class CuestionarioModule {}
|
||||
|
||||
@@ -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>(CuestionarioService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -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 { Repository, DataSource, In, MoreThan } from 'typeorm';
|
||||
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,8 +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 {
|
||||
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 {
|
||||
@@ -32,10 +44,16 @@ export class CuestionarioService {
|
||||
private preguntaOpcionRepository: Repository<PreguntaOpcion>,
|
||||
@InjectRepository(TipoPregunta)
|
||||
private tipoPreguntaRepository: Repository<TipoPregunta>,
|
||||
@InjectRepository(Evento)
|
||||
private eventoRepository: Repository<Evento>,
|
||||
private dataSource: DataSource,
|
||||
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();
|
||||
@@ -45,85 +63,101 @@ export class CuestionarioService {
|
||||
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.id_tipo_evento = createCuestionarioDto.id_tipo_evento;
|
||||
cuestionario.contador_secciones =
|
||||
createCuestionarioDto.secciones?.length || 0;
|
||||
cuestionario.editable = true;
|
||||
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];
|
||||
|
||||
// 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`);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
@@ -133,10 +167,192 @@ 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 findAllRecientes() {
|
||||
console.log('Buscando cuestionarios recientes');
|
||||
const now = new Date();
|
||||
const pastDate = new Date();
|
||||
pastDate.setDate(now.getDate() - 30);
|
||||
|
||||
return this.cuestionarioRepository.find({
|
||||
where: {
|
||||
fecha_inicio: MoreThan(pastDate),
|
||||
},
|
||||
order: { fecha_fin: 'DESC' },
|
||||
});
|
||||
}
|
||||
|
||||
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,
|
||||
id_tipo_evento: cuestionario.id_tipo_evento,
|
||||
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();
|
||||
@@ -146,104 +362,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<Cuestionario> {
|
||||
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 }
|
||||
where: { id_cuestionario: id },
|
||||
relations: ['evento'], // Incluir la relación con el evento
|
||||
});
|
||||
|
||||
|
||||
if (!cuestionario) {
|
||||
throw new NotFoundException(`Cuestionario con ID ${id} no encontrado`);
|
||||
}
|
||||
|
||||
|
||||
// Para fines de demostración, usar valores hardcodeados
|
||||
// para el tipo de cuestionario según get_formulario_feria.ts
|
||||
const tipoCuestionario = {
|
||||
id_tipo_cuestionario: 1,
|
||||
tipo_cuestionario: 'Encuesta'
|
||||
tipo_cuestionario: 'Encuesta',
|
||||
};
|
||||
|
||||
|
||||
// Mapeador de ID de tipo de pregunta a nombres conocidos
|
||||
const tiposPreguntaMap = {
|
||||
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 un mapeo hardcodeado para tipos de pregunta
|
||||
const tiposPregunta = {
|
||||
1: { id_tipo: 1, tipo_pregunta: 'Cerrada' },
|
||||
2: { id_tipo: 2, tipo_pregunta: 'Abierta' },
|
||||
3: { id_tipo: 3, tipo_pregunta: 'Multiple' }
|
||||
|
||||
// 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 = tiposPregunta[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,
|
||||
@@ -254,17 +510,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,
|
||||
@@ -273,36 +529,84 @@ 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,
|
||||
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,
|
||||
secciones: seccionesFormateadas
|
||||
}
|
||||
id_evento: cuestionario.id_evento || null,
|
||||
secciones: seccionesFormateadas,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
update(id: number, updateCuestionarioDto: UpdateCuestionarioDto) {
|
||||
return this.cuestionarioRepository.update(id, updateCuestionarioDto);
|
||||
async update(id: number, updateCuestionarioDto: UpdateCuestionarioDto) {
|
||||
// Si el DTO incluye el campo "evento" (string), debemos procesarlo aparte
|
||||
if (updateCuestionarioDto.evento) {
|
||||
// Buscar o crear evento
|
||||
let evento = await this.eventoRepository.findOne({
|
||||
where: { nombre_evento: updateCuestionarioDto.evento },
|
||||
});
|
||||
|
||||
if (!evento) {
|
||||
// Crear el evento
|
||||
const nuevoEvento = {
|
||||
nombre_evento: updateCuestionarioDto.evento,
|
||||
tipo_evento: 'Predeterminado',
|
||||
fecha_inicio: new Date(),
|
||||
fecha_fin: new Date(Date.now() + 86400000),
|
||||
};
|
||||
|
||||
evento = await this.eventoRepository.save(nuevoEvento);
|
||||
}
|
||||
|
||||
// Asignar el ID del evento al DTO de actualización
|
||||
updateCuestionarioDto.id_evento = evento.id_evento;
|
||||
|
||||
// Eliminar la propiedad evento para evitar conflictos
|
||||
delete updateCuestionarioDto.evento;
|
||||
}
|
||||
|
||||
// Preparar los datos para la actualización
|
||||
const updateData: any = { ...updateCuestionarioDto };
|
||||
|
||||
return this.cuestionarioRepository.update(id, updateData);
|
||||
}
|
||||
|
||||
remove(id: number) {
|
||||
|
||||
+94
-87
@@ -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 } 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'
|
||||
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',
|
||||
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'
|
||||
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',
|
||||
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',
|
||||
value: feriaSexualidad
|
||||
}
|
||||
}
|
||||
ejemplo_con_evento_nuevo: ejemploCreateEventoWithCuestionario,
|
||||
},
|
||||
}),
|
||||
ApiResponse({
|
||||
status: 201,
|
||||
description: 'Cuestionario creado correctamente',
|
||||
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' }),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -1,31 +1,107 @@
|
||||
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',
|
||||
})
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
nombre_form: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Descripción del formulario',
|
||||
example: 'Formulario de registro para la feria...',
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
descripcion?: string;
|
||||
|
||||
@IsOptional()
|
||||
@ApiProperty({
|
||||
description: 'Fecha de inicio',
|
||||
example: '2025-03-26T00:00:00',
|
||||
required: false,
|
||||
})
|
||||
@IsDateString()
|
||||
fecha_inicio?: Date;
|
||||
fecha_inicio: Date;
|
||||
|
||||
@IsOptional()
|
||||
@ApiProperty({
|
||||
description: 'Fecha de fin',
|
||||
example: '2025-03-31T23:59:59',
|
||||
required: false,
|
||||
})
|
||||
@IsDateString()
|
||||
fecha_fin?: Date;
|
||||
fecha_fin: Date;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'ID del tipo de cuestionario',
|
||||
example: 1,
|
||||
})
|
||||
@IsNotEmpty()
|
||||
@IsNumber()
|
||||
id_tipo_cuestionario: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'ID del tipo de evento',
|
||||
example: 1,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
id_tipo_evento: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Cupo máximo de participantes',
|
||||
example: 100,
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
cupo_maximo?: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Nombre del evento asociado',
|
||||
example: 'Feria de la Sexualidad',
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
evento?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Secciones del cuestionario',
|
||||
type: [CreateSeccionDto],
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => CreateSeccionDto)
|
||||
secciones?: CreateSeccionDto[];
|
||||
}
|
||||
|
||||
export class CreateCuestionarioEventoDto extends OmitType(
|
||||
CreateCuestionarioDto,
|
||||
['id_evento'] as const,
|
||||
) {}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,94 +1,214 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class OpcionDto {
|
||||
@ApiProperty({
|
||||
description: 'Valor de la opción',
|
||||
example: 'Si'
|
||||
})
|
||||
valor: string;
|
||||
}
|
||||
|
||||
export class PreguntaDto {
|
||||
@ApiProperty({
|
||||
description: 'Título de la pregunta',
|
||||
example: '¿Eres parte de la comunidad de la FES Acatlán?'
|
||||
})
|
||||
titulo: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Indica si la pregunta es obligatoria',
|
||||
example: true
|
||||
})
|
||||
obligatoria: boolean;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Tipo de pregunta (Texto, Numero, Multiple, Radio, Abierto, Fecha)',
|
||||
example: 'Multiple'
|
||||
})
|
||||
tipo: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Lista de opciones para preguntas de tipo Multiple o Radio',
|
||||
type: [OpcionDto],
|
||||
required: false
|
||||
})
|
||||
opciones?: OpcionDto[];
|
||||
}
|
||||
|
||||
export class SeccionDto {
|
||||
@ApiProperty({
|
||||
description: 'Título de la sección',
|
||||
example: 'Información Personal'
|
||||
})
|
||||
titulo: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Descripción de la sección',
|
||||
example: 'Proporciona tus datos personales para poder contactarte.'
|
||||
})
|
||||
descripcion?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Lista de preguntas que conforman la sección',
|
||||
type: [PreguntaDto]
|
||||
})
|
||||
preguntas: PreguntaDto[];
|
||||
}
|
||||
// Ejemplo de respuesta de cuestionario completo
|
||||
export const FormularioFeriaSchema = {
|
||||
tipo_cuestionario: {
|
||||
id_tipo_cuestionario: 1,
|
||||
tipo_cuestionario: 'Encuesta'
|
||||
},
|
||||
cuestionario: {
|
||||
id_cuestionario: 10,
|
||||
nombre_form: 'Registro para la Feria de la Sexualidad - Comunidad FES Acatlán',
|
||||
contador_secciones: 2,
|
||||
descripcion: 'La Feria de la Sexualidad es un espacio seguro e informativo dirigido a toda la comunidad universitaria. Este registro nos ayudará a organizar mejor las actividades, talleres y charlas, así como conocer tus intereses. Por favor, completa el siguiente formulario para confirmar tu participación.',
|
||||
editable: true,
|
||||
fecha_inicio: '2025-03-26T00:00:00',
|
||||
fecha_fin: '2025-03-31T23:59:59',
|
||||
id_cuestionario_original: null,
|
||||
id_tipo_cuestionario: 1,
|
||||
secciones: [
|
||||
{
|
||||
id_cuestionario_seccion: 1,
|
||||
posicion: 1,
|
||||
seccion: {
|
||||
id_seccion: 100,
|
||||
contador_pregunta: 3,
|
||||
descripcion: 'Información básica',
|
||||
titulo: 'Datos personales'
|
||||
},
|
||||
preguntas: [
|
||||
{
|
||||
id_seccion_pregunta: 1000,
|
||||
posicion: 1,
|
||||
pregunta: {
|
||||
id_pregunta: 101,
|
||||
pregunta: '¿Eres parte de la comunidad de la FES Acatlán?',
|
||||
contador_opcion: 2,
|
||||
obligatoria: true,
|
||||
id_tipo_pregunta: 1,
|
||||
tipo_pregunta: {
|
||||
id_tipo: 1,
|
||||
tipo_pregunta: 'Cerrada'
|
||||
},
|
||||
opciones: [
|
||||
{
|
||||
id_pregunta_opcion: 50001,
|
||||
posicion: 1,
|
||||
id_opcion: 90001,
|
||||
opcion: {
|
||||
id_opcion: 90001,
|
||||
opcion: 'Sí'
|
||||
}
|
||||
},
|
||||
{
|
||||
id_pregunta_opcion: 50002,
|
||||
posicion: 2,
|
||||
id_opcion: 90002,
|
||||
opcion: {
|
||||
id_opcion: 90002,
|
||||
opcion: 'No'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
id_seccion_pregunta: 1005,
|
||||
posicion: 2,
|
||||
pregunta: {
|
||||
id_pregunta: 109,
|
||||
pregunta: 'Correo electrónico',
|
||||
contador_opcion: 0,
|
||||
obligatoria: true,
|
||||
id_tipo_pregunta: 2,
|
||||
limite: 250, // Límite de caracteres para respuestas de texto
|
||||
validacion: 'correo', // Validación de formato de correo electrónico
|
||||
tipo_pregunta: {
|
||||
id_tipo: 2,
|
||||
tipo_pregunta: 'Abierta'
|
||||
},
|
||||
opciones: []
|
||||
}
|
||||
},
|
||||
{
|
||||
id_seccion_pregunta: 1001,
|
||||
posicion: 3,
|
||||
pregunta: {
|
||||
id_pregunta: 102,
|
||||
pregunta: 'Número de cuenta (solo para estudiantes de la FES Acatlán)',
|
||||
contador_opcion: 0,
|
||||
obligatoria: false,
|
||||
id_tipo_pregunta: 2,
|
||||
limite: 250, // Límite de caracteres para respuestas de texto
|
||||
validacion: 'cuenta_alumno', // Validación de formato de número de cuenta
|
||||
tipo_pregunta: {
|
||||
id_tipo: 2,
|
||||
tipo_pregunta: 'Abierta'
|
||||
},
|
||||
opciones: []
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id_cuestionario_seccion: 2,
|
||||
posicion: 2,
|
||||
seccion: {
|
||||
id_seccion: 101,
|
||||
contador_pregunta: 2,
|
||||
descripcion: 'Información sobre tus intereses',
|
||||
titulo: 'Intereses'
|
||||
},
|
||||
preguntas: [
|
||||
{
|
||||
id_seccion_pregunta: 1003,
|
||||
posicion: 1,
|
||||
pregunta: {
|
||||
id_pregunta: 106,
|
||||
pregunta: '¿Qué temas te interesan para la Feria de la Sexualidad?',
|
||||
contador_opcion: 5,
|
||||
obligatoria: true,
|
||||
id_tipo_pregunta: 3,
|
||||
tipo_pregunta: {
|
||||
id_tipo: 3,
|
||||
tipo_pregunta: 'Multiple'
|
||||
},
|
||||
opciones: [
|
||||
{
|
||||
id_pregunta_opcion: 50010,
|
||||
posicion: 1,
|
||||
id_opcion: 90010,
|
||||
opcion: {
|
||||
id_opcion: 90010,
|
||||
opcion: 'Educación sexual'
|
||||
}
|
||||
},
|
||||
{
|
||||
id_pregunta_opcion: 50011,
|
||||
posicion: 2,
|
||||
id_opcion: 90011,
|
||||
opcion: {
|
||||
id_opcion: 90011,
|
||||
opcion: 'Prevención de ITS'
|
||||
}
|
||||
},
|
||||
{
|
||||
id_pregunta_opcion: 50012,
|
||||
posicion: 3,
|
||||
id_opcion: 90012,
|
||||
opcion: {
|
||||
id_opcion: 90012,
|
||||
opcion: 'Salud reproductiva'
|
||||
}
|
||||
},
|
||||
{
|
||||
id_pregunta_opcion: 50013,
|
||||
posicion: 4,
|
||||
id_opcion: 90013,
|
||||
opcion: {
|
||||
id_opcion: 90013,
|
||||
opcion: 'Diversidad sexual y de género'
|
||||
}
|
||||
},
|
||||
{
|
||||
id_pregunta_opcion: 50014,
|
||||
posicion: 5,
|
||||
id_opcion: 90014,
|
||||
opcion: {
|
||||
id_opcion: 90014,
|
||||
opcion: 'Relaciones saludables'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
id_seccion_pregunta: 1004,
|
||||
posicion: 2,
|
||||
pregunta: {
|
||||
id_pregunta: 107,
|
||||
pregunta: '¿Algún otro tema que te gustaría que se abordara?',
|
||||
contador_opcion: 0,
|
||||
obligatoria: false,
|
||||
id_tipo_pregunta: 2,
|
||||
limite: 250, // Límite de caracteres para respuestas de texto
|
||||
tipo_pregunta: {
|
||||
id_tipo: 2,
|
||||
tipo_pregunta: 'Abierta'
|
||||
},
|
||||
opciones: []
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
// Ejemplo de tipo FormularioDto para usar en la API
|
||||
export class FormularioDto {
|
||||
@ApiProperty({
|
||||
description: 'Nombre del formulario',
|
||||
example: 'Registro para la Feria de la Sexualidad - FES Acatlán'
|
||||
description: 'Tipo de cuestionario',
|
||||
example: {
|
||||
id_tipo_cuestionario: 1,
|
||||
tipo_cuestionario: 'Encuesta'
|
||||
}
|
||||
})
|
||||
nombre_form: string;
|
||||
tipo_cuestionario: any;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Descripción del formulario',
|
||||
example: 'La Feria de la Sexualidad es un espacio seguro e informativo...'
|
||||
description: 'Información completa del cuestionario',
|
||||
example: FormularioFeriaSchema.cuestionario
|
||||
})
|
||||
descripcion?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Fecha de inicio del formulario',
|
||||
example: '2025-03-07T00:00:01'
|
||||
})
|
||||
fecha_inicio?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Fecha de fin del formulario',
|
||||
example: '2025-03-18T23:59:59'
|
||||
})
|
||||
fecha_fin?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'ID del tipo de cuestionario',
|
||||
example: 1
|
||||
})
|
||||
id_tipo_cuestionario: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Secciones que conforman el formulario',
|
||||
type: [SeccionDto]
|
||||
})
|
||||
secciones: SeccionDto[];
|
||||
cuestionario: any;
|
||||
}
|
||||
@@ -1,4 +1,15 @@
|
||||
import { PartialType } from '@nestjs/mapped-types';
|
||||
import { CreateCuestionarioDto } from './create-cuestionario.dto';
|
||||
import { IsNumber, IsOptional } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class UpdateCuestionarioDto extends PartialType(CreateCuestionarioDto) {}
|
||||
export class UpdateCuestionarioDto extends PartialType(CreateCuestionarioDto) {
|
||||
@ApiProperty({
|
||||
description: 'ID del evento asociado',
|
||||
example: 1,
|
||||
required: false
|
||||
})
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
id_evento?: number;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
OneToMany,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { CuestionarioSeccion } from 'src/cuestionario_seccion/entities/cuestionario_seccion.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 } from 'typeorm';
|
||||
|
||||
|
||||
import { CuestionarioRespondido } from 'src/cuestionario_respondido/entities/cuestionario_respondido.entity';
|
||||
import { TipoEvento } from 'src/tipo_evento/entities/tipo_evento.entity';
|
||||
|
||||
@Entity('cuestionario')
|
||||
export class Cuestionario {
|
||||
@@ -12,6 +21,9 @@ export class Cuestionario {
|
||||
@Column({ type: 'text' })
|
||||
nombre_form: string;
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
banner: string;
|
||||
|
||||
@Column({ type: 'int' })
|
||||
contador_secciones: number;
|
||||
|
||||
@@ -21,22 +33,53 @@ 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 })
|
||||
id_tipo_evento: number;
|
||||
|
||||
@Column({ type: 'int' })
|
||||
id_evento: number;
|
||||
|
||||
// Relaciones
|
||||
@ManyToOne(() => Evento, (evento) => evento.cuestionarios, { nullable: true })
|
||||
@JoinColumn({ name: 'id_evento' })
|
||||
evento?: Evento;
|
||||
|
||||
@ManyToOne(() => TipoCuestionario, (tipo) => tipo.cuestionarios, {
|
||||
eager: true,
|
||||
})
|
||||
@JoinColumn({ name: 'id_tipo_cuestionario' })
|
||||
tipoCuestionario: TipoCuestionario;
|
||||
|
||||
@ManyToOne(() => TipoEvento, (tipo) => tipo.cuestionarios, {
|
||||
eager: true,
|
||||
})
|
||||
@JoinColumn({ name: 'id_tipo_evento' })
|
||||
tipoEvento: TipoEvento;
|
||||
|
||||
@OneToMany(
|
||||
() => CuestionarioSeccion,
|
||||
(cuestionarioSeccion) => cuestionarioSeccion.cuestionario,
|
||||
)
|
||||
cuestionarioSeccion: CuestionarioSeccion[];
|
||||
|
||||
@OneToMany(
|
||||
() => ParticipanteEvento,
|
||||
(participanteEvento) => participanteEvento.cuestionario,
|
||||
)
|
||||
inscripciones: ParticipanteEvento[];
|
||||
|
||||
@OneToMany(() => CuestionarioRespondido, (cr) => cr.cuestionario)
|
||||
cuestionariosRespondidos: CuestionarioRespondido[];
|
||||
}
|
||||
|
||||
@@ -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>(CuestionarioRespondidoController);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(controller).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,13 @@
|
||||
import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common';
|
||||
import { Controller, Get, Post, Body, Patch, Param, Delete, Res } from '@nestjs/common';
|
||||
import { CuestionarioRespondidoService } from './cuestionario_respondido.service';
|
||||
import { CreateCuestionarioRespondidoDto } from './dto/create-cuestionario_respondido.dto';
|
||||
import { UpdateCuestionarioRespondidoDto } from './dto/update-cuestionario_respondido.dto';
|
||||
import { SubmitRespuestasDto } from './dto/submit-respuestas.dto';
|
||||
import { ApiBody, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { CuestionarioRespondidoApiDocumentation } from './cuestionario_respondido.documentation';
|
||||
import { Response } from 'express';
|
||||
|
||||
@ApiTags('Cuestionario Respondido')
|
||||
@Controller('cuestionario-respondido')
|
||||
export class CuestionarioRespondidoController {
|
||||
constructor(private readonly cuestionarioRespondidoService: CuestionarioRespondidoService) {}
|
||||
@@ -12,11 +17,26 @@ export class CuestionarioRespondidoController {
|
||||
return this.cuestionarioRespondidoService.create(createCuestionarioRespondidoDto);
|
||||
}
|
||||
|
||||
@Post('submit')
|
||||
@CuestionarioRespondidoApiDocumentation.ApiSubmit
|
||||
submitRespuestas(@Body() submitRespuestasDto: SubmitRespuestasDto) {
|
||||
return this.cuestionarioRespondidoService.submitRespuestas(submitRespuestasDto);
|
||||
}
|
||||
|
||||
@Get()
|
||||
findAll() {
|
||||
return this.cuestionarioRespondidoService.findAll();
|
||||
}
|
||||
|
||||
@Get('/reporte-respuestas/:id')
|
||||
@CuestionarioRespondidoApiDocumentation.ApiReporteRespuestas
|
||||
async descargarReporteRespuestas(@Param('id') id: string, @Res() res: Response) {
|
||||
const reporte = await this.cuestionarioRespondidoService.generarReporteRespuestas(+id);
|
||||
res.header('Content-Type', 'text/csv');
|
||||
res.header('Content-Disposition', `attachment; filename="reporte-cuestionario-${id}.csv"`);
|
||||
return res.send(reporte);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
findOne(@Param('id') id: string) {
|
||||
return this.cuestionarioRespondidoService.findOne(+id);
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
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 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,
|
||||
description: 'Respuestas guardadas correctamente',
|
||||
schema: {
|
||||
properties: {
|
||||
success: { type: 'boolean', example: true },
|
||||
message: {
|
||||
type: 'string',
|
||||
example: 'Respuestas guardadas correctamente',
|
||||
},
|
||||
cuestionarioRespondidoId: { type: 'number', example: 1 },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
static ApiSubmitResponse400 = ApiResponse({
|
||||
status: 400,
|
||||
description: 'Datos inválidos en la solicitud',
|
||||
schema: {
|
||||
properties: {
|
||||
statusCode: { type: 'number', example: 400 },
|
||||
message: {
|
||||
type: 'string',
|
||||
example: 'La opción con ID 999 no está asociada a la pregunta 101',
|
||||
},
|
||||
error: { type: 'string', example: 'Bad Request' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
static ApiSubmitResponse404 = ApiResponse({
|
||||
status: 404,
|
||||
description: 'Cuestionario o pregunta no encontrada',
|
||||
schema: {
|
||||
properties: {
|
||||
statusCode: { type: 'number', example: 404 },
|
||||
message: {
|
||||
type: 'string',
|
||||
example: 'Cuestionario con ID 999 no encontrado',
|
||||
},
|
||||
error: { type: 'string', example: 'Not Found' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
static get ApiReporteRespuestas() {
|
||||
return applyDecorators(
|
||||
ApiOperation({
|
||||
summary: 'Descargar reporte de respuestas de un cuestionario',
|
||||
description: `
|
||||
Este endpoint permite descargar un archivo CSV con todas las respuestas de un cuestionario.
|
||||
El reporte incluye información del participante, evento asociado, y el contenido de las respuestas.
|
||||
Se generará un archivo CSV con los siguientes campos:
|
||||
- ID Respuesta
|
||||
- Fecha Respuesta
|
||||
- ID Participante
|
||||
- Correo Participante
|
||||
- Evento
|
||||
- Cuestionario
|
||||
- Pregunta
|
||||
- Respuesta
|
||||
`,
|
||||
}),
|
||||
ApiResponse({
|
||||
status: 200,
|
||||
description:
|
||||
'Reporte de respuestas descargado exitosamente como archivo CSV',
|
||||
}),
|
||||
ApiResponse({
|
||||
status: 404,
|
||||
description: 'Cuestionario no encontrado',
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
static ApiSubmitRespuestasExamples = {
|
||||
communityExample: {
|
||||
summary: 'Ejemplo de respuesta para un miembro de la comunidad',
|
||||
value: {
|
||||
id_formulario: 1,
|
||||
correo: 'estudiante@acatlan.unam.mx',
|
||||
respuestas: [
|
||||
{ id_pregunta: 101, valor: 3 }, // Opción "Si" (usando el ID de la opción)
|
||||
{ id_pregunta: 102, valor: 'estudiante@acatlan.unam.mx' }, // Correo electrónico
|
||||
{ id_pregunta: 103, valor: '123456789' }, // Numero de cuenta
|
||||
{ id_pregunta: 106, valor: 7 }, // Opción "Prefiero no decirlo" (usando el ID de la opción)
|
||||
],
|
||||
fecha_envio: '2025-04-02T13:45:00',
|
||||
},
|
||||
description:
|
||||
'Datos de un estudiante de la FES Acatlán con respuestas abiertas y opciones por ID.',
|
||||
},
|
||||
externalExample: {
|
||||
summary:
|
||||
'Ejemplo de respuesta para un participante externo con selección múltiple',
|
||||
value: {
|
||||
id_formulario: 1,
|
||||
correo: 'externo@ejemplo.com',
|
||||
respuestas: [
|
||||
{ id_pregunta: 101, valor: 4 }, // Opción "No" (usando el ID de la opción)
|
||||
{ id_pregunta: 102, valor: 'externo@ejemplo.com' }, // Correo electrónico
|
||||
{ id_pregunta: 103, valor: 'Juan Carlos' }, // Nombre(s)
|
||||
{ id_pregunta: 104, valor: 'Ramírez López' }, // Apellidos
|
||||
{ id_pregunta: 105, valor: [2, 5, 8] }, // Selección múltiple (array de IDs de opciones)
|
||||
{ id_pregunta: 106, valor: 7 }, // Opción "Prefiero no decirlo" (usando el ID de la opción)
|
||||
{ id_pregunta: 107, valor: 'UAM Azcapotzalco' }, // Institución de procedencia
|
||||
],
|
||||
fecha_envio: '2025-04-02T13:45:00',
|
||||
},
|
||||
description:
|
||||
'Datos de un participante externo con respuestas abiertas, opciones por ID y selección múltiple.',
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -5,15 +5,35 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { CuestionarioRespondido } from './entities/cuestionario_respondido.entity';
|
||||
import { CuestionarioModule } from '../cuestionario/cuestionario.module';
|
||||
import { ParticipanteModule } from '../participante/participante.module';
|
||||
import { QrModule } from '../qr/qr.module';
|
||||
import { Participante } from '../participante/entities/participante.entity';
|
||||
import { Cuestionario } from '../cuestionario/entities/cuestionario.entity';
|
||||
import { Pregunta } from '../pregunta/entities/pregunta.entity';
|
||||
import { RespuestaParticipanteAbierta } from '../respuesta_participante_abierta/entities/respuesta_participante_abierta.entity';
|
||||
import { RespuestaParticipanteCerrada } from '../respuesta_participante_cerrada/entities/respuesta_participante_cerrada.entity';
|
||||
import { PreguntaOpcion } from '../pregunta_opcion/entities/pregunta_opcion.entity';
|
||||
import { Opcion } from '../opcion/entities/opcion.entity';
|
||||
import { ValidacionesModule } from '../validaciones/validaciones.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([CuestionarioRespondido]),
|
||||
TypeOrmModule.forFeature([
|
||||
CuestionarioRespondido,
|
||||
Participante,
|
||||
Cuestionario,
|
||||
Pregunta,
|
||||
RespuestaParticipanteAbierta,
|
||||
RespuestaParticipanteCerrada,
|
||||
PreguntaOpcion,
|
||||
Opcion,
|
||||
]),
|
||||
CuestionarioModule,
|
||||
ParticipanteModule
|
||||
ParticipanteModule,
|
||||
ValidacionesModule,
|
||||
QrModule,
|
||||
],
|
||||
controllers: [CuestionarioRespondidoController],
|
||||
providers: [CuestionarioRespondidoService],
|
||||
exports: [TypeOrmModule]
|
||||
exports: [TypeOrmModule],
|
||||
})
|
||||
export class CuestionarioRespondidoModule {}
|
||||
|
||||
@@ -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>(CuestionarioRespondidoService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -1,26 +1,833 @@
|
||||
import { Injectable } 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/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';
|
||||
import { RespuestaParticipanteCerrada } from '../respuesta_participante_cerrada/entities/respuesta_participante_cerrada.entity';
|
||||
import { PreguntaOpcion } from '../pregunta_opcion/entities/pregunta_opcion.entity';
|
||||
import { Opcion } from '../opcion/entities/opcion.entity';
|
||||
import { ValidadorRespuestasService } from '../validaciones/validador-respuestas.service';
|
||||
import { QrTokenService } from '../qr/qr-token.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(
|
||||
@InjectRepository(CuestionarioRespondido)
|
||||
private cuestionarioRespondidoRepository: Repository<CuestionarioRespondido>,
|
||||
@InjectRepository(Participante)
|
||||
private participanteRepository: Repository<Participante>,
|
||||
@InjectRepository(Cuestionario)
|
||||
private cuestionarioRepository: Repository<Cuestionario>,
|
||||
@InjectRepository(Pregunta)
|
||||
private preguntaRepository: Repository<Pregunta>,
|
||||
@InjectRepository(PreguntaOpcion)
|
||||
private preguntaOpcionRepository: Repository<PreguntaOpcion>,
|
||||
|
||||
private dataSource: DataSource,
|
||||
private validadorRespuestasService: ValidadorRespuestasService,
|
||||
private cuestionarioService: CuestionarioService,
|
||||
private qrTokenService: QrTokenService,
|
||||
) {}
|
||||
|
||||
create(createCuestionarioRespondidoDto: CreateCuestionarioRespondidoDto) {
|
||||
return 'This action adds a new cuestionarioRespondido';
|
||||
}
|
||||
|
||||
findAll() {
|
||||
return `This action returns all cuestionarioRespondido`;
|
||||
async submitRespuestas(submitDto: SubmitRespuestasDto) {
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
try {
|
||||
// 1. Verificar que el cuestionario existe
|
||||
const cuestionario = await this.cuestionarioService.getCuestionarioOrFail(
|
||||
submitDto.id_cuestionario,
|
||||
);
|
||||
|
||||
//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 },
|
||||
});
|
||||
|
||||
if (!participante) {
|
||||
participante = new Participante();
|
||||
participante.correo = submitDto.correo;
|
||||
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();
|
||||
|
||||
let qrBuffer: Buffer | undefined;
|
||||
let qrToken: string | undefined;
|
||||
|
||||
// Aunque ya haya respondido, enviamos el correo nuevamente
|
||||
if (cuestionario && cuestionario.evento) {
|
||||
try {
|
||||
// Generar token para el QR
|
||||
qrToken = this.qrTokenService.generateQrToken(
|
||||
participante.id_participante,
|
||||
cuestionario.evento.id_evento,
|
||||
cuestionario.id_cuestionario,
|
||||
);
|
||||
|
||||
// Generar código QR con el token
|
||||
qrBuffer = await QRCode.toBuffer(qrToken);
|
||||
|
||||
// Enviar correo con el QR
|
||||
const urlApiCorreos = process.env.url_api_correos;
|
||||
|
||||
if (urlApiCorreos) {
|
||||
await axios.post(
|
||||
`${urlApiCorreos}/mail/send`,
|
||||
{
|
||||
to: participante.correo,
|
||||
subject: `Evento: ${cuestionario.evento?.nombre_evento}`,
|
||||
fecha_recibido: new Date().toISOString(),
|
||||
html: generarHtmlCorreoAsistencia({
|
||||
nombreForm: cuestionario.nombre_form,
|
||||
nombreEvento: cuestionario.evento?.nombre_evento,
|
||||
correo: participante.correo,
|
||||
fechaRegistro: new Date().toLocaleString(),
|
||||
fechaInicioEvento: cuestionario.evento?.fecha_inicio
|
||||
? new Date(
|
||||
cuestionario.evento.fecha_inicio,
|
||||
).toLocaleString()
|
||||
: undefined,
|
||||
fechaFinEvento: cuestionario.evento?.fecha_fin
|
||||
? new Date(cuestionario.evento.fecha_fin).toLocaleString()
|
||||
: undefined,
|
||||
}),
|
||||
adjuntos: [
|
||||
{
|
||||
filename: 'qr.png',
|
||||
content: qrBuffer.toString('base64'),
|
||||
encoding: 'base64',
|
||||
cid: 'qrCode',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
token: process.env.SYSTEM_TOKEN_API_CORREOS,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
console.log(
|
||||
`Correo reenviado exitosamente a ${participante.correo}`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error al reenviar correo:', error);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
registrado: true,
|
||||
message: `El participante con correo ${submitDto.correo} ya había respondido el cuestionario ${cuestionario.nombre_form}. Se ha reenviado el correo con el código QR.`,
|
||||
qr_token: qrToken,
|
||||
qr_buffer: qrBuffer ? qrBuffer.toString('base64') : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
// 3. Crear un nuevo registro de cuestionario respondido
|
||||
const cuestionarioRespondido = new CuestionarioRespondido();
|
||||
cuestionarioRespondido.cuestionario = cuestionario;
|
||||
cuestionarioRespondido.participante = participante;
|
||||
cuestionarioRespondido.fechaRespuesta = submitDto.fecha_envio
|
||||
? new Date(submitDto.fecha_envio)
|
||||
: new Date();
|
||||
|
||||
const savedCuestionarioRespondido = await queryRunner.manager.save(
|
||||
cuestionarioRespondido,
|
||||
);
|
||||
|
||||
// 4. Procesar cada respuesta
|
||||
for (const respuestaDto of submitDto.respuestas) {
|
||||
// Buscar la pregunta
|
||||
const pregunta = await this.preguntaRepository.findOne({
|
||||
where: { id_pregunta: respuestaDto.id_pregunta },
|
||||
relations: ['tipoPregunta'],
|
||||
});
|
||||
|
||||
if (!pregunta) {
|
||||
throw new NotFoundException(
|
||||
`Pregunta con ID ${respuestaDto.id_pregunta} no encontrada`,
|
||||
);
|
||||
}
|
||||
|
||||
// Si la pregunta tiene una validación configurada y es una respuesta abierta, validarla
|
||||
if (pregunta.validacion && typeof respuestaDto.valor === 'string') {
|
||||
const resultadoValidacion =
|
||||
this.validadorRespuestasService.validarRespuesta(
|
||||
respuestaDto.valor,
|
||||
pregunta.validacion,
|
||||
);
|
||||
|
||||
if (!resultadoValidacion.valido) {
|
||||
throw new BadRequestException(
|
||||
`Validación fallida en pregunta ${pregunta.id_pregunta}: ${resultadoValidacion.mensaje}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Determinar tipo de pregunta por ID
|
||||
const esPreguntaAbierta = pregunta.id_tipo_pregunta === 2;
|
||||
|
||||
// O determinar tipo de pregunta por su nombre si existe
|
||||
const esPreguntaTipoAbierta =
|
||||
pregunta.tipoPregunta.tipo_pregunta ===
|
||||
TipoPreguntaEnum.AbiertaParrafo ||
|
||||
pregunta.tipoPregunta.tipo_pregunta ===
|
||||
TipoPreguntaEnum.AbiertaRespuestaCorta;
|
||||
|
||||
// Determinar tipo de pregunta y guardar respuesta adecuadamente
|
||||
if (esPreguntaAbierta || esPreguntaTipoAbierta) {
|
||||
// Respuesta abierta (texto)
|
||||
// Convertir a string y truncar a 250 caracteres si es necesario
|
||||
let respuestaTexto = String(respuestaDto.valor);
|
||||
if (respuestaTexto.length > 250) {
|
||||
respuestaTexto = respuestaTexto.substring(0, 250);
|
||||
}
|
||||
|
||||
const respuestaAbierta = new RespuestaParticipanteAbierta();
|
||||
respuestaAbierta.pregunta = pregunta;
|
||||
respuestaAbierta.respuesta = respuestaTexto;
|
||||
respuestaAbierta.cuestionarioRespondido = savedCuestionarioRespondido;
|
||||
|
||||
await queryRunner.manager.save(respuestaAbierta);
|
||||
} else {
|
||||
// Respuesta cerrada (opción)
|
||||
// Verificar si es una respuesta múltiple (array de IDs)
|
||||
// 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.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}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Guardar respuesta cerrada
|
||||
const respuestaCerrada = new RespuestaParticipanteCerrada();
|
||||
respuestaCerrada.preguntaOpcion = preguntaOpciones[0];
|
||||
respuestaCerrada.cuestionarioRespondido =
|
||||
savedCuestionarioRespondido;
|
||||
|
||||
await queryRunner.manager.save(respuestaCerrada);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Variables para el QR y correo electrónico
|
||||
let datosQR: {
|
||||
id_participante: number;
|
||||
id_evento: number;
|
||||
id_cuestionario: number;
|
||||
} | null = null;
|
||||
|
||||
let qrBuffer: Buffer | undefined;
|
||||
let qrToken: string | undefined;
|
||||
|
||||
// Capturamos los datos para el QR
|
||||
if (cuestionario.evento && cuestionario.evento.id_evento) {
|
||||
datosQR = {
|
||||
id_participante: participante.id_participante,
|
||||
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 && 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_cuestionario = ?`,
|
||||
[participante.id_participante, cuestionario.id_cuestionario],
|
||||
);
|
||||
|
||||
// Si no existe, crear la relación
|
||||
if (
|
||||
!participanteEventoExistente ||
|
||||
participanteEventoExistente.length === 0
|
||||
) {
|
||||
await this.dataSource.query(
|
||||
`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,
|
||||
);
|
||||
}
|
||||
|
||||
// Generar token para el QR
|
||||
qrToken = this.qrTokenService.generateQrToken(
|
||||
participante.id_participante,
|
||||
cuestionario.evento.id_evento,
|
||||
cuestionario.id_cuestionario,
|
||||
);
|
||||
|
||||
qrBuffer = await QRCode.toBuffer(qrToken);
|
||||
|
||||
// Enviar correo con el QR
|
||||
const urlApiCorreos = process.env.url_api_correos;
|
||||
|
||||
if (!urlApiCorreos) {
|
||||
throw new BadRequestException(
|
||||
'No se encontro servicio de correos configurado',
|
||||
);
|
||||
}
|
||||
|
||||
await axios.post(
|
||||
`${urlApiCorreos}/mail/send`,
|
||||
{
|
||||
to: participante.correo,
|
||||
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',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
token: process.env.SYSTEM_TOKEN_API_CORREOS,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
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,
|
||||
registrado: false,
|
||||
message: 'Respuestas guardadas correctamente',
|
||||
cuestionarioRespondidoId:
|
||||
savedCuestionarioRespondido.idCuestionarioRespondido,
|
||||
qr_token: qrToken,
|
||||
qr_buffer: qrBuffer ? qrBuffer.toString('base64') : undefined,
|
||||
};
|
||||
|
||||
// Incluir IDs para generar QR si hay evento asociado
|
||||
if (datosQR) {
|
||||
response.datos_qr = datosQR;
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
// Solo hacemos rollback si no hemos hecho commit todavía
|
||||
try {
|
||||
await queryRunner.rollbackTransaction();
|
||||
} catch (rollbackError) {
|
||||
console.error('Error durante rollback:', rollbackError.message);
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
|
||||
findOne(id: number) {
|
||||
return `This action returns a #${id} cuestionarioRespondido`;
|
||||
async findAll() {
|
||||
// Obtener todos los cuestionarios respondidos con sus relaciones básicas
|
||||
const cuestionariosRespondidos =
|
||||
await this.cuestionarioRespondidoRepository.find({
|
||||
relations: [
|
||||
'participante',
|
||||
'cuestionario',
|
||||
'respuestasAbiertas',
|
||||
'respuestasAbiertas.pregunta',
|
||||
'respuestasCerradas',
|
||||
],
|
||||
});
|
||||
|
||||
// Para cada cuestionario respondido, obtener y formatear las respuestas cerradas
|
||||
const resultados = await Promise.all(
|
||||
cuestionariosRespondidos.map(async (cuestionarioRespondido) => {
|
||||
// Obtener datos de respuestas cerradas con consulta SQL
|
||||
const respuestasCerradas = await this.dataSource.query(
|
||||
`
|
||||
SELECT
|
||||
rpc.idRespuestaParticipanteCerrada,
|
||||
rpc.id_pregunta_opcion,
|
||||
po.id_pregunta,
|
||||
p.pregunta,
|
||||
p.id_tipo_pregunta,
|
||||
o.id_opcion,
|
||||
o.opcion
|
||||
FROM respuesta_participante_cerrada rpc
|
||||
LEFT JOIN pregunta_opcion po ON rpc.id_pregunta_opcion = po.id_pregunta_opcion
|
||||
LEFT JOIN pregunta p ON po.id_pregunta = p.id_pregunta
|
||||
LEFT JOIN opcion o ON po.id_opcion = o.id_opcion
|
||||
WHERE rpc.id_cuestionario_respondido = ?
|
||||
`,
|
||||
[cuestionarioRespondido.idCuestionarioRespondido],
|
||||
);
|
||||
|
||||
// Formatear respuestas cerradas
|
||||
const respuestasCerradasFormateadas = respuestasCerradas.map(
|
||||
(respuesta) => ({
|
||||
idRespuestaParticipanteCerrada:
|
||||
respuesta.idRespuestaParticipanteCerrada,
|
||||
id_pregunta_opcion: respuesta.id_pregunta_opcion,
|
||||
pregunta: {
|
||||
id_pregunta: respuesta.id_pregunta,
|
||||
texto_pregunta: respuesta.pregunta,
|
||||
id_tipo_pregunta: respuesta.id_tipo_pregunta,
|
||||
},
|
||||
opcion: {
|
||||
id_opcion: respuesta.id_opcion,
|
||||
opcion: respuesta.opcion,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// Devolver cuestionario con respuestas formateadas
|
||||
return {
|
||||
...cuestionarioRespondido,
|
||||
respuestasCerradas: respuestasCerradasFormateadas,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
return resultados;
|
||||
}
|
||||
|
||||
update(id: number, updateCuestionarioRespondidoDto: UpdateCuestionarioRespondidoDto) {
|
||||
async findOne(id: number) {
|
||||
// Obtener cuestionario respondido con sus relaciones básicas
|
||||
const cuestionarioRespondido =
|
||||
await this.cuestionarioRespondidoRepository.findOne({
|
||||
where: { idCuestionarioRespondido: id },
|
||||
relations: [
|
||||
'participante',
|
||||
'cuestionario',
|
||||
'respuestasAbiertas',
|
||||
'respuestasAbiertas.pregunta',
|
||||
'respuestasCerradas',
|
||||
],
|
||||
});
|
||||
|
||||
if (!cuestionarioRespondido) {
|
||||
throw new NotFoundException(
|
||||
`Cuestionario respondido con ID ${id} no encontrado`,
|
||||
);
|
||||
}
|
||||
|
||||
// Obtener directamente los datos de respuestas cerradas desde la base de datos
|
||||
// usando una consulta SQL directa
|
||||
const respuestasCerradas = await this.dataSource.query(
|
||||
`
|
||||
SELECT
|
||||
rpc.idRespuestaParticipanteCerrada,
|
||||
rpc.id_pregunta_opcion,
|
||||
po.id_pregunta,
|
||||
p.pregunta,
|
||||
p.id_tipo_pregunta,
|
||||
o.id_opcion,
|
||||
o.opcion
|
||||
FROM respuesta_participante_cerrada rpc
|
||||
LEFT JOIN pregunta_opcion po ON rpc.id_pregunta_opcion = po.id_pregunta_opcion
|
||||
LEFT JOIN pregunta p ON po.id_pregunta = p.id_pregunta
|
||||
LEFT JOIN opcion o ON po.id_opcion = o.id_opcion
|
||||
WHERE rpc.id_cuestionario_respondido = ?
|
||||
`,
|
||||
[id],
|
||||
);
|
||||
|
||||
// Transformar las respuestas cerradas al formato deseado
|
||||
const respuestasCerradasFormateadas = respuestasCerradas.map(
|
||||
(respuesta) => ({
|
||||
idRespuestaParticipanteCerrada:
|
||||
respuesta.idRespuestaParticipanteCerrada,
|
||||
id_pregunta_opcion: respuesta.id_pregunta_opcion,
|
||||
pregunta: {
|
||||
id_pregunta: respuesta.id_pregunta,
|
||||
texto_pregunta: respuesta.pregunta,
|
||||
id_tipo_pregunta: respuesta.id_tipo_pregunta,
|
||||
},
|
||||
opcion: {
|
||||
id_opcion: respuesta.id_opcion,
|
||||
opcion: respuesta.opcion,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// Crear el objeto de respuesta con el formato deseado
|
||||
const resultado = {
|
||||
...cuestionarioRespondido,
|
||||
respuestasCerradas: respuestasCerradasFormateadas,
|
||||
};
|
||||
|
||||
return resultado;
|
||||
}
|
||||
|
||||
update(
|
||||
id: number,
|
||||
updateCuestionarioRespondidoDto: UpdateCuestionarioRespondidoDto,
|
||||
) {
|
||||
return `This action updates a #${id} cuestionarioRespondido`;
|
||||
}
|
||||
|
||||
remove(id: number) {
|
||||
return `This action removes a #${id} cuestionarioRespondido`;
|
||||
}
|
||||
|
||||
/* async generarReporteRespuestas(idCuestionario: number): Promise<string> {
|
||||
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<string, Map<number, string>>();
|
||||
|
||||
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<string> {
|
||||
// Buscar el cuestionario para validar que existe
|
||||
const cuestionario = await this.cuestionarioRepository.findOne({
|
||||
where: { id_cuestionario: idCuestionario },
|
||||
relations: ['evento'],
|
||||
});
|
||||
|
||||
if (!cuestionario) {
|
||||
throw new NotFoundException(
|
||||
`Cuestionario con ID ${idCuestionario} no encontrado`,
|
||||
);
|
||||
}
|
||||
|
||||
// Obtener todas las preguntas del cuestionario para los encabezados
|
||||
const preguntas = await this.dataSource.query(
|
||||
`
|
||||
SELECT
|
||||
p.id_pregunta,
|
||||
p.pregunta,
|
||||
p.id_tipo_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],
|
||||
);
|
||||
|
||||
// Obtener los participantes que han respondido este cuestionario
|
||||
const participantes = await this.dataSource.query(
|
||||
`
|
||||
SELECT DISTINCT
|
||||
cr.id_cuestionario_respondido,
|
||||
cr.fecha_respuesta,
|
||||
p.id_participante,
|
||||
p.correo AS correo_participante,
|
||||
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_cuestionario = ?
|
||||
WHERE cr.id_cuestionario = ?
|
||||
ORDER BY cr.fecha_respuesta DESC
|
||||
`,
|
||||
[
|
||||
cuestionario.evento ? cuestionario.id_cuestionario : null,
|
||||
idCuestionario,
|
||||
],
|
||||
);
|
||||
|
||||
// Obtener todas las respuestas para este cuestionario
|
||||
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],
|
||||
);
|
||||
|
||||
// Crear un mapa de respuestas por participante y pregunta
|
||||
const respuestasPorParticipante = new Map();
|
||||
|
||||
// Agregar respuestas abiertas al mapa
|
||||
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) => {
|
||||
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}`,
|
||||
);
|
||||
} else {
|
||||
respuestasPorParticipante.set(key, respuesta.opcion);
|
||||
}
|
||||
});
|
||||
|
||||
// Preparar los datos para el CSV
|
||||
const datosReporte: string[][] = [];
|
||||
|
||||
// Agregar encabezados con datos del participante seguidos por las preguntas
|
||||
const encabezados = [
|
||||
'ID Participante',
|
||||
'Correo',
|
||||
'Fecha Respuesta',
|
||||
'Asistencia',
|
||||
];
|
||||
|
||||
// Agregar cada pregunta como un encabezado
|
||||
preguntas.forEach((pregunta) => {
|
||||
encabezados.push(pregunta.pregunta);
|
||||
});
|
||||
|
||||
datosReporte.push(encabezados);
|
||||
|
||||
// Crear una fila por cada participante
|
||||
participantes.forEach((participante) => {
|
||||
const fila = [
|
||||
participante.id_participante.toString(),
|
||||
participante.correo_participante,
|
||||
new Date(participante.fecha_respuesta).toLocaleString(),
|
||||
participante.fecha_asistencia ? 'Sí' : 'No',
|
||||
];
|
||||
|
||||
// Agregar cada respuesta en el orden de las preguntas
|
||||
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');
|
||||
|
||||
return csvContent;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import {
|
||||
IsArray,
|
||||
IsDateString,
|
||||
IsNotEmpty,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString,
|
||||
ValidateNested,
|
||||
ValidateIf,
|
||||
MaxLength,
|
||||
} from 'class-validator';
|
||||
import { Type, Transform } from 'class-transformer';
|
||||
|
||||
export class RespuestaDto {
|
||||
@ApiProperty({
|
||||
description: 'ID de la pregunta respondida',
|
||||
example: 101,
|
||||
})
|
||||
@IsNumber()
|
||||
id_pregunta: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Valor de la respuesta según tipo de pregunta',
|
||||
examples: [
|
||||
'Texto para pregunta abierta', // String para preguntas abiertas
|
||||
5, // Número para preguntas cerradas (ID de opción)
|
||||
[2, 7, 9], // Array para preguntas múltiples (IDs de opciones)
|
||||
],
|
||||
oneOf: [
|
||||
{
|
||||
type: 'string',
|
||||
description: 'Texto para pregunta abierta (máximo 250 caracteres)',
|
||||
},
|
||||
{ type: 'number', description: 'ID de opción para pregunta cerrada' },
|
||||
{
|
||||
type: 'array',
|
||||
items: { type: 'number' },
|
||||
description: 'Array de IDs de opciones para pregunta multiple',
|
||||
},
|
||||
],
|
||||
})
|
||||
@ValidateIf((o) => typeof o.valor === 'string')
|
||||
@MaxLength(250, {
|
||||
message: 'Las respuestas de texto no pueden exceder 250 caracteres',
|
||||
})
|
||||
valor: string | number | number[];
|
||||
}
|
||||
|
||||
export class SubmitRespuestasDto {
|
||||
@ApiProperty({
|
||||
description: 'ID del cuestionario a responder',
|
||||
example: 1,
|
||||
})
|
||||
@IsNumber()
|
||||
id_cuestionario: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Correo electrónico del participante',
|
||||
example: 'usuario@ejemplo.com',
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MaxLength(250, {
|
||||
message: 'El correo electrónico no puede exceder 250 caracteres',
|
||||
})
|
||||
correo: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Array de respuestas a las preguntas',
|
||||
type: [RespuestaDto],
|
||||
examples: [
|
||||
{
|
||||
id_pregunta: 101,
|
||||
valor: 'Texto para pregunta abierta',
|
||||
},
|
||||
{
|
||||
id_pregunta: 102,
|
||||
valor: 5,
|
||||
},
|
||||
{
|
||||
id_pregunta: 103,
|
||||
valor: [2, 7, 9],
|
||||
},
|
||||
],
|
||||
})
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => RespuestaDto)
|
||||
respuestas: RespuestaDto[];
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Fecha y hora del envío',
|
||||
example: '2025-04-02T13:45:00',
|
||||
})
|
||||
@IsDateString()
|
||||
@IsOptional()
|
||||
fecha_envio?: string;
|
||||
}
|
||||
@@ -3,7 +3,8 @@ 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')
|
||||
export class CuestionarioRespondido {
|
||||
|
||||
@@ -10,7 +10,6 @@ export class CuestionarioSeccion {
|
||||
@Column({ type: 'tinyint' })
|
||||
posicion: number;
|
||||
|
||||
|
||||
@ManyToOne(() => Cuestionario, (cuestionario) => cuestionario.cuestionarioSeccion)
|
||||
cuestionario: Cuestionario;
|
||||
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
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';
|
||||
import { TipoEvento } from 'src/tipo_evento/entities/tipo_evento.entity';
|
||||
|
||||
export const createMainDbConfig = async (
|
||||
configService: ConfigService,
|
||||
): Promise<TypeOrmModuleOptions> => ({
|
||||
type: 'mariadb',
|
||||
host: configService.get<string>('DB_HOST'),
|
||||
port: configService.get<number>('DB_PORT'),
|
||||
username: configService.get<string>('DB_USERNAME'),
|
||||
password: configService.get<string>('DB_PASSWORD'),
|
||||
database: configService.get<string>('DB_DATABASE'),
|
||||
entities: [
|
||||
Administrador,
|
||||
Asistencia,
|
||||
Cuestionario,
|
||||
CuestionarioRespondido,
|
||||
CuestionarioSeccion,
|
||||
Evento,
|
||||
Opcion,
|
||||
Participante,
|
||||
ParticipanteEvento,
|
||||
Pregunta,
|
||||
PreguntaOpcion,
|
||||
RespuestaParticipanteAbierta,
|
||||
RespuestaParticipanteCerrada,
|
||||
Seccion,
|
||||
SeccionPregunta,
|
||||
TipoCuestionario,
|
||||
TipoEvento,
|
||||
TipoPregunta,
|
||||
TipoUser,
|
||||
],
|
||||
retryAttempts: 5,
|
||||
retryDelay: 3000,
|
||||
connectTimeout: 30000,
|
||||
|
||||
synchronize: configService.get<string>('SYNCHRONIZE') === 'true',
|
||||
dropSchema: configService.get<string>('DROP_SCHEMA') === 'true',
|
||||
});
|
||||
|
||||
export const createAlumnosDbConfig = async (
|
||||
configService: ConfigService,
|
||||
): Promise<TypeOrmModuleOptions> => ({
|
||||
name: 'alumnosConnection',
|
||||
type: 'mariadb',
|
||||
host: configService.get<string>('ALUMNOS_DB_HOST'),
|
||||
port: configService.get<number>('ALUMNOS_DB_PORT'),
|
||||
username: configService.get<string>('ALUMNOS_DB_USERNAME'),
|
||||
password: configService.get<string>('ALUMNOS_DB_PASSWORD'),
|
||||
database: configService.get<string>('ALUMNOS_DB_DATABASE'),
|
||||
entities: [RegistroAlumno, RegistroTrabajador],
|
||||
retryAttempts: 10,
|
||||
retryDelay: 3000,
|
||||
connectTimeout: 30000,
|
||||
|
||||
synchronize: false,
|
||||
dropSchema: false,
|
||||
});
|
||||
@@ -18,7 +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.
|
||||
`)
|
||||
|
||||
.setVersion('1.0')
|
||||
.addTag('Administradores')
|
||||
.addTag('Cuestionarios')
|
||||
.addTag('Secciones')
|
||||
.addTag('Preguntas')
|
||||
|
||||
@@ -0,0 +1,307 @@
|
||||
export function generarHtmlCorreoAsistenciaTicket({
|
||||
nombreForm,
|
||||
nombreEvento,
|
||||
correo,
|
||||
fechaRegistro,
|
||||
fechaInicioEvento,
|
||||
fechaFinEvento,
|
||||
}: {
|
||||
nombreForm: string;
|
||||
nombreEvento: string;
|
||||
correo: string;
|
||||
fechaRegistro: string;
|
||||
fechaInicioEvento?: string;
|
||||
fechaFinEvento?: string;
|
||||
}) {
|
||||
const horarioEvento =
|
||||
fechaInicioEvento && fechaFinEvento
|
||||
? `${fechaInicioEvento} al ${fechaFinEvento}`
|
||||
: 'Por confirmar';
|
||||
|
||||
return `
|
||||
<div
|
||||
style="
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
max-width: 650px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
border-radius: 16px;
|
||||
"
|
||||
>
|
||||
<!-- Header del correo -->
|
||||
<div style="text-align: center">
|
||||
<h1>¡Registro Confirmado!</h1>
|
||||
</div>
|
||||
|
||||
<!-- Ticket Container -->
|
||||
<div
|
||||
style="
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
background: #fefefe;
|
||||
position: relative;
|
||||
"
|
||||
>
|
||||
<!-- Ticket Header -->
|
||||
<div
|
||||
style="
|
||||
background: linear-gradient(90deg, #003d79 0%, #0056b3 100%);
|
||||
color: white;
|
||||
padding: 20px;
|
||||
position: relative;
|
||||
"
|
||||
>
|
||||
<h2 style="margin: 0 0 8px 0; font-size: 24px; font-weight: bold">
|
||||
🎓 ${nombreEvento || 'Evento Especial'}
|
||||
</h2>
|
||||
<p style="margin: 0; opacity: 0.9; font-size: 14px">
|
||||
TICKET DE ENTRADA • ENTRADA GRATUITA
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Ticket Body -->
|
||||
<div style="padding: 30px 20px">
|
||||
<div style="display: flex; gap: 30px; align-items: center">
|
||||
<!-- QR Code Section -->
|
||||
<div
|
||||
style="
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
"
|
||||
>
|
||||
<div
|
||||
style="
|
||||
background: #f8f9fa;
|
||||
border: 2px dashed #dee2e6;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
"
|
||||
>
|
||||
<img
|
||||
src="cid:qrCode"
|
||||
alt="Código QR de Entrada"
|
||||
style="
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
"
|
||||
/>
|
||||
<p
|
||||
style="
|
||||
margin: 12px 0 0 0;
|
||||
font-size: 12px;
|
||||
color: #6c757d;
|
||||
font-weight: 600;
|
||||
"
|
||||
>
|
||||
ESCANEA PARA REGISTRAR ASISTENCIA
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ticket Info Section -->
|
||||
<div style="flex: 1.2">
|
||||
<div style="margin-bottom: 24px">
|
||||
<h3
|
||||
style="
|
||||
color: #003d79;
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 18px;
|
||||
border-bottom: 2px solid #e9ecef;
|
||||
padding-bottom: 8px;
|
||||
"
|
||||
>
|
||||
📋 Detalles de tu registro
|
||||
</h3>
|
||||
|
||||
<div>
|
||||
<div style="margin-bottom: 12px">
|
||||
<p
|
||||
style="
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: #6c757d;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
"
|
||||
>
|
||||
Participante
|
||||
</p>
|
||||
<p
|
||||
style="
|
||||
margin: 2px 0 0 0;
|
||||
font-size: 15px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
"
|
||||
>
|
||||
${correo}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 12px">
|
||||
<p
|
||||
style="
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: #6c757d;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
"
|
||||
>
|
||||
Formulario
|
||||
</p>
|
||||
<p
|
||||
style="
|
||||
margin: 2px 0 0 0;
|
||||
font-size: 15px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
"
|
||||
>
|
||||
${nombreForm}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 12px">
|
||||
<p
|
||||
style="
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: #6c757d;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
"
|
||||
>
|
||||
Fecha de Registro
|
||||
</p>
|
||||
<p
|
||||
style="
|
||||
margin: 2px 0 0 0;
|
||||
font-size: 15px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
"
|
||||
>
|
||||
${fechaRegistro}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 12px">
|
||||
<p
|
||||
style="
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: #6c757d;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
"
|
||||
>
|
||||
🕒 Horario del Evento
|
||||
</p>
|
||||
<p
|
||||
style="
|
||||
margin: 2px 0 0 0;
|
||||
font-size: 15px;
|
||||
color: #003d79;
|
||||
font-weight: 600;
|
||||
"
|
||||
>
|
||||
${horarioEvento}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ticket Footer -->
|
||||
<div
|
||||
style="
|
||||
background: #f8f9fa;
|
||||
padding: 16px 20px;
|
||||
border-top: 2px dashed #dee2e6;
|
||||
"
|
||||
>
|
||||
<div
|
||||
style="
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
"
|
||||
>
|
||||
<div>
|
||||
<p style="margin: 0; font-size: 12px; color: #6c757d">
|
||||
<strong>IMPORTANTE:</strong> Presenta este código QR en la mesa
|
||||
de registro
|
||||
</p>
|
||||
<p style="margin: 4px 0 0 0; font-size: 11px; color: #6c757d">
|
||||
Disponible en formato digital o impreso
|
||||
</p>
|
||||
</div>
|
||||
<div style="text-align: right">
|
||||
<p
|
||||
style="
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: #003d79;
|
||||
font-weight: bold;
|
||||
"
|
||||
>
|
||||
ENTRADA VÁLIDA
|
||||
</p>
|
||||
<p style="margin: 2px 0 0 0; font-size: 11px; color: #28a745">
|
||||
✓ Verificado
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Instrucciones adicionales -->
|
||||
<div
|
||||
style="
|
||||
margin-top: 20px;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
"
|
||||
>
|
||||
<h4 style="color: #003d79; margin: 0 0 12px 0; font-size: 16px">
|
||||
📝 Instrucciones importantes:
|
||||
</h4>
|
||||
<ul
|
||||
style="
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
"
|
||||
>
|
||||
<li>Llega 15 minutos antes del inicio del evento</li>
|
||||
<li>
|
||||
Presenta este QR en la mesa de registro para validar tu asistencia
|
||||
</li>
|
||||
<li>Tu constancia será otorgada al finalizar el evento</li>
|
||||
<li>Para dudas, acude al módulo de información durante el evento</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Footer del correo -->
|
||||
<div style="text-align: center; margin-top: 20px">
|
||||
<p style="color: rgba(255, 255, 255, 0.8); font-size: 13px; margin: 0">
|
||||
Este mensaje fue enviado automáticamente. Por favor, no respondas a
|
||||
este correo.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -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
|
||||
? `
|
||||
<div style="margin: 20px 0; padding: 12px; background-color: #e8f0fe; border-left: 4px solid #003d79;">
|
||||
<p style="margin: 0; font-size: 15px; color: #003d79;">
|
||||
<strong>🕒 Horario del evento:</strong><br/>
|
||||
${fechaInicioEvento} <strong>al</strong> ${fechaFinEvento}
|
||||
</p>
|
||||
</div>
|
||||
`
|
||||
: '';
|
||||
|
||||
return `
|
||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 24px; border: 1px solid #ccc; border-radius: 8px; background-color: #f9f9f9;">
|
||||
<h1 style="color: #003d79; border-bottom: 2px solid #003d79; padding-bottom: 10px;">🎓 ¡Gracias por registrarte!</h1>
|
||||
|
||||
<p style="font-size: 16px; color: #333;">
|
||||
Has completado exitosamente el formulario: <strong>${nombreForm}</strong>
|
||||
</p>
|
||||
|
||||
<p style="font-size: 16px; color: #333;">
|
||||
Favor de presentar este código QR (celular o impreso) en la mesa de registro. el día del evento, para validar su asistencia y tener derecho a la constancia.
|
||||
</p>
|
||||
|
||||
<div style="text-align: center; margin: 20px 0;">
|
||||
<img src="cid:qrCode" alt="Código QR" style="width: 250px; height: 250px; border: 1px solid #ccc; padding: 8px; background-color: white;" />
|
||||
</div>
|
||||
|
||||
<h3 style="color: #003d79;">📋 Tus datos de registro:</h3>
|
||||
<ul style="font-size: 15px; color: #333; line-height: 1.6;">
|
||||
<li><strong>Evento:</strong> ${nombreEvento || 'Evento'}</li>
|
||||
<li><strong>Correo:</strong> ${correo}</li>
|
||||
<li><strong>Fecha de registro:</strong> ${fechaRegistro}</li>
|
||||
</ul>
|
||||
|
||||
${horarioEvento}
|
||||
|
||||
<p style="margin-top: 30px; font-size: 14px; color: #666;">
|
||||
Si tienes alguna duda, acude al módulo de información durante el evento.
|
||||
</p>
|
||||
|
||||
<p style="font-size: 13px; color: #999; text-align: center; margin-top: 40px;">
|
||||
Este mensaje fue enviado automáticamente. Por favor, no respondas a este correo.
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -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'],
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
};
|
||||
@@ -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
|
||||
}
|
||||
@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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import { Expose, Type } from 'class-transformer';
|
||||
import { TipoCuestionarioOutputDto } from 'src/tipo_cuestionario/dto/outputs.dto';
|
||||
import { TipoEventoOutputDto } from 'src/tipo_evento/dto/outputs.dto';
|
||||
|
||||
export class CuestionarioConCupoDto {
|
||||
@Expose()
|
||||
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;
|
||||
|
||||
@Expose()
|
||||
@Type(() => TipoEventoOutputDto)
|
||||
tipoEvento: TipoEventoOutputDto;
|
||||
}
|
||||
|
||||
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[];
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { Cuestionario } from 'src/cuestionario/entities/cuestionario.entity';
|
||||
import { TipoEvento } from 'src/tipo_evento/entities/tipo_evento.entity';
|
||||
import {
|
||||
Column,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
PrimaryGeneratedColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
@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[];
|
||||
|
||||
@ManyToOne(() => TipoEvento, (tipo) => tipo.eventos, {
|
||||
eager: true,
|
||||
})
|
||||
@JoinColumn({ name: 'id_tipo_evento' })
|
||||
tipoEvento: TipoEvento;
|
||||
}
|
||||
+109
-24
@@ -1,37 +1,122 @@
|
||||
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<Evento[]> {
|
||||
return this.eventoService.getEventos()
|
||||
@Post()
|
||||
@EventoApiDocumentation.ApiCreate
|
||||
createEvento(@Body() newEvento: CreateEventoDto) {
|
||||
return this.eventoService.createEvento(newEvento);
|
||||
}
|
||||
|
||||
@Get()
|
||||
getEventos(): Promise<Evento[]> {
|
||||
return this.eventoService.getEventos();
|
||||
}
|
||||
|
||||
@Get('activos/cuestionarios')
|
||||
getCuestionariosActivos() {
|
||||
return this.eventoService.getEventosActivosCuestionarios();
|
||||
}
|
||||
|
||||
@Get('recientes/cuestionarios')
|
||||
getEventosRecientes() {
|
||||
return this.eventoService.getEventosRecientesCuestionarios();
|
||||
}
|
||||
|
||||
@Get('activos')
|
||||
getEventosActivos(): Promise<Evento[]> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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[];
|
||||
|
||||
}
|
||||
@@ -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])],
|
||||
|
||||
+250
-53
@@ -1,73 +1,270 @@
|
||||
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<Evento>,
|
||||
) {}
|
||||
constructor(
|
||||
@InjectRepository(Evento) private eventoRepository: Repository<Evento>,
|
||||
) {}
|
||||
|
||||
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: unknown) {
|
||||
let errorMessage = '';
|
||||
if (err && typeof err === 'object' && 'message' in err) {
|
||||
errorMessage = (err as { message?: string }).message ?? '';
|
||||
}
|
||||
console.error(
|
||||
`Error al eliminar el banner anterior: ${errorMessage}`,
|
||||
);
|
||||
}
|
||||
|
||||
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<Evento> {
|
||||
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 getEventosRecientesCuestionarios(): Promise<
|
||||
EventoConCuestionariosDto[]
|
||||
> {
|
||||
const ahora = new Date();
|
||||
const hace30Dias = new Date();
|
||||
hace30Dias.setDate(ahora.getDate() - 30);
|
||||
|
||||
const eventos = await this.eventoRepository.find({
|
||||
where: {
|
||||
fecha_fin: MoreThan(hace30Dias),
|
||||
},
|
||||
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 getEventosActivos() {
|
||||
const now = new Date();
|
||||
const eventos = await this.eventoRepository.find({
|
||||
where: {
|
||||
fecha_fin: MoreThan(now),
|
||||
},
|
||||
relations: ['cuestionarios'],
|
||||
});
|
||||
|
||||
return eventos.map((evento) => ({
|
||||
...evento,
|
||||
total_cuestionarios: evento.cuestionarios
|
||||
? evento.cuestionarios.length
|
||||
: 0,
|
||||
}));
|
||||
}
|
||||
|
||||
async getCuestionarioEvento(id_evento: number, id_cuestionario: number) {
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+35
-34
@@ -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<NestExpressApplication>(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<number>('API_PORT') || 4200);
|
||||
console.log('\nAPI en ejecución en http://localhost:4200');
|
||||
console.log('Swagger disponible en http://localhost:4200/api-docs');
|
||||
}
|
||||
bootstrap();
|
||||
|
||||
@@ -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[];
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { IsEmail } from "class-validator";
|
||||
import { IsEmail, IsNotEmpty, IsNumber } from "class-validator";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
|
||||
export class CreateParticipanteDto {
|
||||
@IsEmail()
|
||||
correo: string
|
||||
id_tipo_user: number
|
||||
@ApiProperty({
|
||||
description: 'Correo electrónico del participante',
|
||||
example: 'usuario@ejemplo.com',
|
||||
required: true
|
||||
})
|
||||
@IsEmail({}, { message: 'El correo electrónico debe tener un formato válido' })
|
||||
@IsNotEmpty({ message: 'El correo electrónico es requerido' })
|
||||
correo: string;
|
||||
}
|
||||
@@ -1,3 +1,22 @@
|
||||
import { IsEmail, IsOptional, IsNumber } from "class-validator";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
|
||||
export class UpdateParticipanteDto {
|
||||
correo: string
|
||||
@ApiProperty({
|
||||
description: 'Nuevo correo electrónico del participante',
|
||||
example: 'nuevo_correo@ejemplo.com',
|
||||
required: false
|
||||
})
|
||||
@IsEmail({}, { message: 'El correo electrónico debe tener un formato válido' })
|
||||
@IsOptional()
|
||||
correo?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Nuevo ID del tipo de usuario',
|
||||
example: 2,
|
||||
required: false
|
||||
})
|
||||
@IsNumber({}, { message: 'El ID del tipo de usuario debe ser un número' })
|
||||
@IsOptional()
|
||||
id_tipo_user?: number;
|
||||
}
|
||||
@@ -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[];
|
||||
}
|
||||
@@ -1,60 +1,42 @@
|
||||
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';
|
||||
import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiBody } from '@nestjs/swagger';
|
||||
import { ParticipanteApiDocumentation } from './participante.documentation';
|
||||
|
||||
@ApiTags('Participantes') // Agrupa los endpoints en Swagger
|
||||
@ParticipanteApiDocumentation.ApiController
|
||||
@Controller('participante')
|
||||
export class ParticipanteController {
|
||||
constructor(private participanteService: ParticipanteService) {}
|
||||
|
||||
@ApiOperation({ summary: 'Obtener todos los participantes' })
|
||||
@ApiResponse({ status: 200, description: 'Lista de participantes obtenida correctamente.' })
|
||||
@ParticipanteApiDocumentation.ApiGetAll
|
||||
@Get()
|
||||
getParticipantes(): Promise<Participante[]> {
|
||||
return this.participanteService.getParticipantes()
|
||||
return this.participanteService.getParticipantes();
|
||||
}
|
||||
|
||||
@ParticipanteApiDocumentation.ApiGetOne
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: 'Obtener un participante por ID' })
|
||||
@ApiParam({ name: 'id', description: 'ID del participante', example: 1 })
|
||||
@ApiResponse({ status: 200, description: 'Participante obtenido correctamente.' })
|
||||
@ApiResponse({ status: 404, description: 'Participante no encontrado.' })
|
||||
getParticipante(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.participanteService.getParticipante(id);
|
||||
}
|
||||
|
||||
@ParticipanteApiDocumentation.ApiCreate
|
||||
@Post()
|
||||
@ApiOperation({ summary: 'Registrar un nuevo participante' })
|
||||
@ApiBody({
|
||||
description: 'Datos del participante a registrar',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
correo: { type: 'string', example: 'user@example.com' },
|
||||
id_tipo_user: { type: 'integer', example: 2 }
|
||||
}
|
||||
}
|
||||
})
|
||||
@ApiResponse({ status: 201, description: 'Participante registrado exitosamente.' })
|
||||
@ApiResponse({ status: 400, description: 'Datos inválidos.' })
|
||||
createParticipante(@Body() newParticipante: CreateParticipanteDto) {
|
||||
return this.participanteService.createParticipante(newParticipante);
|
||||
}
|
||||
|
||||
@ParticipanteApiDocumentation.ApiRemove
|
||||
@Delete(':id')
|
||||
deleteParticipante(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.participanteService.deleteParticipante(id)
|
||||
return this.participanteService.deleteParticipante(id);
|
||||
}
|
||||
|
||||
|
||||
@ParticipanteApiDocumentation.ApiUpdate
|
||||
@Patch(':id')
|
||||
updateParticipante(@Param('correo') id: number, @Body() participante: UpdateParticipanteDto) {
|
||||
updateParticipante(@Param('id', ParseIntPipe) id: number, @Body() participante: UpdateParticipanteDto) {
|
||||
return this.participanteService.updateParticipante(id, participante);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
import { ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { applyDecorators } from '@nestjs/common';
|
||||
|
||||
export class ParticipanteApiDocumentation {
|
||||
// Decorador para toda la clase del controlador
|
||||
static ApiController = ApiTags('Participantes');
|
||||
|
||||
// Documentación para crear un participante
|
||||
static ApiCreate = applyDecorators(
|
||||
ApiOperation({
|
||||
summary: 'Registrar un nuevo participante',
|
||||
description: 'Crea un nuevo registro de participante en el sistema'
|
||||
}),
|
||||
ApiBody({
|
||||
description: 'Datos del participante a registrar',
|
||||
schema: {
|
||||
type: 'object',
|
||||
required: ['correo', 'id_tipo_user'],
|
||||
properties: {
|
||||
correo: {
|
||||
type: 'string',
|
||||
format: 'email',
|
||||
example: 'usuario@ejemplo.com',
|
||||
description: 'Correo electrónico del participante'
|
||||
},
|
||||
id_tipo_user: {
|
||||
type: 'integer',
|
||||
example: 1,
|
||||
description: 'ID del tipo de usuario'
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
ApiResponse({
|
||||
status: 201,
|
||||
description: 'Participante registrado exitosamente',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id_participante: { type: 'number', example: 1 },
|
||||
correo: { type: 'string', example: 'usuario@ejemplo.com' },
|
||||
id_tipo_user: { type: 'number', example: 1 }
|
||||
}
|
||||
}
|
||||
}),
|
||||
ApiResponse({ status: 400, description: 'Datos del participante inválidos' }),
|
||||
ApiResponse({ status: 409, description: 'El participante ya existe' })
|
||||
);
|
||||
|
||||
// Documentación para obtener todos los participantes
|
||||
static ApiGetAll = applyDecorators(
|
||||
ApiOperation({
|
||||
summary: 'Obtener todos los participantes',
|
||||
description: 'Retorna una lista de todos los participantes registrados'
|
||||
}),
|
||||
ApiResponse({
|
||||
status: 200,
|
||||
description: 'Lista de participantes obtenida correctamente',
|
||||
schema: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id_participante: { type: 'number', example: 1 },
|
||||
correo: { type: 'string', example: 'usuario@ejemplo.com' },
|
||||
id_tipo_user: { type: 'number', example: 1 },
|
||||
tipo_user: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id_tipo_user: { type: 'number', example: 1 },
|
||||
tipo_user: { type: 'string', example: 'Estudiante' }
|
||||
}
|
||||
},
|
||||
participanteEventos: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id_participante_evento: { type: 'number', example: 1 },
|
||||
id_participante: { type: 'number', example: 1 },
|
||||
id_evento: { type: 'number', example: 1 },
|
||||
fecha_inscripcion: { type: 'string', format: 'date-time', example: '2025-04-01T10:00:00Z' },
|
||||
estatus: { type: 'boolean', example: true }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Documentación para obtener un participante por ID
|
||||
static ApiGetOne = applyDecorators(
|
||||
ApiOperation({
|
||||
summary: 'Obtener un participante por ID',
|
||||
description: 'Retorna los datos de un participante específico según su ID'
|
||||
}),
|
||||
ApiParam({
|
||||
name: 'id',
|
||||
description: 'ID del participante',
|
||||
type: 'number',
|
||||
example: 1
|
||||
}),
|
||||
ApiResponse({
|
||||
status: 200,
|
||||
description: 'Participante obtenido correctamente',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id_participante: { type: 'number', example: 1 },
|
||||
correo: { type: 'string', example: 'usuario@ejemplo.com' },
|
||||
id_tipo_user: { type: 'number', example: 1 },
|
||||
tipo_user: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id_tipo_user: { type: 'number', example: 1 },
|
||||
tipo_user: { type: 'string', example: 'Estudiante' }
|
||||
}
|
||||
},
|
||||
participanteEventos: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id_participante_evento: { type: 'number', example: 1 },
|
||||
id_participante: { type: 'number', example: 1 },
|
||||
id_evento: { type: 'number', example: 1 },
|
||||
fecha_inscripcion: { type: 'string', format: 'date-time', example: '2025-04-01T10:00:00Z' },
|
||||
estatus: { type: 'boolean', example: true }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
ApiResponse({ status: 404, description: 'Participante no encontrado' })
|
||||
);
|
||||
|
||||
// Documentación para actualizar un participante
|
||||
static ApiUpdate = applyDecorators(
|
||||
ApiOperation({
|
||||
summary: 'Actualizar datos de un participante',
|
||||
description: 'Actualiza la información de un participante existente'
|
||||
}),
|
||||
ApiParam({
|
||||
name: 'id',
|
||||
description: 'ID del participante a actualizar',
|
||||
type: 'number',
|
||||
example: 1
|
||||
}),
|
||||
ApiBody({
|
||||
description: 'Datos a actualizar del participante',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
correo: {
|
||||
type: 'string',
|
||||
format: 'email',
|
||||
example: 'nuevo_correo@ejemplo.com',
|
||||
description: 'Nuevo correo electrónico del participante'
|
||||
},
|
||||
id_tipo_user: {
|
||||
type: 'integer',
|
||||
example: 2,
|
||||
description: 'Nuevo tipo de usuario'
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
ApiResponse({
|
||||
status: 200,
|
||||
description: 'Participante actualizado correctamente',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id_participante: { type: 'number', example: 1 },
|
||||
correo: { type: 'string', example: 'nuevo_correo@ejemplo.com' },
|
||||
id_tipo_user: { type: 'number', example: 2 }
|
||||
}
|
||||
}
|
||||
}),
|
||||
ApiResponse({ status: 400, description: 'Datos de actualización inválidos' }),
|
||||
ApiResponse({ status: 404, description: 'Participante no encontrado' })
|
||||
);
|
||||
|
||||
// Documentación para eliminar un participante
|
||||
static ApiRemove = applyDecorators(
|
||||
ApiOperation({
|
||||
summary: 'Eliminar un participante',
|
||||
description: 'Elimina permanentemente un participante por su ID'
|
||||
}),
|
||||
ApiParam({
|
||||
name: 'id',
|
||||
description: 'ID del participante a eliminar',
|
||||
type: 'number',
|
||||
example: 1
|
||||
}),
|
||||
ApiResponse({
|
||||
status: 200,
|
||||
description: 'Participante eliminado correctamente',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
affected: { type: 'number', example: 1 }
|
||||
}
|
||||
}
|
||||
}),
|
||||
ApiResponse({ status: 404, description: 'Participante no encontrado' })
|
||||
);
|
||||
}
|
||||
@@ -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[];
|
||||
|
||||
|
||||
}
|
||||
@@ -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])],
|
||||
|
||||
@@ -1,74 +1,180 @@
|
||||
import { HttpException, HttpStatus, Injectable } 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 { UpdateAdminDto } from 'src/admin/dto/update.admin.dto';
|
||||
import { UpdateParticipanteDto } from './dto/update.participante.dto';
|
||||
|
||||
@Injectable()
|
||||
export class ParticipanteService {
|
||||
constructor(
|
||||
@InjectRepository(Participante)
|
||||
private participanteRepository: Repository<Participante>,
|
||||
) {}
|
||||
|
||||
constructor(
|
||||
@InjectRepository(Participante) private participanteRepository: Repository<Participante>
|
||||
) {}
|
||||
/**
|
||||
* Crea un nuevo participante
|
||||
* @param participante Datos del participante a crear
|
||||
* @returns El participante creado
|
||||
* @throws ConflictException si ya existe un participante con el mismo correo
|
||||
*/
|
||||
async createParticipante(
|
||||
participante: CreateParticipanteDto,
|
||||
): Promise<Participante> {
|
||||
// Verificar si ya existe un participante con el mismo correo
|
||||
const participanteFound = await this.participanteRepository.findOne({
|
||||
where: {
|
||||
correo: participante.correo,
|
||||
},
|
||||
});
|
||||
|
||||
async createParticipante(participante: CreateParticipanteDto) {
|
||||
const participanteFound = await this.participanteRepository.findOne({
|
||||
where: {
|
||||
correo: participante.correo
|
||||
}
|
||||
})
|
||||
|
||||
if (participanteFound)
|
||||
return new HttpException('Participante already exists', HttpStatus.CONFLICT)
|
||||
|
||||
return this.participanteRepository.save(participante)
|
||||
if (participanteFound) {
|
||||
throw new ConflictException(
|
||||
`Ya existe un participante con el correo ${participante.correo}`,
|
||||
);
|
||||
}
|
||||
|
||||
getParticipantes() {
|
||||
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<Participante[]> {
|
||||
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<Participante> {
|
||||
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`,
|
||||
);
|
||||
}
|
||||
|
||||
async getParticipante(id_participante: number) {
|
||||
const participanteFound = await this.participanteRepository.findOne({
|
||||
where: {
|
||||
id_participante
|
||||
},
|
||||
relations: ['tipo_user', 'participanteEventos']
|
||||
})
|
||||
return participanteFound;
|
||||
}
|
||||
|
||||
if (!participanteFound)
|
||||
return new HttpException('Participante not found', HttpStatus.NOT_FOUND);
|
||||
|
||||
return participanteFound;
|
||||
async getParticipanteByCorreo(correo: string): Promise<Participante | null> {
|
||||
const participanteFound = await this.participanteRepository.findOne({
|
||||
where: {
|
||||
correo,
|
||||
},
|
||||
relations: ['tipo_user', 'participanteEventos'],
|
||||
});
|
||||
return participanteFound;
|
||||
}
|
||||
|
||||
async getParticipanteByCorreoOrFail(correo: string): Promise<Participante> {
|
||||
const participanteFound = await this.participanteRepository.findOne({
|
||||
where: {
|
||||
correo,
|
||||
},
|
||||
relations: ['tipo_user', 'participanteEventos'],
|
||||
});
|
||||
|
||||
if (!participanteFound) {
|
||||
throw new NotFoundException(
|
||||
`Participante con correo ${correo} no encontrado`,
|
||||
);
|
||||
}
|
||||
|
||||
async deleteParticipante(id_participante: number) {
|
||||
const result = await this.participanteRepository.delete({ id_participante })
|
||||
return participanteFound;
|
||||
}
|
||||
|
||||
if (result.affected === 0) {
|
||||
return new HttpException('Participante not found', HttpStatus.NOT_FOUND);
|
||||
}
|
||||
/**
|
||||
* 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 result;
|
||||
if (result.affected === 0) {
|
||||
throw new NotFoundException(
|
||||
`Participante con ID ${id_participante} no encontrado`,
|
||||
);
|
||||
}
|
||||
|
||||
async updateParticipante(id_participante: number, participante: UpdateParticipanteDto) {
|
||||
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) {
|
||||
return new HttpException('Participante not found', HttpStatus.NOT_FOUND)
|
||||
}
|
||||
/**
|
||||
* Actualiza los datos de un participante
|
||||
* @param id_participante ID del participante a actualizar
|
||||
* @param participante Datos actualizados del participante
|
||||
* @returns El participante actualizado
|
||||
* @throws NotFoundException si no se encuentra el participante
|
||||
* @throws BadRequestException si los datos son inválidos
|
||||
*/
|
||||
async updateParticipante(
|
||||
id_participante: number,
|
||||
participante: UpdateParticipanteDto,
|
||||
): Promise<Participante> {
|
||||
// Verificar si el participante existe
|
||||
const participanteFound = await this.participanteRepository.findOne({
|
||||
where: {
|
||||
id_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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { IsString, IsNotEmpty } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class RegistrarAsistenciaQrDto {
|
||||
@ApiProperty({
|
||||
description: 'Token JWT del código QR escaneado',
|
||||
example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
qr_token: string;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,36 +1,305 @@
|
||||
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 { RegistrarAsistenciaQrDto } from './dto/registrar-asistencia-qr.dto';
|
||||
import {
|
||||
ApiTags,
|
||||
ApiOperation,
|
||||
ApiParam,
|
||||
ApiResponse,
|
||||
ApiBody,
|
||||
} 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,
|
||||
);
|
||||
}
|
||||
|
||||
@Get()
|
||||
getParticipantesEvento(): Promise<ParticipanteEvento[]> {
|
||||
return this.participanteEventoService.getParticipantesEvento()
|
||||
}
|
||||
@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,
|
||||
);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
getParticipanteEvento(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.participanteEventoService.getParticipanteEvento(id)
|
||||
}
|
||||
@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,
|
||||
);
|
||||
}
|
||||
|
||||
@Post()
|
||||
createParticipanteEvento(@Body() newParticipanteEvento: CreateParticipanteEventoDto) {
|
||||
return this.participanteEventoService.createParticipanteEvento(newParticipanteEvento)
|
||||
}
|
||||
@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);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
deleteParticipanteEvento(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.participanteEventoService.deleteParticipanteEvento(id)
|
||||
}
|
||||
@ApiOperation({
|
||||
summary: 'Obtener todos los registros de participante-evento',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description:
|
||||
'Lista de todos los registros participante-evento con sus relaciones',
|
||||
schema: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id_participante_evento: { type: 'number' },
|
||||
id_participante: { type: 'number' },
|
||||
id_evento: { type: 'number' },
|
||||
fecha_inscripcion: { type: 'string', format: 'date-time' },
|
||||
estatus: { type: 'boolean' },
|
||||
fecha_asistencia: {
|
||||
type: 'string',
|
||||
format: 'date-time',
|
||||
nullable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
@Get()
|
||||
getParticipantesEvento(): Promise<ParticipanteEvento[]> {
|
||||
return this.participanteEventoService.getParticipantesEvento();
|
||||
}
|
||||
|
||||
@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: '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',
|
||||
})
|
||||
@ApiBody({
|
||||
description: 'Token JWT generado por el código QR',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
token: { type: 'string' },
|
||||
},
|
||||
example: {
|
||||
token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
|
||||
},
|
||||
},
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Asistencia registrada correctamente',
|
||||
})
|
||||
@ApiResponse({ status: 404, description: 'Registro no encontrado' })
|
||||
@Post('asistencia')
|
||||
registrarAsistencia(@Body() body: { token: string }) {
|
||||
return this.participanteEventoService.registrarAsistenciaConToken(
|
||||
body.token,
|
||||
);
|
||||
}
|
||||
|
||||
@ApiOperation({
|
||||
summary: 'Registrar asistencia usando token QR',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Asistencia registrada correctamente mediante token QR',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
success: { type: 'boolean' },
|
||||
message: { type: 'string' },
|
||||
data: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
participante: { type: 'string' },
|
||||
fecha_asistencia: { type: 'string', format: 'date-time' },
|
||||
id_evento: { type: 'number' },
|
||||
id_cuestionario: { type: 'number' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 400,
|
||||
description: 'Token QR inválido o expirado',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 404,
|
||||
description: 'Participante no encontrado o no registrado en el evento',
|
||||
})
|
||||
@Post('asistencia-qr')
|
||||
registrarAsistenciaConToken(@Body() body: { qr_token: string }) {
|
||||
return this.participanteEventoService.registrarAsistenciaConToken(
|
||||
body.qr_token,
|
||||
);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: 'Eliminar un registro participante-evento' })
|
||||
@ApiParam({ name: 'id', description: 'ID del participante', type: 'number' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Registro eliminado correctamente',
|
||||
})
|
||||
@ApiResponse({ status: 404, description: 'Registro no encontrado' })
|
||||
@Delete(':id')
|
||||
deleteParticipanteEvento(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.participanteEventoService.deleteParticipanteEvento(id);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: 'Actualizar un registro participante-evento' })
|
||||
@ApiParam({ name: 'id', description: 'ID del participante', type: 'number' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Registro actualizado correctamente',
|
||||
})
|
||||
@ApiResponse({ status: 404, description: 'Registro no encontrado' })
|
||||
@Patch(':id')
|
||||
updateParticipanteEvento(
|
||||
@Param('id', ParseIntPipe) id_participante: number,
|
||||
id_evento: number,
|
||||
@Body() participanteEvento: UpdateParticipanteEventoDto,
|
||||
) {
|
||||
return this.participanteEventoService.updateParticipanteEvento(
|
||||
id_participante,
|
||||
id_evento,
|
||||
participanteEvento,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,38 +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
|
||||
|
||||
/*
|
||||
@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;
|
||||
|
||||
}
|
||||
@@ -2,13 +2,19 @@ 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';
|
||||
import { QrModule } from 'src/qr/qr.module';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([ParticipanteEvento, Evento, Participante])],
|
||||
controllers: [ParticipanteEventoController],
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([ParticipanteEvento, Evento, Participante]),
|
||||
CuestionarioModule,
|
||||
QrModule,
|
||||
],
|
||||
controllers: [ParticipanteEventoController],
|
||||
providers: [ParticipanteEventoService],
|
||||
exports: [ParticipanteEventoService],
|
||||
})
|
||||
|
||||
@@ -1,81 +1,263 @@
|
||||
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';
|
||||
import { QrTokenService } from 'src/qr/qr-token.service';
|
||||
|
||||
@Injectable()
|
||||
export class ParticipanteEventoService {
|
||||
constructor(
|
||||
@InjectRepository(ParticipanteEvento)
|
||||
private participanteEventoRepository: Repository<ParticipanteEvento>,
|
||||
@InjectRepository(Evento) private eventoRepository: Repository<Evento>,
|
||||
@InjectRepository(Participante)
|
||||
private participanteRepository: Repository<Participante>,
|
||||
private readonly cuestionarioService: CuestionarioService,
|
||||
private readonly qrTokenService: QrTokenService,
|
||||
) {}
|
||||
|
||||
constructor(
|
||||
@InjectRepository(ParticipanteEvento) private participanteEventoRepository: Repository<ParticipanteEvento>,
|
||||
@InjectRepository(Evento) private eventoRepository: Repository<Evento>,
|
||||
@InjectRepository(Participante) private participanteRepository: Repository<Participante>
|
||||
) {}
|
||||
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 deleteParticipanteEvento(id_participante: number) {
|
||||
const result = await this.participanteEventoRepository.delete({ id_participante })
|
||||
return participante_eventoFound;
|
||||
}
|
||||
|
||||
if (result.affected === 0) {
|
||||
return new HttpException('Participante not found', 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 result
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
async updateParticipanteEvento(id_participante: number, id_evento: number, participante_evento: UpdateParticipanteEventoDto) {
|
||||
const participante_eventoFound = await this.participanteEventoRepository.findOne({
|
||||
where: {
|
||||
id_participante,
|
||||
id_evento
|
||||
}
|
||||
})
|
||||
// Obtener todos los registros participante-evento para este participante
|
||||
const eventosParticipante = await this.participanteEventoRepository.find({
|
||||
where: { id_participante },
|
||||
relations: ['evento'],
|
||||
});
|
||||
|
||||
if (!participante_eventoFound) {
|
||||
return new HttpException('Participante not found', HttpStatus.NOT_FOUND)
|
||||
}
|
||||
return eventosParticipante;
|
||||
}
|
||||
|
||||
return this.participanteEventoRepository.save(participante_evento)
|
||||
async deleteParticipanteEvento(id_participante: number) {
|
||||
const result = await this.participanteEventoRepository.delete({
|
||||
id_participante,
|
||||
});
|
||||
|
||||
if (result.affected === 0) {
|
||||
return new HttpException('Participante not found', HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
||||
if (!participante_eventoFound) {
|
||||
return new HttpException('Participante not found', HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
return this.participanteEventoRepository.save(participante_evento);
|
||||
}
|
||||
|
||||
async registrarAsistencia(
|
||||
id_participante: number,
|
||||
id_cuestionario: number,
|
||||
token: string,
|
||||
) {
|
||||
const participante_eventoFound =
|
||||
await this.participanteEventoRepository.findOne({
|
||||
where: {
|
||||
id_participante,
|
||||
cuestionario: { id_cuestionario },
|
||||
},
|
||||
});
|
||||
|
||||
if (!participante_eventoFound) {
|
||||
return new HttpException('Registro no encontrado', HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
participante_eventoFound.fecha_asistencia = new Date();
|
||||
participante_eventoFound.asistio = true;
|
||||
return this.participanteEventoRepository.save(participante_eventoFound);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registra la asistencia usando un token QR
|
||||
* @param qrToken Token JWT del código QR
|
||||
* @returns Resultado del registro de asistencia
|
||||
*/
|
||||
async registrarAsistenciaConToken(qrToken: string) {
|
||||
// Validar el token QR
|
||||
const tokenData = this.qrTokenService.validateQrToken(qrToken);
|
||||
|
||||
if (!tokenData) {
|
||||
throw new HttpException(
|
||||
'Token QR inválido o expirado',
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
const { id_participante, id_evento, id_cuestionario } = tokenData;
|
||||
|
||||
// Verificar que el participante existe
|
||||
const participante = await this.participanteRepository.findOne({
|
||||
where: { id_participante },
|
||||
});
|
||||
|
||||
if (!participante) {
|
||||
throw new HttpException(
|
||||
`Participante con ID ${id_participante} no encontrado`,
|
||||
HttpStatus.NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
// Buscar el registro participante-evento
|
||||
const participanteEvento = await this.participanteEventoRepository.findOne({
|
||||
where: {
|
||||
id_participante,
|
||||
id_cuestionario,
|
||||
},
|
||||
relations: ['participante'],
|
||||
});
|
||||
|
||||
if (!participanteEvento) {
|
||||
throw new HttpException(
|
||||
'El participante no está registrado en este evento',
|
||||
HttpStatus.NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
// Verificar si ya había registrado asistencia
|
||||
if (participanteEvento.asistio && participanteEvento.fecha_asistencia) {
|
||||
return {
|
||||
success: true,
|
||||
message:
|
||||
'El participante ya había registrado su asistencia previamente',
|
||||
data: {
|
||||
participante: participante.correo,
|
||||
fecha_asistencia_previa: participanteEvento.fecha_asistencia,
|
||||
fecha_actual: new Date(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Registrar la asistencia
|
||||
participanteEvento.fecha_asistencia = new Date();
|
||||
participanteEvento.asistio = true;
|
||||
|
||||
const resultado =
|
||||
await this.participanteEventoRepository.save(participanteEvento);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Asistencia registrada exitosamente',
|
||||
data: {
|
||||
participante: participante.correo,
|
||||
fecha_asistencia: resultado.fecha_asistencia,
|
||||
id_evento,
|
||||
id_cuestionario,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,99 @@
|
||||
import { Type } from 'class-transformer';
|
||||
import { IsArray, IsBoolean, IsNotEmpty, IsNumber, IsOptional, IsString, ValidateNested } from 'class-validator';
|
||||
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 class OpcionDto {
|
||||
@ApiProperty({
|
||||
description: 'Valor de la opción',
|
||||
example: 'Sí'
|
||||
})
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
valor: string;
|
||||
}
|
||||
|
||||
export class CreatePreguntaDto {
|
||||
@ApiProperty({
|
||||
description: 'Título o texto de la pregunta',
|
||||
example: '¿Eres parte de la comunidad de la FES Acatlán?'
|
||||
})
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
titulo: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Indica si la pregunta es obligatoria',
|
||||
example: true,
|
||||
default: false
|
||||
})
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
obligatoria?: boolean;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
tipo: string;
|
||||
@ApiProperty({
|
||||
description: 'Límite de caracteres para respuestas de texto (solo aplica a preguntas tipo Abierta)',
|
||||
example: 250,
|
||||
required: false
|
||||
})
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
limite?: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Tipo de pregunta',
|
||||
enum: Object.values(TipoPreguntaEnum),
|
||||
enumName: 'TipoPreguntaEnum',
|
||||
examples: {
|
||||
'cerrada': {
|
||||
summary: 'Pregunta de opción única',
|
||||
value: TipoPreguntaEnum.Cerrada
|
||||
},
|
||||
'abierta': {
|
||||
summary: 'Pregunta de texto libre',
|
||||
value: TipoPreguntaEnum.AbiertaParrafo
|
||||
},
|
||||
'multiple': {
|
||||
summary: 'Pregunta de selección múltiple',
|
||||
value: TipoPreguntaEnum.Multiple
|
||||
}
|
||||
}
|
||||
})
|
||||
@IsNotEmpty()
|
||||
@IsEnum(TipoPreguntaEnum, {
|
||||
message: 'El tipo debe ser uno de los siguientes valores: Cerrada, Abierta, Multiple'
|
||||
})
|
||||
tipo: TipoPreguntaEnum;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Contador de opciones (se calcula automáticamente)',
|
||||
required: false
|
||||
})
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
contador_opcion?: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Tipo de validación para la respuesta',
|
||||
required: false,
|
||||
examples: ['cuenta_alumno', 'correo', 'telefono', 'nombre', 'entero', 'decimal'],
|
||||
example: 'correo'
|
||||
})
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
id_opcion_dependiente?: number;
|
||||
@IsString()
|
||||
validacion?: TiposValidacion;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Lista de opciones para preguntas de tipo Cerrada o Multiple',
|
||||
type: [OpcionDto],
|
||||
required: false,
|
||||
examples: [
|
||||
{ valor: 'Sí' },
|
||||
{ valor: 'No' }
|
||||
]
|
||||
})
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
|
||||
@@ -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,19 +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;
|
||||
|
||||
@OneToMany(() => SeccionPregunta, seccionPregunta => seccionPregunta.pregunta)
|
||||
@OneToMany(
|
||||
() => SeccionPregunta,
|
||||
(seccionPregunta) => seccionPregunta.pregunta,
|
||||
)
|
||||
seccionPreguntas: SeccionPregunta[];
|
||||
|
||||
@OneToMany(() => PreguntaOpcion, preguntaOpcion => preguntaOpcion.pregunta)
|
||||
@OneToMany(() => PreguntaOpcion, (preguntaOpcion) => preguntaOpcion.pregunta)
|
||||
opciones: PreguntaOpcion[];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>(PreguntaController);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(controller).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -1,5 +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 'src/tipo_pregunta/entities/tipo_pregunta.entity';
|
||||
|
||||
export class PreguntaApiDocumentation {
|
||||
// Decoradores para toda la clase del controlador
|
||||
@@ -9,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',
|
||||
@@ -19,23 +26,69 @@ export class PreguntaApiDocumentation {
|
||||
properties: {
|
||||
titulo: { type: 'string', example: '¿Eres parte de la comunidad?' },
|
||||
obligatoria: { type: 'boolean', example: true },
|
||||
tipo: { type: 'string', example: 'Multiple' },
|
||||
tipo: {
|
||||
type: 'string',
|
||||
enum: Object.values(TipoPreguntaEnum),
|
||||
description:
|
||||
'Tipo de pregunta: Cerrada (opción única), Abierta (texto libre), Multiple (varias opciones)',
|
||||
example: TipoPreguntaEnum.Cerrada,
|
||||
},
|
||||
contador_opcion: { type: 'number', example: 0 },
|
||||
id_opcion_dependiente: { type: 'number', example: null },
|
||||
validacion: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Tipo de validación para la respuesta (cuenta_alumno, correo, telefono, nombre, entero, decimal, etc)',
|
||||
example: 'correo',
|
||||
},
|
||||
opciones: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
valor: { type: 'string', example: 'Si' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
valor: { type: 'string', example: 'Si' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
examples: {
|
||||
cerrada: {
|
||||
summary: 'Pregunta de opción única',
|
||||
value: {
|
||||
titulo: '¿Eres parte de la comunidad de la FES Acatlán?',
|
||||
obligatoria: true,
|
||||
tipo: TipoPreguntaEnum.Cerrada,
|
||||
validacion: null,
|
||||
opciones: [{ valor: 'Sí' }, { valor: 'No' }],
|
||||
},
|
||||
},
|
||||
abierta: {
|
||||
summary: 'Pregunta de texto libre',
|
||||
value: {
|
||||
titulo: '¿Qué mejorarías en nuestro servicio?',
|
||||
obligatoria: false,
|
||||
tipo: TipoPreguntaEnum.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,
|
||||
validacion: null,
|
||||
opciones: [
|
||||
{ valor: 'Rapidez' },
|
||||
{ valor: 'Atención al cliente' },
|
||||
{ valor: 'Facilidad de uso' },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
ApiResponse({
|
||||
status: 201,
|
||||
ApiResponse({
|
||||
status: 201,
|
||||
description: 'Pregunta creada correctamente',
|
||||
schema: {
|
||||
type: 'object',
|
||||
@@ -45,19 +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' },
|
||||
},
|
||||
},
|
||||
}),
|
||||
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,
|
||||
@@ -68,30 +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' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
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,
|
||||
@@ -104,36 +160,36 @@ 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',
|
||||
items: {
|
||||
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',
|
||||
@@ -141,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,
|
||||
@@ -151,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,
|
||||
@@ -179,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' }),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>(PreguntaService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -42,8 +42,8 @@ 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;
|
||||
|
||||
const savedPregunta = await queryRunner.manager.save(pregunta);
|
||||
|
||||
@@ -157,7 +157,7 @@ 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
|
||||
if (updatePreguntaDto.tipo) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Opcion } from 'src/opcion/entities/opcion.entity';
|
||||
import { Pregunta } from 'src/pregunta/entities/pregunta.entity';
|
||||
import { RespuestaParticipanteCerrada } from 'src/respuesta_participante_cerrada/entities/respuesta_participante_cerrada.entity';
|
||||
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, OneToMany } from 'typeorm';
|
||||
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, OneToMany, JoinColumn } from 'typeorm';
|
||||
|
||||
|
||||
@Entity()
|
||||
@@ -13,13 +13,18 @@ export class PreguntaOpcion {
|
||||
posicion: number;
|
||||
|
||||
@ManyToOne(() => Pregunta)
|
||||
@JoinColumn({ name: 'id_pregunta' })
|
||||
pregunta: Pregunta;
|
||||
|
||||
@Column()
|
||||
id_pregunta: number;
|
||||
|
||||
@ManyToOne(() => Opcion, opcion => opcion.preguntasOpciones)
|
||||
@JoinColumn({ name: 'id_opcion' })
|
||||
opcion: Opcion;
|
||||
|
||||
@Column()
|
||||
id_opcion: number;
|
||||
|
||||
@OneToMany(()=> RespuestaParticipanteCerrada, respuestaParticipanteCerrada=>respuestaParticipanteCerrada.preguntaOpcion)
|
||||
respuestaParticipanteCerrada:RespuestaParticipanteCerrada[];
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { IsString, IsNotEmpty } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class ValidateQrTokenDto {
|
||||
@ApiProperty({
|
||||
description: 'Token JWT del código QR',
|
||||
example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
|
||||
})
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
token: string;
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
@Injectable()
|
||||
export class QrTokenService {
|
||||
constructor(
|
||||
private jwtService: JwtService,
|
||||
private configService: ConfigService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Genera un token JWT específico para el QR de asistencia
|
||||
* @param id_participante ID del participante
|
||||
* @param id_evento ID del evento
|
||||
* @param id_cuestionario ID del cuestionario
|
||||
* @returns Token JWT codificado
|
||||
*/
|
||||
generateQrToken(
|
||||
id_participante: number,
|
||||
id_evento: number,
|
||||
id_cuestionario: number,
|
||||
): string {
|
||||
const payload = {
|
||||
id_participante,
|
||||
id_evento,
|
||||
id_cuestionario,
|
||||
type: 'qr_asistencia',
|
||||
iat: Math.floor(Date.now() / 1000),
|
||||
};
|
||||
|
||||
// Usar una clave secreta específica para QR o la misma que JWT general
|
||||
const secret =
|
||||
this.configService.get<string>('QR_JWT_SECRET') ||
|
||||
this.configService.get<string>('JWT_SECRET', 'tu_clave_secreta');
|
||||
|
||||
return this.jwtService.sign(payload, {
|
||||
secret,
|
||||
expiresIn: '30d', // El QR puede ser válido por 30 días
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Valida y decodifica un token de QR
|
||||
* @param token Token JWT a validar
|
||||
* @returns Datos decodificados del token o null si es inválido
|
||||
*/
|
||||
validateQrToken(token: string): {
|
||||
id_participante: number;
|
||||
id_evento: number;
|
||||
id_cuestionario: number;
|
||||
type: string;
|
||||
iat: number;
|
||||
} | null {
|
||||
try {
|
||||
const secret =
|
||||
this.configService.get<string>('QR_JWT_SECRET') ||
|
||||
this.configService.get<string>('JWT_SECRET', 'tu_clave_secreta');
|
||||
|
||||
const decoded = this.jwtService.verify(token, { secret });
|
||||
|
||||
// Verificar que sea un token de tipo QR de asistencia
|
||||
if (decoded.type !== 'qr_asistencia') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id_participante: decoded.id_participante,
|
||||
id_evento: decoded.id_evento,
|
||||
id_cuestionario: decoded.id_cuestionario,
|
||||
type: decoded.type,
|
||||
iat: decoded.iat,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error validando token QR:', error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica si un token QR es válido y no ha expirado
|
||||
* @param token Token a verificar
|
||||
* @returns true si es válido, false si no
|
||||
*/
|
||||
isTokenValid(token: string): boolean {
|
||||
const decoded = this.validateQrToken(token);
|
||||
return decoded !== null;
|
||||
}
|
||||
}
|
||||
+67
-2
@@ -10,14 +10,20 @@ import {
|
||||
Query,
|
||||
} from '@nestjs/common';
|
||||
import { QrService } from './qr.service';
|
||||
import { QrTokenService } from './qr-token.service';
|
||||
import { Qr } from './qr.entity';
|
||||
import { CreateQrDto } from './dto/create-qr.dto';
|
||||
import { UpdateQrDto } from './dto/update.qr.dto';
|
||||
import { ApiOperation, ApiQuery } from '@nestjs/swagger';
|
||||
import { ValidateQrTokenDto } from './dto/validate-qr-token.dto';
|
||||
import { ApiOperation, ApiParam, ApiQuery, ApiTags } from '@nestjs/swagger';
|
||||
|
||||
@ApiTags('QR')
|
||||
@Controller('qr')
|
||||
export class QrController {
|
||||
constructor(private qrService: QrService) {}
|
||||
constructor(
|
||||
private qrService: QrService,
|
||||
private qrTokenService: QrTokenService,
|
||||
) {}
|
||||
|
||||
@Get('generate')
|
||||
@ApiOperation({ summary: 'Genera un código QR a partir de un texto' })
|
||||
@@ -30,6 +36,65 @@ export class QrController {
|
||||
return this.qrService.generateQRCode(text);
|
||||
}
|
||||
|
||||
@Get('asistencia/:idParticipante/:idEvento')
|
||||
@ApiOperation({
|
||||
summary:
|
||||
'Genera un código QR para asistencia con IDs de participante y evento',
|
||||
})
|
||||
@ApiParam({ name: 'idParticipante', description: 'ID del participante' })
|
||||
@ApiParam({ name: 'idEvento', description: 'ID del evento' })
|
||||
async generateAsistenciaQR(
|
||||
@Param('idParticipante', ParseIntPipe) idParticipante: number,
|
||||
@Param('idEvento', ParseIntPipe) idEvento: number,
|
||||
): Promise<string> {
|
||||
return this.qrService.generateAsistenciaQR(idParticipante, idEvento);
|
||||
}
|
||||
|
||||
@Post('validate-token')
|
||||
@ApiOperation({ summary: 'Valida un token de QR para asistencia' })
|
||||
async validateQrToken(@Body() validateDto: ValidateQrTokenDto) {
|
||||
const tokenData = this.qrTokenService.validateQrToken(validateDto.token);
|
||||
|
||||
if (!tokenData) {
|
||||
return {
|
||||
valid: false,
|
||||
message: 'Token inválido o expirado',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
data: tokenData,
|
||||
message: 'Token válido',
|
||||
};
|
||||
}
|
||||
|
||||
@Get('generate-token/:idParticipante/:idEvento/:idCuestionario')
|
||||
@ApiOperation({ summary: 'Genera un token JWT para QR de asistencia' })
|
||||
@ApiParam({ name: 'idParticipante', description: 'ID del participante' })
|
||||
@ApiParam({ name: 'idEvento', description: 'ID del evento' })
|
||||
@ApiParam({ name: 'idCuestionario', description: 'ID del cuestionario' })
|
||||
async generateQrToken(
|
||||
@Param('idParticipante', ParseIntPipe) idParticipante: number,
|
||||
@Param('idEvento', ParseIntPipe) idEvento: number,
|
||||
@Param('idCuestionario', ParseIntPipe) idCuestionario: number,
|
||||
) {
|
||||
const token = this.qrTokenService.generateQrToken(
|
||||
idParticipante,
|
||||
idEvento,
|
||||
idCuestionario,
|
||||
);
|
||||
|
||||
return {
|
||||
token,
|
||||
qr_data: {
|
||||
id_participante: idParticipante,
|
||||
id_evento: idEvento,
|
||||
id_cuestionario: idCuestionario,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@Get()
|
||||
getQrs(): Promise<Qr[]> {
|
||||
return this.qrService.getQrs();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user