Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 297e9913bb | |||
| 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 | |||
| 2af1b23f4d | |||
| 7c3aba670e | |||
| c4d4fed43d | |||
| f9549529b9 | |||
| a16e82005e | |||
| a1f52daa8a | |||
| bf7c0e93a8 | |||
| 60c75f437f | |||
| 7b72ee4ec6 |
+18
@@ -0,0 +1,18 @@
|
|||||||
|
# Etapa de desarrollo para ejecutar en watch mode
|
||||||
|
FROM node:22-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Asegurarse de compilar el proyecto
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Puerto expuesto
|
||||||
|
EXPOSE 4204
|
||||||
|
|
||||||
|
# Comando para iniciar en modo desarrollo
|
||||||
|
CMD ["npm", "run", "start:dev"]
|
||||||
@@ -97,3 +97,25 @@ Nest is an MIT-licensed open source project. It can grow thanks to the sponsors
|
|||||||
## License
|
## License
|
||||||
|
|
||||||
Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE).
|
Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# usar validadores en el front end
|
||||||
|
|
||||||
|
import { validarRespuesta, FabricaValidadores } from
|
||||||
|
'./validaciones/validador';
|
||||||
|
|
||||||
|
// To validate a single response
|
||||||
|
const respuesta = 'user@example.com';
|
||||||
|
const tipoValidacion = 'correo';
|
||||||
|
const resultado = validarRespuesta(respuesta, tipoValidacion);
|
||||||
|
|
||||||
|
if (resultado.valido) {
|
||||||
|
// proceed
|
||||||
|
} else {
|
||||||
|
// show error message: resultado.mensaje
|
||||||
|
}
|
||||||
|
|
||||||
|
// Or directly use the validators
|
||||||
|
const validadorCorreo = new ValidadorCorreo();
|
||||||
|
const resultadoCorreo = validadorCorreo.validar('user@example.com');
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
mariadb:
|
||||||
|
image: mariadb:latest
|
||||||
|
container_name: formularios_db
|
||||||
|
ports:
|
||||||
|
- "4201:3306"
|
||||||
|
environment:
|
||||||
|
MARIADB_ROOT_PASSWORD: mypass
|
||||||
|
MARIADB_DATABASE: formularios_test
|
||||||
|
volumes:
|
||||||
|
- mariadb_data:/var/lib/mysql
|
||||||
|
- ./init-scripts:/docker-entrypoint-initdb.d
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
api:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: formularios_api
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
volumes:
|
||||||
|
- .:/app
|
||||||
|
- /app/node_modules
|
||||||
|
ports:
|
||||||
|
- "4204:4204"
|
||||||
|
depends_on:
|
||||||
|
- mariadb
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
front:
|
||||||
|
build: ../formularios_front
|
||||||
|
container_name: formularios_front
|
||||||
|
restart: always
|
||||||
|
depends_on:
|
||||||
|
- api
|
||||||
|
env_file:
|
||||||
|
- ../formularios_front/.env
|
||||||
|
ports:
|
||||||
|
- "4202:3000"
|
||||||
|
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mariadb_data:
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
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
|
||||||
@@ -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');
|
||||||
Generated
+422
-18
@@ -9,13 +9,19 @@
|
|||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nestjs/common": "^11.0.1",
|
"@nestjs/common": "^11.0.12",
|
||||||
"@nestjs/config": "^4.0.2",
|
"@nestjs/config": "^4.0.2",
|
||||||
"@nestjs/core": "^11.0.1",
|
"@nestjs/core": "^11.0.1",
|
||||||
"@nestjs/mapped-types": "*",
|
"@nestjs/mapped-types": "*",
|
||||||
"@nestjs/platform-express": "^11.0.1",
|
"@nestjs/platform-express": "^11.0.1",
|
||||||
|
"@nestjs/swagger": "^11.1.0",
|
||||||
"@nestjs/typeorm": "^11.0.0",
|
"@nestjs/typeorm": "^11.0.0",
|
||||||
|
"axios": "^1.8.4",
|
||||||
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.1",
|
"class-validator": "^0.14.1",
|
||||||
|
"dotenv": "^16.4.7",
|
||||||
|
"mysql2": "^3.14.0",
|
||||||
|
"qrcode": "^1.5.4",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"typeorm": "^0.3.21"
|
"typeorm": "^0.3.21"
|
||||||
@@ -31,6 +37,7 @@
|
|||||||
"@types/express": "^5.0.0",
|
"@types/express": "^5.0.0",
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/node": "^22.10.7",
|
"@types/node": "^22.10.7",
|
||||||
|
"@types/qrcode": "^1.5.5",
|
||||||
"@types/supertest": "^6.0.2",
|
"@types/supertest": "^6.0.2",
|
||||||
"eslint": "^9.18.0",
|
"eslint": "^9.18.0",
|
||||||
"eslint-config-prettier": "^10.0.1",
|
"eslint-config-prettier": "^10.0.1",
|
||||||
@@ -44,7 +51,7 @@
|
|||||||
"ts-loader": "^9.5.2",
|
"ts-loader": "^9.5.2",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"tsconfig-paths": "^4.2.0",
|
"tsconfig-paths": "^4.2.0",
|
||||||
"typescript": "^5.7.3",
|
"typescript": "^5.8.2",
|
||||||
"typescript-eslint": "^8.20.0"
|
"typescript-eslint": "^8.20.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1906,6 +1913,12 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@microsoft/tsdoc": {
|
||||||
|
"version": "0.15.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.15.1.tgz",
|
||||||
|
"integrity": "sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@napi-rs/nice": {
|
"node_modules/@napi-rs/nice": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/nice/-/nice-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@napi-rs/nice/-/nice-1.0.1.tgz",
|
||||||
@@ -2495,6 +2508,39 @@
|
|||||||
"tslib": "^2.1.0"
|
"tslib": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@microsoft/tsdoc": "0.15.1",
|
||||||
|
"@nestjs/mapped-types": "2.1.0",
|
||||||
|
"js-yaml": "4.1.0",
|
||||||
|
"lodash": "4.17.21",
|
||||||
|
"path-to-regexp": "8.2.0",
|
||||||
|
"swagger-ui-dist": "5.20.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@fastify/static": "^8.0.0",
|
||||||
|
"@nestjs/common": "^11.0.1",
|
||||||
|
"@nestjs/core": "^11.0.1",
|
||||||
|
"class-transformer": "*",
|
||||||
|
"class-validator": "*",
|
||||||
|
"reflect-metadata": "^0.1.12 || ^0.2.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@fastify/static": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"class-transformer": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"class-validator": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@nestjs/testing": {
|
"node_modules/@nestjs/testing": {
|
||||||
"version": "11.0.12",
|
"version": "11.0.12",
|
||||||
"resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-11.0.12.tgz",
|
"resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-11.0.12.tgz",
|
||||||
@@ -2613,6 +2659,13 @@
|
|||||||
"url": "https://opencollective.com/unts"
|
"url": "https://opencollective.com/unts"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@scarf/scarf": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
"node_modules/@sec-ant/readable-stream": {
|
"node_modules/@sec-ant/readable-stream": {
|
||||||
"version": "0.4.1",
|
"version": "0.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz",
|
||||||
@@ -3231,6 +3284,16 @@
|
|||||||
"undici-types": "~6.20.0"
|
"undici-types": "~6.20.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/qrcode": {
|
||||||
|
"version": "1.5.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.5.tgz",
|
||||||
|
"integrity": "sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/qs": {
|
"node_modules/@types/qs": {
|
||||||
"version": "6.9.18",
|
"version": "6.9.18",
|
||||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz",
|
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz",
|
||||||
@@ -4115,7 +4178,6 @@
|
|||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||||
"dev": true,
|
|
||||||
"license": "Python-2.0"
|
"license": "Python-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/array-timsort": {
|
"node_modules/array-timsort": {
|
||||||
@@ -4143,9 +4205,28 @@
|
|||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/aws-ssl-profiles": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/axios": {
|
||||||
|
"version": "1.8.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz",
|
||||||
|
"integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"follow-redirects": "^1.15.6",
|
||||||
|
"form-data": "^4.0.0",
|
||||||
|
"proxy-from-env": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/b4a": {
|
"node_modules/b4a": {
|
||||||
"version": "1.6.7",
|
"version": "1.6.7",
|
||||||
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz",
|
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz",
|
||||||
@@ -4636,7 +4717,6 @@
|
|||||||
"version": "5.3.1",
|
"version": "5.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
|
||||||
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
|
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
@@ -4746,6 +4826,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/class-transformer": {
|
||||||
|
"version": "0.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz",
|
||||||
|
"integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/class-validator": {
|
"node_modules/class-validator": {
|
||||||
"version": "0.14.1",
|
"version": "0.14.1",
|
||||||
"resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.1.tgz",
|
"resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.1.tgz",
|
||||||
@@ -4911,7 +4997,6 @@
|
|||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"delayed-stream": "~1.0.0"
|
"delayed-stream": "~1.0.0"
|
||||||
@@ -5154,6 +5239,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/decamelize": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/decompress-response": {
|
"node_modules/decompress-response": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
||||||
@@ -5242,12 +5336,20 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.4.0"
|
"node": ">=0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/denque": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/depd": {
|
"node_modules/depd": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||||
@@ -5308,6 +5410,12 @@
|
|||||||
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dijkstrajs": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/dotenv": {
|
"node_modules/dotenv": {
|
||||||
"version": "16.4.7",
|
"version": "16.4.7",
|
||||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
|
||||||
@@ -5477,7 +5585,6 @@
|
|||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"es-errors": "^1.3.0",
|
"es-errors": "^1.3.0",
|
||||||
@@ -6255,6 +6362,26 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/follow-redirects": {
|
||||||
|
"version": "1.15.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
||||||
|
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"debug": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/foreground-child": {
|
"node_modules/foreground-child": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
|
||||||
@@ -6368,7 +6495,6 @@
|
|||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
|
||||||
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
|
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"asynckit": "^0.4.0",
|
"asynckit": "^0.4.0",
|
||||||
@@ -6394,7 +6520,6 @@
|
|||||||
"version": "1.52.0",
|
"version": "1.52.0",
|
||||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
@@ -6404,7 +6529,6 @@
|
|||||||
"version": "2.1.35",
|
"version": "2.1.35",
|
||||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"mime-db": "1.52.0"
|
"mime-db": "1.52.0"
|
||||||
@@ -6499,6 +6623,15 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/generate-function": {
|
||||||
|
"version": "2.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
|
||||||
|
"integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"is-property": "^1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/gensync": {
|
"node_modules/gensync": {
|
||||||
"version": "1.0.0-beta.2",
|
"version": "1.0.0-beta.2",
|
||||||
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
||||||
@@ -6749,7 +6882,6 @@
|
|||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"has-symbols": "^1.0.3"
|
"has-symbols": "^1.0.3"
|
||||||
@@ -7077,6 +7209,12 @@
|
|||||||
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
|
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/is-property": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/is-stream": {
|
"node_modules/is-stream": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
|
||||||
@@ -7912,7 +8050,6 @@
|
|||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"argparse": "^2.0.1"
|
"argparse": "^2.0.1"
|
||||||
@@ -8125,6 +8262,12 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/long": {
|
||||||
|
"version": "5.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/long/-/long-5.3.1.tgz",
|
||||||
|
"integrity": "sha512-ka87Jz3gcx/I7Hal94xaN2tZEOPoUOEVftkQqZx2EeQRN7LGdfLlI3FvZ+7WDplm+vK2Urx9ULrvSowtdCieng==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
"node_modules/lowercase-keys": {
|
"node_modules/lowercase-keys": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz",
|
||||||
@@ -8148,6 +8291,21 @@
|
|||||||
"yallist": "^3.0.2"
|
"yallist": "^3.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lru.min": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"bun": ">=1.0.0",
|
||||||
|
"deno": ">=1.30.0",
|
||||||
|
"node": ">=8.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/wellwelwel"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/magic-string": {
|
"node_modules/magic-string": {
|
||||||
"version": "0.30.17",
|
"version": "0.30.17",
|
||||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
|
||||||
@@ -8464,6 +8622,59 @@
|
|||||||
"node": "^18.17.0 || >=20.5.0"
|
"node": "^18.17.0 || >=20.5.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mysql2": {
|
||||||
|
"version": "3.14.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.14.0.tgz",
|
||||||
|
"integrity": "sha512-8eMhmG6gt/hRkU1G+8KlGOdQi2w+CgtNoD1ksXZq9gQfkfDsX4LHaBwTe1SY0Imx//t2iZA03DFnyYKPinxSRw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"aws-ssl-profiles": "^1.1.1",
|
||||||
|
"denque": "^2.1.0",
|
||||||
|
"generate-function": "^2.3.1",
|
||||||
|
"iconv-lite": "^0.6.3",
|
||||||
|
"long": "^5.2.1",
|
||||||
|
"lru.min": "^1.0.0",
|
||||||
|
"named-placeholders": "^1.1.3",
|
||||||
|
"seq-queue": "^0.0.5",
|
||||||
|
"sqlstring": "^2.3.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mysql2/node_modules/iconv-lite": {
|
||||||
|
"version": "0.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||||
|
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/named-placeholders": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"lru-cache": "^7.14.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/named-placeholders/node_modules/lru-cache": {
|
||||||
|
"version": "7.18.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
|
||||||
|
"integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/natural-compare": {
|
"node_modules/natural-compare": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
|
||||||
@@ -8733,7 +8944,6 @@
|
|||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
|
||||||
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
|
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
@@ -8790,7 +9000,6 @@
|
|||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||||
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
|
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
@@ -9008,6 +9217,15 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pngjs": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.13.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/prelude-ls": {
|
"node_modules/prelude-ls": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||||
@@ -9108,6 +9326,12 @@
|
|||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/proxy-from-env": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/punycode": {
|
"node_modules/punycode": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||||
@@ -9135,6 +9359,148 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/qrcode": {
|
||||||
|
"version": "1.5.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
|
||||||
|
"integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"dijkstrajs": "^1.0.1",
|
||||||
|
"pngjs": "^5.0.0",
|
||||||
|
"yargs": "^15.3.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"qrcode": "bin/qrcode"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.13.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/qrcode/node_modules/ansi-regex": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/qrcode/node_modules/cliui": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"string-width": "^4.2.0",
|
||||||
|
"strip-ansi": "^6.0.0",
|
||||||
|
"wrap-ansi": "^6.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/qrcode/node_modules/find-up": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"locate-path": "^5.0.0",
|
||||||
|
"path-exists": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/qrcode/node_modules/locate-path": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"p-locate": "^4.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/qrcode/node_modules/p-limit": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"p-try": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/qrcode/node_modules/p-locate": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"p-limit": "^2.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/qrcode/node_modules/strip-ansi": {
|
||||||
|
"version": "6.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||||
|
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-regex": "^5.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/qrcode/node_modules/y18n": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/qrcode/node_modules/yargs": {
|
||||||
|
"version": "15.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
|
||||||
|
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cliui": "^6.0.0",
|
||||||
|
"decamelize": "^1.2.0",
|
||||||
|
"find-up": "^4.1.0",
|
||||||
|
"get-caller-file": "^2.0.1",
|
||||||
|
"require-directory": "^2.1.1",
|
||||||
|
"require-main-filename": "^2.0.0",
|
||||||
|
"set-blocking": "^2.0.0",
|
||||||
|
"string-width": "^4.2.0",
|
||||||
|
"which-module": "^2.0.0",
|
||||||
|
"y18n": "^4.0.0",
|
||||||
|
"yargs-parser": "^18.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/qrcode/node_modules/yargs-parser": {
|
||||||
|
"version": "18.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
|
||||||
|
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"camelcase": "^5.0.0",
|
||||||
|
"decamelize": "^1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/qs": {
|
"node_modules/qs": {
|
||||||
"version": "6.13.0",
|
"version": "6.13.0",
|
||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
||||||
@@ -9307,6 +9673,12 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/require-main-filename": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/resolve": {
|
"node_modules/resolve": {
|
||||||
"version": "1.22.10",
|
"version": "1.22.10",
|
||||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
|
||||||
@@ -9637,6 +10009,11 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/seq-queue": {
|
||||||
|
"version": "0.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz",
|
||||||
|
"integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="
|
||||||
|
},
|
||||||
"node_modules/serialize-javascript": {
|
"node_modules/serialize-javascript": {
|
||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
|
||||||
@@ -9662,6 +10039,12 @@
|
|||||||
"node": ">= 18"
|
"node": ">= 18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/set-blocking": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/setprototypeof": {
|
"node_modules/setprototypeof": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||||
@@ -9883,6 +10266,15 @@
|
|||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/sqlstring": {
|
||||||
|
"version": "2.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz",
|
||||||
|
"integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/stack-utils": {
|
"node_modules/stack-utils": {
|
||||||
"version": "2.0.6",
|
"version": "2.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
|
||||||
@@ -10220,6 +10612,15 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@scarf/scarf": "=1.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/symbol-observable": {
|
"node_modules/symbol-observable": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz",
|
||||||
@@ -11459,6 +11860,12 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/which-module": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/word-wrap": {
|
"node_modules/word-wrap": {
|
||||||
"version": "1.2.5",
|
"version": "1.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
||||||
@@ -11473,7 +11880,6 @@
|
|||||||
"version": "6.2.0",
|
"version": "6.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
||||||
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
|
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ansi-styles": "^4.0.0",
|
"ansi-styles": "^4.0.0",
|
||||||
@@ -11527,7 +11933,6 @@
|
|||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
@@ -11537,7 +11942,6 @@
|
|||||||
"version": "6.0.1",
|
"version": "6.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ansi-regex": "^5.0.1"
|
"ansi-regex": "^5.0.1"
|
||||||
|
|||||||
+10
-3
@@ -8,7 +8,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "nest build",
|
"build": "nest build",
|
||||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
"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:dev": "nest start --watch",
|
||||||
"start:debug": "nest start --debug --watch",
|
"start:debug": "nest start --debug --watch",
|
||||||
"start:prod": "node dist/main",
|
"start:prod": "node dist/main",
|
||||||
@@ -20,13 +20,19 @@
|
|||||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nestjs/common": "^11.0.1",
|
"@nestjs/common": "^11.0.12",
|
||||||
"@nestjs/config": "^4.0.2",
|
"@nestjs/config": "^4.0.2",
|
||||||
"@nestjs/core": "^11.0.1",
|
"@nestjs/core": "^11.0.1",
|
||||||
"@nestjs/mapped-types": "*",
|
"@nestjs/mapped-types": "*",
|
||||||
"@nestjs/platform-express": "^11.0.1",
|
"@nestjs/platform-express": "^11.0.1",
|
||||||
|
"@nestjs/swagger": "^11.1.0",
|
||||||
"@nestjs/typeorm": "^11.0.0",
|
"@nestjs/typeorm": "^11.0.0",
|
||||||
|
"axios": "^1.8.4",
|
||||||
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.1",
|
"class-validator": "^0.14.1",
|
||||||
|
"dotenv": "^16.4.7",
|
||||||
|
"mysql2": "^3.14.0",
|
||||||
|
"qrcode": "^1.5.4",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"typeorm": "^0.3.21"
|
"typeorm": "^0.3.21"
|
||||||
@@ -42,6 +48,7 @@
|
|||||||
"@types/express": "^5.0.0",
|
"@types/express": "^5.0.0",
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/node": "^22.10.7",
|
"@types/node": "^22.10.7",
|
||||||
|
"@types/qrcode": "^1.5.5",
|
||||||
"@types/supertest": "^6.0.2",
|
"@types/supertest": "^6.0.2",
|
||||||
"eslint": "^9.18.0",
|
"eslint": "^9.18.0",
|
||||||
"eslint-config-prettier": "^10.0.1",
|
"eslint-config-prettier": "^10.0.1",
|
||||||
@@ -55,7 +62,7 @@
|
|||||||
"ts-loader": "^9.5.2",
|
"ts-loader": "^9.5.2",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"tsconfig-paths": "^4.2.0",
|
"tsconfig-paths": "^4.2.0",
|
||||||
"typescript": "^5.7.3",
|
"typescript": "^5.8.2",
|
||||||
"typescript-eslint": "^8.20.0"
|
"typescript-eslint": "^8.20.0"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { Controller, Get, Param, NotFoundException } from '@nestjs/common';
|
||||||
|
import { AlumnosService } from './alumnos.service';
|
||||||
|
import { ApiTags, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger';
|
||||||
|
import { AlumnoDto, ALUMNO_RESPONSES } from './alumnos.documentation';
|
||||||
|
|
||||||
|
@ApiTags('alumnos')
|
||||||
|
@Controller('alumnos')
|
||||||
|
export class AlumnosController {
|
||||||
|
constructor(private readonly alumnosService: AlumnosService) {}
|
||||||
|
|
||||||
|
@Get(':cuenta')
|
||||||
|
@ApiOperation({ summary: 'Obtener datos de un alumno por su número de cuenta' })
|
||||||
|
@ApiParam({ name: 'cuenta', description: 'Número de cuenta del alumno' })
|
||||||
|
@ApiResponse(ALUMNO_RESPONSES[200])
|
||||||
|
@ApiResponse(ALUMNO_RESPONSES[404])
|
||||||
|
@ApiResponse(ALUMNO_RESPONSES[500])
|
||||||
|
async getAlumnoByCuenta(@Param('cuenta') cuenta: string): Promise<AlumnoDto | null> {
|
||||||
|
const alumno = await this.alumnosService.findByCuenta(cuenta);
|
||||||
|
if (!alumno) {
|
||||||
|
throw new NotFoundException(`No se encontró alumno con número de cuenta ${cuenta}`);
|
||||||
|
}
|
||||||
|
return alumno;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,53 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { AlumnosController } from './alumnos.controller';
|
||||||
|
import { AlumnosService } from './alumnos.service';
|
||||||
|
import { RegistroAlumno } from './entities/registro-alumno.entity';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([RegistroAlumno], 'alumnosConnection'),
|
||||||
|
|
||||||
|
|
||||||
|
TypeOrmModule.forRootAsync({
|
||||||
|
name: 'alumnosConnection',
|
||||||
|
useFactory: (configService: ConfigService) => ({
|
||||||
|
type: 'mysql',
|
||||||
|
host: configService.get<string>('ALUMNOS_DB_HOST'),
|
||||||
|
port: Number(configService.get<number>('ALUMNOS_DB_PORT')),
|
||||||
|
username: configService.get<string>('ALUMNOS_DB_USERNAME'),
|
||||||
|
password: configService.get<string>('ALUMNOS_DB_PASSWORD'),
|
||||||
|
database: configService.get<string>('ALUMNOS_DB_DATABASE'),
|
||||||
|
entities: [RegistroAlumno],
|
||||||
|
synchronize: false,
|
||||||
|
retryAttempts: 10,
|
||||||
|
retryDelay: 3000,
|
||||||
|
connectTimeout: 30000,
|
||||||
|
}),
|
||||||
|
inject: [ConfigService],
|
||||||
|
}),
|
||||||
|
|
||||||
|
|
||||||
|
/* TypeOrmModule.forRoot({
|
||||||
|
name: 'alumnosConnection',
|
||||||
|
type: 'mysql',
|
||||||
|
host: process.env.ALUMNOS_DB_HOST ,
|
||||||
|
port: Number(process.env.ALUMNOS_DB_PORT),
|
||||||
|
username: process.env.ALUMNOS_DB_USERNAME ,
|
||||||
|
password: process.env.ALUMNOS_DB_PASSWORD ,
|
||||||
|
database: process.env.ALUMNOS_DB_DATABASE ,
|
||||||
|
entities: [RegistroAlumno],
|
||||||
|
synchronize: false,
|
||||||
|
|
||||||
|
retryAttempts: 10, // Intentos para reconectar
|
||||||
|
retryDelay: 3000, // Tiempo entre reintentos
|
||||||
|
connectTimeout: 30000, // Timeout de conexión (30 segundos) │ │
|
||||||
|
|
||||||
|
}), */
|
||||||
|
],
|
||||||
|
controllers: [AlumnosController],
|
||||||
|
providers: [AlumnosService],
|
||||||
|
exports: [AlumnosService],
|
||||||
|
})
|
||||||
|
export class AlumnosModule {}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { RegistroAlumno } from './entities/registro-alumno.entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AlumnosService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(RegistroAlumno, 'alumnosConnection')
|
||||||
|
private registroAlumnoRepository: Repository<RegistroAlumno>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async findByCuenta(cuenta: string): Promise<RegistroAlumno | null> {
|
||||||
|
return this.registroAlumnoRepository.findOne({
|
||||||
|
where: { id_ncuenta: parseInt(cuenta, 10) },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
+43
-3
@@ -1,3 +1,4 @@
|
|||||||
|
//import { Module } from '@nestjs/common;
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { AppController } from './app.controller';
|
import { AppController } from './app.controller';
|
||||||
import { AppService } from './app.service';
|
import { AppService } from './app.service';
|
||||||
@@ -15,14 +16,26 @@ import { CuestionarioRespondidoModule } from './cuestionario_respondido/cuestion
|
|||||||
import { RespuestaParticipanteAbiertaModule } from './respuesta_participante_abierta/respuesta_participante_abierta.module';
|
import { RespuestaParticipanteAbiertaModule } from './respuesta_participante_abierta/respuesta_participante_abierta.module';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { ConfigModule } from '@nestjs/config';
|
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';
|
||||||
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
ConfigModule.forRoot(),
|
ConfigModule.forRoot({
|
||||||
|
isGlobal: true,
|
||||||
|
}),
|
||||||
TypeOrmModule.forRoot({
|
TypeOrmModule.forRoot({
|
||||||
|
|
||||||
type: 'mysql',
|
type: 'mysql',
|
||||||
host: process.env.db_host,
|
host: process.env.db_host, //process.env.db_host
|
||||||
username: process.env.db_username,
|
username: process.env.db_username,
|
||||||
database: process.env.db_database,
|
database: process.env.db_database,
|
||||||
password: process.env.db_password,
|
password: process.env.db_password,
|
||||||
@@ -32,6 +45,23 @@ import { ConfigModule } from '@nestjs/config';
|
|||||||
// logging: true, // Habilita los logs para depuración
|
// logging: true, // Habilita los logs para depuración
|
||||||
autoLoadEntities: true, // Carga automáticamente las entidades
|
autoLoadEntities: true, // Carga automáticamente las entidades
|
||||||
|
|
||||||
|
//esta es mi base de datos local
|
||||||
|
|
||||||
|
/*
|
||||||
|
type: 'mysql',
|
||||||
|
host: 'localhost',
|
||||||
|
port: 3306, //3306
|
||||||
|
username: 'root',
|
||||||
|
password: 'admin', //admin
|
||||||
|
database: 'cidwa', //nestdb
|
||||||
|
*/
|
||||||
|
// entities: [__dirname + '/**/*.entity{.ts,.js}'],
|
||||||
|
// synchronize: true,
|
||||||
|
|
||||||
|
|
||||||
|
//extra
|
||||||
|
retryAttempts: 10, // Intentos para reconectar
|
||||||
|
retryDelay: 3000, // Tiempo entre reintentos
|
||||||
}),
|
}),
|
||||||
CuestionarioModule,
|
CuestionarioModule,
|
||||||
TipoCuestionarioModule,
|
TipoCuestionarioModule,
|
||||||
@@ -43,7 +73,17 @@ import { ConfigModule } from '@nestjs/config';
|
|||||||
OpcionModule, PreguntaOpcionModule,
|
OpcionModule, PreguntaOpcionModule,
|
||||||
RespuestaParticipanteCerradaModule,
|
RespuestaParticipanteCerradaModule,
|
||||||
CuestionarioRespondidoModule,
|
CuestionarioRespondidoModule,
|
||||||
RespuestaParticipanteAbiertaModule],
|
RespuestaParticipanteAbiertaModule,
|
||||||
|
EventoModule,
|
||||||
|
TipoUserModule,
|
||||||
|
ParticipanteModule,
|
||||||
|
QrModule,
|
||||||
|
AdministradorModule,
|
||||||
|
AsistenciaModule,
|
||||||
|
ParticipanteEventoModule,
|
||||||
|
ValidacionesModule,
|
||||||
|
AlumnosModule,
|
||||||
|
],
|
||||||
|
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [AppService],
|
providers: [AppService],
|
||||||
|
|||||||
@@ -2,32 +2,46 @@ import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/commo
|
|||||||
import { CuestionarioService } from './cuestionario.service';
|
import { CuestionarioService } from './cuestionario.service';
|
||||||
import { CreateCuestionarioDto } from './dto/create-cuestionario.dto';
|
import { CreateCuestionarioDto } from './dto/create-cuestionario.dto';
|
||||||
import { UpdateCuestionarioDto } from './dto/update-cuestionario.dto';
|
import { UpdateCuestionarioDto } from './dto/update-cuestionario.dto';
|
||||||
|
import { CuestionarioApiDocumentation } from './cuestionario.documentation';
|
||||||
|
import { ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger';
|
||||||
|
|
||||||
@Controller('cuestionario')
|
@Controller('cuestionario')
|
||||||
|
@CuestionarioApiDocumentation.ApiController
|
||||||
export class CuestionarioController {
|
export class CuestionarioController {
|
||||||
constructor(private readonly cuestionarioService: CuestionarioService) {}
|
constructor(private readonly cuestionarioService: CuestionarioService) {}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
|
@CuestionarioApiDocumentation.ApiCreate
|
||||||
create(@Body() createCuestionarioDto: CreateCuestionarioDto) {
|
create(@Body() createCuestionarioDto: CreateCuestionarioDto) {
|
||||||
return this.cuestionarioService.create(createCuestionarioDto);
|
return this.cuestionarioService.create(createCuestionarioDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
|
@CuestionarioApiDocumentation.ApiGetAll
|
||||||
findAll() {
|
findAll() {
|
||||||
return this.cuestionarioService.findAll();
|
return this.cuestionarioService.findAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
|
@CuestionarioApiDocumentation.ApiGetOne
|
||||||
findOne(@Param('id') id: string) {
|
findOne(@Param('id') id: string) {
|
||||||
return this.cuestionarioService.findOne(+id);
|
return this.cuestionarioService.findOne(+id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get(':id/formulario')
|
||||||
|
@CuestionarioApiDocumentation.ApiGetFormulario
|
||||||
|
findFormulario(@Param('id') id: string) {
|
||||||
|
return this.cuestionarioService.findCompleto(+id);
|
||||||
|
}
|
||||||
|
|
||||||
@Patch(':id')
|
@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);
|
return this.cuestionarioService.update(+id, updateCuestionarioDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
|
@CuestionarioApiDocumentation.ApiRemove
|
||||||
remove(@Param('id') id: string) {
|
remove(@Param('id') id: string) {
|
||||||
return this.cuestionarioService.remove(+id);
|
return this.cuestionarioService.remove(+id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,210 @@
|
|||||||
|
import { ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags, getSchemaPath } from '@nestjs/swagger';
|
||||||
|
import { feriaSexualidad } from '../../utils/crear_formulario_feria';
|
||||||
|
import { applyDecorators } from '@nestjs/common';
|
||||||
|
import { FormularioDto, FormularioFeriaSchema } from './dto/formulario.schema';
|
||||||
|
|
||||||
|
export class CuestionarioApiDocumentation {
|
||||||
|
// Decoradores para toda la clase del controlador
|
||||||
|
static ApiController = ApiTags('Cuestionarios');
|
||||||
|
|
||||||
|
// Documentación para obtener el formulario completo
|
||||||
|
static ApiGetFormulario = applyDecorators(
|
||||||
|
ApiOperation({
|
||||||
|
summary: 'Obtener formulario completo',
|
||||||
|
description: 'Devuelve el cuestionario completo con la misma estructura que se usa para crearlo, incluyendo el atributo validacion para las preguntas que tengan reglas de validación específicas'
|
||||||
|
}),
|
||||||
|
ApiParam({
|
||||||
|
name: 'id',
|
||||||
|
description: 'ID del cuestionario',
|
||||||
|
required: true,
|
||||||
|
type: 'number',
|
||||||
|
example: 1
|
||||||
|
}),
|
||||||
|
ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Formulario completo con todas sus secciones, preguntas y opciones. Las preguntas incluyen el atributo "validacion" si tienen reglas de validación específicas (como "correo", "cuenta_alumno", "telefono", "nombre", etc.)',
|
||||||
|
type: FormularioDto,
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: { $ref: getSchemaPath(FormularioDto) },
|
||||||
|
example: feriaSexualidad
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
ApiResponse({ status: 404, description: 'Cuestionario no encontrado' })
|
||||||
|
);
|
||||||
|
|
||||||
|
// Documentación para crear un cuestionario
|
||||||
|
static ApiCreate = applyDecorators(
|
||||||
|
ApiOperation({
|
||||||
|
summary: 'Crear un nuevo cuestionario',
|
||||||
|
description: 'Crea un nuevo cuestionario con secciones, preguntas y opciones, incluyendo validaciones personalizadas'
|
||||||
|
}),
|
||||||
|
ApiBody({
|
||||||
|
description: 'Datos del cuestionario a crear. Para las preguntas de tipo "Abierta", se puede especificar un tipo de validación usando la propiedad "validacion" con valores como: "correo", "cuenta_alumno", "telefono", "nombre", "entero", "decimal", etc.',
|
||||||
|
type: FormularioDto,
|
||||||
|
examples: {
|
||||||
|
feriaSexualidad: {
|
||||||
|
summary: 'Ejemplo de cuestionario para feria de sexualidad',
|
||||||
|
description: 'Cuestionario con secciones, preguntas y opciones para feria de sexualidad, ahora con validaciones específicas para cada tipo de campo (correo, nombre, cuenta_alumno, etc.)',
|
||||||
|
value: feriaSexualidad
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
ApiResponse({
|
||||||
|
status: 201,
|
||||||
|
description: 'Cuestionario creado correctamente. Los campos con la propiedad "validacion" se validarán según su tipo: "correo", "cuenta_alumno", "telefono", "nombre", "entero", "decimal", etc.',
|
||||||
|
schema: {
|
||||||
|
properties: {
|
||||||
|
success: { type: 'boolean', example: true },
|
||||||
|
cuestionario: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id_cuestionario: { type: 'number', example: 1 },
|
||||||
|
nombre_form: { type: 'string', example: 'Registro para la Feria de la Sexualidad - FES Acatlán' },
|
||||||
|
descripcion: { type: 'string' },
|
||||||
|
contador_secciones: { type: 'number', example: 1 },
|
||||||
|
editable: { type: 'boolean', example: true },
|
||||||
|
fecha_fin: { type: 'string', format: 'date-time' },
|
||||||
|
fecha_inicio: { type: 'string', format: 'date-time' },
|
||||||
|
id_tipo_cuestionario: { type: 'number', example: 1 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
ApiResponse({ status: 400, description: 'Datos del cuestionario inválidos' }),
|
||||||
|
ApiResponse({ status: 500, description: 'Error interno del servidor' })
|
||||||
|
);
|
||||||
|
|
||||||
|
// Documentación para obtener todos los cuestionarios
|
||||||
|
static ApiGetAll = applyDecorators(
|
||||||
|
ApiOperation({
|
||||||
|
summary: 'Obtener todos los cuestionarios',
|
||||||
|
description: 'Retorna una lista de todos los cuestionarios registrados'
|
||||||
|
}),
|
||||||
|
ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Lista de cuestionarios obtenida correctamente',
|
||||||
|
schema: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id_cuestionario: { type: 'number', example: 1 },
|
||||||
|
nombre_form: { type: 'string', example: 'Registro para la Feria de la Sexualidad - FES Acatlán' },
|
||||||
|
descripcion: { type: 'string' },
|
||||||
|
contador_secciones: { type: 'number', example: 1 },
|
||||||
|
editable: { type: 'boolean', example: true },
|
||||||
|
fecha_fin: { type: 'string', format: 'date-time' },
|
||||||
|
fecha_inicio: { type: 'string', format: 'date-time' },
|
||||||
|
id_tipo_cuestionario: { type: 'number', example: 1 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
ApiResponse({ status: 500, description: 'Error interno del servidor' })
|
||||||
|
);
|
||||||
|
|
||||||
|
// Documentación para obtener un cuestionario por ID
|
||||||
|
static ApiGetOne = applyDecorators(
|
||||||
|
ApiOperation({
|
||||||
|
summary: 'Obtener un cuestionario por ID',
|
||||||
|
description: 'Retorna un cuestionario específico por su ID'
|
||||||
|
}),
|
||||||
|
ApiParam({
|
||||||
|
name: 'id',
|
||||||
|
description: 'ID del cuestionario',
|
||||||
|
required: true,
|
||||||
|
type: 'number',
|
||||||
|
example: 1
|
||||||
|
}),
|
||||||
|
ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Cuestionario obtenido correctamente',
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id_cuestionario: { type: 'number', example: 1 },
|
||||||
|
nombre_form: { type: 'string', example: 'Registro para la Feria de la Sexualidad - FES Acatlán' },
|
||||||
|
descripcion: { type: 'string' },
|
||||||
|
contador_secciones: { type: 'number', example: 1 },
|
||||||
|
editable: { type: 'boolean', example: true },
|
||||||
|
fecha_fin: { type: 'string', format: 'date-time' },
|
||||||
|
fecha_inicio: { type: 'string', format: 'date-time' },
|
||||||
|
id_tipo_cuestionario: { type: 'number', example: 1 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
ApiResponse({ status: 404, description: 'Cuestionario no encontrado' }),
|
||||||
|
ApiResponse({ status: 500, description: 'Error interno del servidor' })
|
||||||
|
);
|
||||||
|
|
||||||
|
// Documentación para actualizar un cuestionario
|
||||||
|
static ApiUpdate = applyDecorators(
|
||||||
|
ApiOperation({
|
||||||
|
summary: 'Actualizar un cuestionario',
|
||||||
|
description: 'Actualiza los datos de un cuestionario existente'
|
||||||
|
}),
|
||||||
|
ApiParam({
|
||||||
|
name: 'id',
|
||||||
|
description: 'ID del cuestionario a actualizar',
|
||||||
|
required: true,
|
||||||
|
type: 'number',
|
||||||
|
example: 1
|
||||||
|
}),
|
||||||
|
ApiBody({
|
||||||
|
description: 'Datos a actualizar del cuestionario',
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
nombre_form: { type: 'string', example: 'Nombre actualizado del formulario' },
|
||||||
|
descripcion: { type: 'string', example: 'Descripción actualizada' },
|
||||||
|
fecha_inicio: { type: 'string', format: 'date-time' },
|
||||||
|
fecha_fin: { type: 'string', format: 'date-time' },
|
||||||
|
editable: { type: 'boolean', example: true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Cuestionario actualizado correctamente',
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
affected: { type: 'number', example: 1 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
ApiResponse({ status: 400, description: 'Datos de actualización inválidos' }),
|
||||||
|
ApiResponse({ status: 404, description: 'Cuestionario no encontrado' }),
|
||||||
|
ApiResponse({ status: 500, description: 'Error interno del servidor' })
|
||||||
|
);
|
||||||
|
|
||||||
|
// Documentación para eliminar un cuestionario
|
||||||
|
static ApiRemove = applyDecorators(
|
||||||
|
ApiOperation({
|
||||||
|
summary: 'Eliminar un cuestionario',
|
||||||
|
description: 'Elimina permanentemente un cuestionario por su ID'
|
||||||
|
}),
|
||||||
|
ApiParam({
|
||||||
|
name: 'id',
|
||||||
|
description: 'ID del cuestionario a eliminar',
|
||||||
|
required: true,
|
||||||
|
type: 'number',
|
||||||
|
example: 1
|
||||||
|
}),
|
||||||
|
ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Cuestionario eliminado correctamente',
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
affected: { type: 'number', example: 1 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
ApiResponse({ status: 404, description: 'Cuestionario no encontrado' }),
|
||||||
|
ApiResponse({ status: 500, description: 'Error interno del servidor' })
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,9 +1,36 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { CuestionarioService } from './cuestionario.service';
|
import { CuestionarioService } from './cuestionario.service';
|
||||||
import { CuestionarioController } from './cuestionario.controller';
|
import { CuestionarioController } from './cuestionario.controller';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { Cuestionario } from './entities/cuestionario.entity';
|
||||||
|
import { Seccion } from '../seccion/entities/seccion.entity';
|
||||||
|
import { CuestionarioSeccion } from '../cuestionario_seccion/entities/cuestionario_seccion.entity';
|
||||||
|
import { Pregunta } from '../pregunta/entities/pregunta.entity';
|
||||||
|
import { SeccionPregunta } from '../seccion_pregunta/entities/seccion_pregunta.entity';
|
||||||
|
import { Opcion } from '../opcion/entities/opcion.entity';
|
||||||
|
import { PreguntaOpcion } from '../pregunta_opcion/entities/pregunta_opcion.entity';
|
||||||
|
import { TipoPregunta } from '../tipo_pregunta/entities/tipo_pregunta.entity';
|
||||||
|
import { TipoCuestionario } from '../tipo_cuestionario/entities/tipo_cuestionario.entity';
|
||||||
|
import { Evento } from '../evento/evento.entity';
|
||||||
|
import { EventoService } from '../evento/evento.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([
|
||||||
|
Cuestionario,
|
||||||
|
Seccion,
|
||||||
|
CuestionarioSeccion,
|
||||||
|
Pregunta,
|
||||||
|
SeccionPregunta,
|
||||||
|
Opcion,
|
||||||
|
PreguntaOpcion,
|
||||||
|
TipoPregunta,
|
||||||
|
TipoCuestionario,
|
||||||
|
Evento
|
||||||
|
])
|
||||||
|
],
|
||||||
controllers: [CuestionarioController],
|
controllers: [CuestionarioController],
|
||||||
providers: [CuestionarioService],
|
providers: [CuestionarioService, EventoService],
|
||||||
|
exports: [TypeOrmModule, CuestionarioService]
|
||||||
})
|
})
|
||||||
export class CuestionarioModule {}
|
export class CuestionarioModule {}
|
||||||
|
|||||||
@@ -1,26 +1,396 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository, DataSource, In } from 'typeorm';
|
||||||
import { CreateCuestionarioDto } from './dto/create-cuestionario.dto';
|
import { CreateCuestionarioDto } from './dto/create-cuestionario.dto';
|
||||||
import { UpdateCuestionarioDto } from './dto/update-cuestionario.dto';
|
import { UpdateCuestionarioDto } from './dto/update-cuestionario.dto';
|
||||||
|
import { Cuestionario } from './entities/cuestionario.entity';
|
||||||
|
import { Seccion } from '../seccion/entities/seccion.entity';
|
||||||
|
import { CuestionarioSeccion } from '../cuestionario_seccion/entities/cuestionario_seccion.entity';
|
||||||
|
import { Pregunta } from '../pregunta/entities/pregunta.entity';
|
||||||
|
import { SeccionPregunta } from '../seccion_pregunta/entities/seccion_pregunta.entity';
|
||||||
|
import { Opcion } from '../opcion/entities/opcion.entity';
|
||||||
|
import { PreguntaOpcion } from '../pregunta_opcion/entities/pregunta_opcion.entity';
|
||||||
|
import { TipoPregunta } from '../tipo_pregunta/entities/tipo_pregunta.entity';
|
||||||
|
import { TipoCuestionario } from '../tipo_cuestionario/entities/tipo_cuestionario.entity';
|
||||||
|
import { Evento } from '../evento/evento.entity';
|
||||||
|
import { EventoService } from '../evento/evento.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CuestionarioService {
|
export class CuestionarioService {
|
||||||
create(createCuestionarioDto: CreateCuestionarioDto) {
|
constructor(
|
||||||
return 'This action adds a new cuestionario';
|
@InjectRepository(Cuestionario)
|
||||||
|
private cuestionarioRepository: Repository<Cuestionario>,
|
||||||
|
@InjectRepository(Seccion)
|
||||||
|
private seccionRepository: Repository<Seccion>,
|
||||||
|
@InjectRepository(CuestionarioSeccion)
|
||||||
|
private cuestionarioSeccionRepository: Repository<CuestionarioSeccion>,
|
||||||
|
@InjectRepository(Pregunta)
|
||||||
|
private preguntaRepository: Repository<Pregunta>,
|
||||||
|
@InjectRepository(SeccionPregunta)
|
||||||
|
private seccionPreguntaRepository: Repository<SeccionPregunta>,
|
||||||
|
@InjectRepository(Opcion)
|
||||||
|
private opcionRepository: Repository<Opcion>,
|
||||||
|
@InjectRepository(PreguntaOpcion)
|
||||||
|
private preguntaOpcionRepository: Repository<PreguntaOpcion>,
|
||||||
|
@InjectRepository(TipoPregunta)
|
||||||
|
private tipoPreguntaRepository: Repository<TipoPregunta>,
|
||||||
|
@InjectRepository(Evento)
|
||||||
|
private eventoRepository: Repository<Evento>,
|
||||||
|
private dataSource: DataSource,
|
||||||
|
private eventoService: EventoService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async create(createCuestionarioDto: CreateCuestionarioDto) {
|
||||||
|
const queryRunner = this.dataSource.createQueryRunner();
|
||||||
|
await queryRunner.connect();
|
||||||
|
await queryRunner.startTransaction();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Verificar si se especificó un evento y búscarlo/crearlo
|
||||||
|
let idEvento: number | undefined = undefined;
|
||||||
|
|
||||||
|
if (createCuestionarioDto.evento) {
|
||||||
|
// Buscar si el evento ya existe
|
||||||
|
let evento = await this.eventoRepository.findOne({
|
||||||
|
where: { nombre_evento: createCuestionarioDto.evento }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Si no existe, crearlo
|
||||||
|
if (!evento) {
|
||||||
|
const nuevoEvento = {
|
||||||
|
nombre_evento: createCuestionarioDto.evento,
|
||||||
|
tipo_evento: 'Predeterminado',
|
||||||
|
fecha_inicio: createCuestionarioDto.fecha_inicio || new Date(),
|
||||||
|
fecha_fin: createCuestionarioDto.fecha_fin || new Date(Date.now() + 86400000) // 1 día después si no hay fecha
|
||||||
|
};
|
||||||
|
|
||||||
|
evento = await queryRunner.manager.save(Evento, nuevoEvento);
|
||||||
|
console.log(`Evento creado: ${evento.nombre_evento}, ID: ${evento.id_evento}`);
|
||||||
|
} else {
|
||||||
|
console.log(`Evento encontrado: ${evento.nombre_evento}, ID: ${evento.id_evento}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
idEvento = evento.id_evento;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Crear cuestionario
|
||||||
|
const cuestionario = new Cuestionario();
|
||||||
|
cuestionario.nombre_form = createCuestionarioDto.nombre_form;
|
||||||
|
cuestionario.descripcion = createCuestionarioDto.descripcion;
|
||||||
|
cuestionario.fecha_inicio = createCuestionarioDto.fecha_inicio ? new Date(createCuestionarioDto.fecha_inicio) : undefined;
|
||||||
|
cuestionario.fecha_fin = createCuestionarioDto.fecha_fin ? new Date(createCuestionarioDto.fecha_fin) : undefined;
|
||||||
|
cuestionario.id_tipo_cuestionario = createCuestionarioDto.id_tipo_cuestionario;
|
||||||
|
cuestionario.contador_secciones = createCuestionarioDto.secciones?.length || 0;
|
||||||
|
cuestionario.editable = true;
|
||||||
|
|
||||||
|
// Asociar el cuestionario con el evento si existe
|
||||||
|
if (idEvento !== undefined) {
|
||||||
|
cuestionario.id_evento = idEvento;
|
||||||
|
}
|
||||||
|
|
||||||
|
const savedCuestionario = await queryRunner.manager.save(cuestionario);
|
||||||
|
|
||||||
|
// 2. Crear secciones y vincularlas al cuestionario
|
||||||
|
if (createCuestionarioDto.secciones && createCuestionarioDto.secciones.length > 0) {
|
||||||
|
for (let i = 0; i < createCuestionarioDto.secciones.length; i++) {
|
||||||
|
const seccionDto = createCuestionarioDto.secciones[i];
|
||||||
|
|
||||||
|
// Crear sección
|
||||||
|
const seccion = new Seccion();
|
||||||
|
seccion.titulo = seccionDto.titulo;
|
||||||
|
seccion.descripcion = seccionDto.descripcion;
|
||||||
|
seccion.contador_pregunta = seccionDto.preguntas?.length || 0;
|
||||||
|
|
||||||
|
const savedSeccion = await queryRunner.manager.save(seccion);
|
||||||
|
|
||||||
|
// Vincular sección con cuestionario
|
||||||
|
const cuestionarioSeccion = new CuestionarioSeccion();
|
||||||
|
cuestionarioSeccion.id_cuestionario = savedCuestionario.id_cuestionario;
|
||||||
|
cuestionarioSeccion.id_seccion = savedSeccion.id_seccion;
|
||||||
|
cuestionarioSeccion.posicion = i + 1;
|
||||||
|
|
||||||
|
await queryRunner.manager.save(cuestionarioSeccion);
|
||||||
|
|
||||||
|
// 3. Crear preguntas y vincularlas a la sección
|
||||||
|
if (seccionDto.preguntas && seccionDto.preguntas.length > 0) {
|
||||||
|
for (let j = 0; j < seccionDto.preguntas.length; j++) {
|
||||||
|
const preguntaDto = seccionDto.preguntas[j];
|
||||||
|
|
||||||
|
// Mapear directamente el tipo de pregunta a su ID correspondiente
|
||||||
|
let idTipoPregunta = 7; // Valor por defecto (desconocido)
|
||||||
|
|
||||||
|
// Mapeamos el tipo de pregunta a su ID correspondiente
|
||||||
|
if (preguntaDto.tipo === 'Cerrada') {
|
||||||
|
idTipoPregunta = 1;
|
||||||
|
} else if (preguntaDto.tipo === 'Abierta') {
|
||||||
|
idTipoPregunta = 2;
|
||||||
|
} else if (preguntaDto.tipo === 'Multiple') {
|
||||||
|
idTipoPregunta = 3;
|
||||||
|
} else {
|
||||||
|
console.warn(`Tipo de pregunta no reconocido: ${preguntaDto.tipo}. Se asignará como desconocido.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tipoPregunta = { id_tipo: idTipoPregunta, tipo_pregunta: preguntaDto.tipo };
|
||||||
|
|
||||||
|
// Crear pregunta
|
||||||
|
const pregunta = new Pregunta();
|
||||||
|
pregunta.pregunta = preguntaDto.titulo;
|
||||||
|
pregunta.obligatoria = preguntaDto.obligatoria || false;
|
||||||
|
pregunta.id_tipo_pregunta = tipoPregunta.id_tipo;
|
||||||
|
pregunta.id_opcion_dependiente = preguntaDto.id_opcion_dependiente || undefined;
|
||||||
|
pregunta.contador_opcion = preguntaDto.opciones?.length || 0;
|
||||||
|
pregunta.validacion = preguntaDto.validacion || undefined;
|
||||||
|
|
||||||
|
const savedPregunta = await queryRunner.manager.save(pregunta);
|
||||||
|
|
||||||
|
// Vincular pregunta con sección
|
||||||
|
const seccionPregunta = new SeccionPregunta();
|
||||||
|
seccionPregunta.id_seccion = savedSeccion.id_seccion;
|
||||||
|
seccionPregunta.id_pregunta = savedPregunta.id_pregunta;
|
||||||
|
seccionPregunta.posicion = j + 1;
|
||||||
|
|
||||||
|
await queryRunner.manager.save(seccionPregunta);
|
||||||
|
|
||||||
|
// 4. Crear opciones para preguntas de tipo multiple/radio
|
||||||
|
if (preguntaDto.opciones && preguntaDto.opciones.length > 0) {
|
||||||
|
for (let k = 0; k < preguntaDto.opciones.length; k++) {
|
||||||
|
const opcionDto = preguntaDto.opciones[k];
|
||||||
|
|
||||||
|
// Crear opción
|
||||||
|
const opcion = new Opcion();
|
||||||
|
opcion.opcion = opcionDto.valor;
|
||||||
|
|
||||||
|
const savedOpcion = await queryRunner.manager.save(opcion);
|
||||||
|
|
||||||
|
// Vincular opción con pregunta
|
||||||
|
const preguntaOpcion = new PreguntaOpcion();
|
||||||
|
preguntaOpcion.id_pregunta = savedPregunta.id_pregunta;
|
||||||
|
// Utilizamos la opción como objeto, no como ID
|
||||||
|
preguntaOpcion.opcion = savedOpcion;
|
||||||
|
preguntaOpcion.posicion = k + 1;
|
||||||
|
|
||||||
|
await queryRunner.manager.save(preguntaOpcion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await queryRunner.commitTransaction();
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
cuestionario: savedCuestionario,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
await queryRunner.rollbackTransaction();
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
await queryRunner.release();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
findAll() {
|
findAll() {
|
||||||
return `This action returns all cuestionario`;
|
return this.cuestionarioRepository.find();
|
||||||
}
|
}
|
||||||
|
|
||||||
findOne(id: number) {
|
async findOne(id: number) {
|
||||||
return `This action returns a #${id} cuestionario`;
|
const cuestionario = await this.cuestionarioRepository.findOne({
|
||||||
|
where: { id_cuestionario: id }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!cuestionario) {
|
||||||
|
throw new NotFoundException(`Cuestionario con ID ${id} no encontrado`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return cuestionario;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findCompleto(id: number) {
|
||||||
|
// Obtener el cuestionario básico
|
||||||
|
const cuestionario = await this.cuestionarioRepository.findOne({
|
||||||
|
where: { id_cuestionario: id },
|
||||||
|
relations: ['evento'] // Incluir la relación con el evento
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!cuestionario) {
|
||||||
|
throw new NotFoundException(`Cuestionario con ID ${id} no encontrado`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Para fines de demostración, usar valores hardcodeados
|
||||||
|
// para el tipo de cuestionario según get_formulario_feria.ts
|
||||||
|
const tipoCuestionario = {
|
||||||
|
id_tipo_cuestionario: 1,
|
||||||
|
tipo_cuestionario: 'Encuesta'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mapeador de ID de tipo de pregunta a nombres conocidos
|
||||||
|
const tiposPreguntaMap = {
|
||||||
|
1: { id_tipo: 1, tipo_pregunta: 'Cerrada' },
|
||||||
|
2: { id_tipo: 2, tipo_pregunta: 'Abierta' },
|
||||||
|
3: { id_tipo: 3, tipo_pregunta: 'Multiple' }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Obtener las relaciones cuestionario-seccion
|
||||||
|
const cuestionarioSecciones = await this.cuestionarioSeccionRepository.find({
|
||||||
|
where: { id_cuestionario: id },
|
||||||
|
order: { posicion: 'ASC' }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Obtener todas las secciones
|
||||||
|
const seccionIds = cuestionarioSecciones.map(cs => cs.id_seccion);
|
||||||
|
const secciones = await this.seccionRepository.find({
|
||||||
|
where: { id_seccion: In(seccionIds) }
|
||||||
|
});
|
||||||
|
|
||||||
|
const seccionesFormateadas = await Promise.all(
|
||||||
|
cuestionarioSecciones.map(async (cs) => {
|
||||||
|
const seccion = secciones.find(s => s.id_seccion === cs.id_seccion);
|
||||||
|
|
||||||
|
if (!seccion) return null;
|
||||||
|
|
||||||
|
// Obtener relaciones sección-pregunta
|
||||||
|
const seccionPreguntas = await this.seccionPreguntaRepository.find({
|
||||||
|
where: { id_seccion: seccion.id_seccion },
|
||||||
|
order: { posicion: 'ASC' }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Obtener preguntas
|
||||||
|
const preguntaIds = seccionPreguntas.map(sp => sp.id_pregunta);
|
||||||
|
const preguntas = await this.preguntaRepository.find({
|
||||||
|
where: { id_pregunta: In(preguntaIds) }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Formatear cada pregunta según el formato requerido
|
||||||
|
const preguntasFormateadas = await Promise.all(
|
||||||
|
seccionPreguntas.map(async (sp) => {
|
||||||
|
const pregunta = preguntas.find(p => p.id_pregunta === sp.id_pregunta);
|
||||||
|
|
||||||
|
if (!pregunta) return null;
|
||||||
|
|
||||||
|
// Usar el mapeo de tipos de pregunta definido arriba
|
||||||
|
const tipoPregunta = tiposPreguntaMap[pregunta.id_tipo_pregunta] ||
|
||||||
|
{ id_tipo: pregunta.id_tipo_pregunta, tipo_pregunta: 'Desconocido' };
|
||||||
|
|
||||||
|
// Obtener opciones para preguntas de tipo multiple/radio
|
||||||
|
const preguntaOpciones = await this.preguntaOpcionRepository.find({
|
||||||
|
where: { id_pregunta: pregunta.id_pregunta },
|
||||||
|
relations: ['opcion'],
|
||||||
|
order: { posicion: 'ASC' }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Formatear las opciones según el formato requerido
|
||||||
|
const opcionesFormateadas = preguntaOpciones.map(po => ({
|
||||||
|
id_pregunta_opcion: po.id_pregunta_opcion,
|
||||||
|
posicion: po.posicion,
|
||||||
|
id_opcion: po.opcion.id_opcion,
|
||||||
|
opcion: {
|
||||||
|
id_opcion: po.opcion.id_opcion,
|
||||||
|
opcion: po.opcion.opcion
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Construir objeto de pregunta según el formato requerido
|
||||||
|
return {
|
||||||
|
id_seccion_pregunta: sp.id_seccion_pregunta,
|
||||||
|
posicion: sp.posicion,
|
||||||
|
pregunta: {
|
||||||
|
id_pregunta: pregunta.id_pregunta,
|
||||||
|
pregunta: pregunta.pregunta,
|
||||||
|
contador_opcion: pregunta.contador_opcion,
|
||||||
|
obligatoria: pregunta.obligatoria,
|
||||||
|
id_tipo_pregunta: pregunta.id_tipo_pregunta,
|
||||||
|
id_opcion_dependiente: pregunta.id_opcion_dependiente,
|
||||||
|
validacion: pregunta.validacion,
|
||||||
|
tipo_pregunta: {
|
||||||
|
id_tipo: tipoPregunta.id_tipo,
|
||||||
|
tipo_pregunta: tipoPregunta.tipo_pregunta
|
||||||
|
},
|
||||||
|
opciones: opcionesFormateadas
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})
|
||||||
|
).then(results => results.filter(p => p !== null));
|
||||||
|
|
||||||
|
// Construir objeto de sección según el formato requerido
|
||||||
|
return {
|
||||||
|
id_cuestionario_seccion: cs.id_cuestionario_seccion,
|
||||||
|
posicion: cs.posicion,
|
||||||
|
seccion: {
|
||||||
|
id_seccion: seccion.id_seccion,
|
||||||
|
contador_pregunta: seccion.contador_pregunta,
|
||||||
|
descripcion: seccion.descripcion,
|
||||||
|
titulo: seccion.titulo
|
||||||
|
},
|
||||||
|
preguntas: preguntasFormateadas
|
||||||
|
};
|
||||||
|
})
|
||||||
|
).then(results => results.filter(s => s !== null));
|
||||||
|
|
||||||
|
// Construir objeto final respetando el formato de get_formulario_feria.ts
|
||||||
|
return {
|
||||||
|
tipo_cuestionario: {
|
||||||
|
id_tipo_cuestionario: tipoCuestionario.id_tipo_cuestionario,
|
||||||
|
tipo_cuestionario: tipoCuestionario.tipo_cuestionario
|
||||||
|
},
|
||||||
|
evento: cuestionario.evento ? {
|
||||||
|
id_evento: cuestionario.evento.id_evento,
|
||||||
|
nombre_evento: cuestionario.evento.nombre_evento,
|
||||||
|
tipo_evento: cuestionario.evento.tipo_evento,
|
||||||
|
fecha_inicio: cuestionario.evento.fecha_inicio ? cuestionario.evento.fecha_inicio.toISOString() : null,
|
||||||
|
fecha_fin: cuestionario.evento.fecha_fin ? cuestionario.evento.fecha_fin.toISOString() : null
|
||||||
|
} : null,
|
||||||
|
cuestionario: {
|
||||||
|
id_cuestionario: cuestionario.id_cuestionario,
|
||||||
|
nombre_form: cuestionario.nombre_form,
|
||||||
|
contador_secciones: cuestionario.contador_secciones,
|
||||||
|
descripcion: cuestionario.descripcion,
|
||||||
|
editable: cuestionario.editable,
|
||||||
|
fecha_inicio: cuestionario.fecha_inicio ? cuestionario.fecha_inicio.toISOString() : null,
|
||||||
|
fecha_fin: cuestionario.fecha_fin ? cuestionario.fecha_fin.toISOString() : null,
|
||||||
|
id_cuestionario_original: cuestionario.id_cuestionario_original,
|
||||||
|
id_tipo_cuestionario: cuestionario.id_tipo_cuestionario,
|
||||||
|
id_evento: cuestionario.id_evento || null,
|
||||||
|
secciones: seccionesFormateadas
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
update(id: number, updateCuestionarioDto: UpdateCuestionarioDto) {
|
async update(id: number, updateCuestionarioDto: UpdateCuestionarioDto) {
|
||||||
return `This action updates a #${id} cuestionario`;
|
// 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) {
|
remove(id: number) {
|
||||||
return `This action removes a #${id} cuestionario`;
|
return this.cuestionarioRepository.delete(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1,69 @@
|
|||||||
export class CreateCuestionarioDto {}
|
import { CreateSeccionDto } from '../../seccion/dto/create-seccion.dto';
|
||||||
|
import { IsArray, IsDateString, IsNotEmpty, IsNumber, IsOptional, IsString, ValidateNested } from 'class-validator';
|
||||||
|
import { Type } from 'class-transformer';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class CreateCuestionarioDto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Nombre del formulario',
|
||||||
|
example: 'Registro para la Feria de la Sexualidad - FES Acatlán'
|
||||||
|
})
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
nombre_form: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Descripción del formulario',
|
||||||
|
example: 'Formulario de registro para la feria...',
|
||||||
|
required: false
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
descripcion?: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Fecha de inicio',
|
||||||
|
example: '2025-03-26T00:00:00',
|
||||||
|
required: false
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsDateString()
|
||||||
|
fecha_inicio?: Date;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Fecha de fin',
|
||||||
|
example: '2025-03-31T23:59:59',
|
||||||
|
required: false
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsDateString()
|
||||||
|
fecha_fin?: Date;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'ID del tipo de cuestionario',
|
||||||
|
example: 1
|
||||||
|
})
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsNumber()
|
||||||
|
id_tipo_cuestionario: number;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Nombre del evento asociado',
|
||||||
|
example: 'Feria de la Sexualidad',
|
||||||
|
required: false
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
evento?: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Secciones del cuestionario',
|
||||||
|
type: [CreateSeccionDto],
|
||||||
|
required: false
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => CreateSeccionDto)
|
||||||
|
secciones?: CreateSeccionDto[];
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,220 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { TipoPreguntaEnum } from '../../pregunta/dto/create-pregunta.dto';
|
||||||
|
|
||||||
|
// Ejemplo de respuesta de cuestionario completo
|
||||||
|
export const FormularioFeriaSchema = {
|
||||||
|
tipo_cuestionario: {
|
||||||
|
id_tipo_cuestionario: 1,
|
||||||
|
tipo_cuestionario: 'Encuesta'
|
||||||
|
},
|
||||||
|
cuestionario: {
|
||||||
|
id_cuestionario: 10,
|
||||||
|
nombre_form: 'Registro para la Feria de la Sexualidad - Comunidad FES Acatlán',
|
||||||
|
contador_secciones: 2,
|
||||||
|
descripcion: 'La Feria de la Sexualidad es un espacio seguro e informativo dirigido a toda la comunidad universitaria. Este registro nos ayudará a organizar mejor las actividades, talleres y charlas, así como conocer tus intereses. Por favor, completa el siguiente formulario para confirmar tu participación.',
|
||||||
|
editable: true,
|
||||||
|
fecha_inicio: '2025-03-26T00:00:00',
|
||||||
|
fecha_fin: '2025-03-31T23:59:59',
|
||||||
|
id_cuestionario_original: null,
|
||||||
|
id_tipo_cuestionario: 1,
|
||||||
|
secciones: [
|
||||||
|
{
|
||||||
|
id_cuestionario_seccion: 1,
|
||||||
|
posicion: 1,
|
||||||
|
seccion: {
|
||||||
|
id_seccion: 100,
|
||||||
|
contador_pregunta: 3,
|
||||||
|
descripcion: 'Información básica',
|
||||||
|
titulo: 'Datos personales'
|
||||||
|
},
|
||||||
|
preguntas: [
|
||||||
|
{
|
||||||
|
id_seccion_pregunta: 1000,
|
||||||
|
posicion: 1,
|
||||||
|
pregunta: {
|
||||||
|
id_pregunta: 101,
|
||||||
|
pregunta: '¿Eres parte de la comunidad de la FES Acatlán?',
|
||||||
|
contador_opcion: 2,
|
||||||
|
obligatoria: true,
|
||||||
|
id_tipo_pregunta: 1,
|
||||||
|
id_opcion_dependiente: null,
|
||||||
|
tipo_pregunta: {
|
||||||
|
id_tipo: 1,
|
||||||
|
tipo_pregunta: 'Cerrada'
|
||||||
|
},
|
||||||
|
opciones: [
|
||||||
|
{
|
||||||
|
id_pregunta_opcion: 50001,
|
||||||
|
posicion: 1,
|
||||||
|
id_opcion: 90001,
|
||||||
|
opcion: {
|
||||||
|
id_opcion: 90001,
|
||||||
|
opcion: 'Sí'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id_pregunta_opcion: 50002,
|
||||||
|
posicion: 2,
|
||||||
|
id_opcion: 90002,
|
||||||
|
opcion: {
|
||||||
|
id_opcion: 90002,
|
||||||
|
opcion: 'No'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id_seccion_pregunta: 1005,
|
||||||
|
posicion: 2,
|
||||||
|
pregunta: {
|
||||||
|
id_pregunta: 109,
|
||||||
|
pregunta: 'Correo electrónico',
|
||||||
|
contador_opcion: 0,
|
||||||
|
obligatoria: true,
|
||||||
|
id_tipo_pregunta: 2,
|
||||||
|
id_opcion_dependiente: null,
|
||||||
|
limite: 250, // Límite de caracteres para respuestas de texto
|
||||||
|
validacion: 'correo', // Validación de formato de correo electrónico
|
||||||
|
tipo_pregunta: {
|
||||||
|
id_tipo: 2,
|
||||||
|
tipo_pregunta: 'Abierta'
|
||||||
|
},
|
||||||
|
opciones: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id_seccion_pregunta: 1001,
|
||||||
|
posicion: 3,
|
||||||
|
pregunta: {
|
||||||
|
id_pregunta: 102,
|
||||||
|
pregunta: 'Número de cuenta (solo para estudiantes de la FES Acatlán)',
|
||||||
|
contador_opcion: 0,
|
||||||
|
obligatoria: false,
|
||||||
|
id_tipo_pregunta: 2,
|
||||||
|
id_opcion_dependiente: 90001, // Dependiente de la respuesta "Sí" en la pregunta anterior
|
||||||
|
limite: 250, // Límite de caracteres para respuestas de texto
|
||||||
|
validacion: 'cuenta_alumno', // Validación de formato de número de cuenta
|
||||||
|
tipo_pregunta: {
|
||||||
|
id_tipo: 2,
|
||||||
|
tipo_pregunta: 'Abierta'
|
||||||
|
},
|
||||||
|
opciones: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id_cuestionario_seccion: 2,
|
||||||
|
posicion: 2,
|
||||||
|
seccion: {
|
||||||
|
id_seccion: 101,
|
||||||
|
contador_pregunta: 2,
|
||||||
|
descripcion: 'Información sobre tus intereses',
|
||||||
|
titulo: 'Intereses'
|
||||||
|
},
|
||||||
|
preguntas: [
|
||||||
|
{
|
||||||
|
id_seccion_pregunta: 1003,
|
||||||
|
posicion: 1,
|
||||||
|
pregunta: {
|
||||||
|
id_pregunta: 106,
|
||||||
|
pregunta: '¿Qué temas te interesan para la Feria de la Sexualidad?',
|
||||||
|
contador_opcion: 5,
|
||||||
|
obligatoria: true,
|
||||||
|
id_tipo_pregunta: 3,
|
||||||
|
id_opcion_dependiente: null,
|
||||||
|
tipo_pregunta: {
|
||||||
|
id_tipo: 3,
|
||||||
|
tipo_pregunta: 'Multiple'
|
||||||
|
},
|
||||||
|
opciones: [
|
||||||
|
{
|
||||||
|
id_pregunta_opcion: 50010,
|
||||||
|
posicion: 1,
|
||||||
|
id_opcion: 90010,
|
||||||
|
opcion: {
|
||||||
|
id_opcion: 90010,
|
||||||
|
opcion: 'Educación sexual'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id_pregunta_opcion: 50011,
|
||||||
|
posicion: 2,
|
||||||
|
id_opcion: 90011,
|
||||||
|
opcion: {
|
||||||
|
id_opcion: 90011,
|
||||||
|
opcion: 'Prevención de ITS'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id_pregunta_opcion: 50012,
|
||||||
|
posicion: 3,
|
||||||
|
id_opcion: 90012,
|
||||||
|
opcion: {
|
||||||
|
id_opcion: 90012,
|
||||||
|
opcion: 'Salud reproductiva'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id_pregunta_opcion: 50013,
|
||||||
|
posicion: 4,
|
||||||
|
id_opcion: 90013,
|
||||||
|
opcion: {
|
||||||
|
id_opcion: 90013,
|
||||||
|
opcion: 'Diversidad sexual y de género'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id_pregunta_opcion: 50014,
|
||||||
|
posicion: 5,
|
||||||
|
id_opcion: 90014,
|
||||||
|
opcion: {
|
||||||
|
id_opcion: 90014,
|
||||||
|
opcion: 'Relaciones saludables'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id_seccion_pregunta: 1004,
|
||||||
|
posicion: 2,
|
||||||
|
pregunta: {
|
||||||
|
id_pregunta: 107,
|
||||||
|
pregunta: '¿Algún otro tema que te gustaría que se abordara?',
|
||||||
|
contador_opcion: 0,
|
||||||
|
obligatoria: false,
|
||||||
|
id_tipo_pregunta: 2,
|
||||||
|
id_opcion_dependiente: null,
|
||||||
|
limite: 250, // Límite de caracteres para respuestas de texto
|
||||||
|
tipo_pregunta: {
|
||||||
|
id_tipo: 2,
|
||||||
|
tipo_pregunta: 'Abierta'
|
||||||
|
},
|
||||||
|
opciones: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ejemplo de tipo FormularioDto para usar en la API
|
||||||
|
export class FormularioDto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Tipo de cuestionario',
|
||||||
|
example: {
|
||||||
|
id_tipo_cuestionario: 1,
|
||||||
|
tipo_cuestionario: 'Encuesta'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
tipo_cuestionario: any;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Información completa del cuestionario',
|
||||||
|
example: FormularioFeriaSchema.cuestionario
|
||||||
|
})
|
||||||
|
cuestionario: any;
|
||||||
|
}
|
||||||
@@ -1,4 +1,15 @@
|
|||||||
import { PartialType } from '@nestjs/mapped-types';
|
import { PartialType } from '@nestjs/mapped-types';
|
||||||
import { CreateCuestionarioDto } from './create-cuestionario.dto';
|
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,6 +1,7 @@
|
|||||||
import { CuestionarioSeccion } from 'src/cuestionario_seccion/entities/cuestionario_seccion.entity';
|
import { CuestionarioSeccion } from 'src/cuestionario_seccion/entities/cuestionario_seccion.entity';
|
||||||
|
import { Evento } from 'src/evento/evento.entity';
|
||||||
import { TipoCuestionario } from 'src/tipo_cuestionario/entities/tipo_cuestionario.entity';
|
import { TipoCuestionario } from 'src/tipo_cuestionario/entities/tipo_cuestionario.entity';
|
||||||
import { Entity, PrimaryGeneratedColumn, Column, OneToMany, ManyToMany, ManyToOne } from 'typeorm';
|
import { Entity, PrimaryGeneratedColumn, Column, OneToMany, ManyToMany, ManyToOne, JoinColumn } from 'typeorm';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -16,29 +17,34 @@ export class Cuestionario {
|
|||||||
contador_secciones: number;
|
contador_secciones: number;
|
||||||
|
|
||||||
@Column({ type: 'text', nullable: true })
|
@Column({ type: 'text', nullable: true })
|
||||||
descripcion: string;
|
descripcion?: string;
|
||||||
|
|
||||||
@Column({ type: 'boolean', default: true })
|
@Column({ type: 'boolean', default: true })
|
||||||
editable: boolean;
|
editable: boolean;
|
||||||
|
|
||||||
@Column({ type: 'datetime', nullable: true })
|
@Column({ type: 'datetime', nullable: true })
|
||||||
fecha_fin: Date;
|
fecha_fin?: Date;
|
||||||
|
|
||||||
@Column({ type: 'datetime', nullable: true })
|
@Column({ type: 'datetime', nullable: true })
|
||||||
fecha_inicio: Date;
|
fecha_inicio?: Date;
|
||||||
|
|
||||||
|
|
||||||
@OneToMany(() => CuestionarioSeccion,(cuestionario_seccion) => cuestionario_seccion.id_cuestionario)
|
@OneToMany(() => CuestionarioSeccion,(cuestionario_seccion) => cuestionario_seccion.id_cuestionario)
|
||||||
cuestionarioSeccion:CuestionarioSeccion[]
|
cuestionarioSeccion:CuestionarioSeccion[]
|
||||||
|
|
||||||
@OneToMany(()=> Cuestionario, (cuestionario) => cuestionario.id_cuestionario)
|
@Column({ type: 'int', nullable: true })
|
||||||
@Column({ type: 'int' })
|
|
||||||
id_cuestionario_original: number;
|
id_cuestionario_original: number;
|
||||||
|
|
||||||
@OneToMany(()=> TipoCuestionario, (tipo_cuestionario) => tipo_cuestionario.id_tipo_cuestionario )
|
|
||||||
@Column({ type: 'int' })
|
@Column({ type: 'int' })
|
||||||
id_tipo_cuestionario: number;
|
id_tipo_cuestionario: number;
|
||||||
|
|
||||||
@ManyToOne(()=> Cuestionario, (cuestionario) => cuestionario.id_cuestionario)
|
@ManyToOne(()=> Cuestionario)
|
||||||
Cuestionario: Cuestionario[];
|
cuestionarioOriginal: Cuestionario;
|
||||||
|
|
||||||
|
@Column({ type: 'int', nullable: true })
|
||||||
|
id_evento: number;
|
||||||
|
|
||||||
|
@ManyToOne(() => Evento)
|
||||||
|
@JoinColumn({ name: 'id_evento' })
|
||||||
|
evento: Evento;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { CuestionarioRespondidoService } from './cuestionario_respondido.service';
|
||||||
import { CreateCuestionarioRespondidoDto } from './dto/create-cuestionario_respondido.dto';
|
import { CreateCuestionarioRespondidoDto } from './dto/create-cuestionario_respondido.dto';
|
||||||
import { UpdateCuestionarioRespondidoDto } from './dto/update-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')
|
@Controller('cuestionario-respondido')
|
||||||
export class CuestionarioRespondidoController {
|
export class CuestionarioRespondidoController {
|
||||||
constructor(private readonly cuestionarioRespondidoService: CuestionarioRespondidoService) {}
|
constructor(private readonly cuestionarioRespondidoService: CuestionarioRespondidoService) {}
|
||||||
@@ -12,11 +17,36 @@ export class CuestionarioRespondidoController {
|
|||||||
return this.cuestionarioRespondidoService.create(createCuestionarioRespondidoDto);
|
return this.cuestionarioRespondidoService.create(createCuestionarioRespondidoDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post('submit')
|
||||||
|
@CuestionarioRespondidoApiDocumentation.ApiSubmitRespuestas
|
||||||
|
@ApiBody({
|
||||||
|
type: SubmitRespuestasDto,
|
||||||
|
examples: {
|
||||||
|
comunidad: CuestionarioRespondidoApiDocumentation.ApiSubmitRespuestasExamples.communityExample,
|
||||||
|
externos: CuestionarioRespondidoApiDocumentation.ApiSubmitRespuestasExamples.externalExample
|
||||||
|
}
|
||||||
|
})
|
||||||
|
@CuestionarioRespondidoApiDocumentation.ApiSubmitResponse201
|
||||||
|
@CuestionarioRespondidoApiDocumentation.ApiSubmitResponse400
|
||||||
|
@CuestionarioRespondidoApiDocumentation.ApiSubmitResponse404
|
||||||
|
submitRespuestas(@Body() submitRespuestasDto: SubmitRespuestasDto) {
|
||||||
|
return this.cuestionarioRespondidoService.submitRespuestas(submitRespuestasDto);
|
||||||
|
}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
findAll() {
|
findAll() {
|
||||||
return this.cuestionarioRespondidoService.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')
|
@Get(':id')
|
||||||
findOne(@Param('id') id: string) {
|
findOne(@Param('id') id: string) {
|
||||||
return this.cuestionarioRespondidoService.findOne(+id);
|
return this.cuestionarioRespondidoService.findOne(+id);
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
import { ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||||
|
import { applyDecorators } from '@nestjs/common';
|
||||||
|
|
||||||
|
export class CuestionarioRespondidoApiDocumentation {
|
||||||
|
static ApiController = ApiTags('Cuestionario Respondido');
|
||||||
|
|
||||||
|
static ApiSubmitRespuestas = ApiOperation({
|
||||||
|
summary: 'Enviar respuestas a un cuestionario',
|
||||||
|
description: `Registra las respuestas de un participante a un cuestionario. Crea un registro de participante si no existe uno con ese correo.
|
||||||
|
|
||||||
|
## Tipos de respuestas:
|
||||||
|
|
||||||
|
- **Preguntas abiertas**: Enviar el texto de la respuesta como string (máximo 250 caracteres)
|
||||||
|
- **Preguntas cerradas**: Enviar el ID de la opción como número
|
||||||
|
- **Preguntas de selección múltiple**: Enviar un array de IDs de opciones como [número, número, ...]
|
||||||
|
|
||||||
|
### Importante
|
||||||
|
|
||||||
|
Para preguntas cerradas y múltiples, se deben enviar los IDs de las opciones, NO los textos.
|
||||||
|
Esto evita conflictos cuando existen opciones con el mismo texto pero diferentes IDs.
|
||||||
|
|
||||||
|
Todas las respuestas de texto y el correo electrónico están limitados a 250 caracteres.`
|
||||||
|
});
|
||||||
|
|
||||||
|
static ApiSubmitResponse201 = ApiResponse({
|
||||||
|
status: 201,
|
||||||
|
description: 'Respuestas guardadas correctamente',
|
||||||
|
schema: {
|
||||||
|
properties: {
|
||||||
|
success: { type: 'boolean', example: true },
|
||||||
|
message: { type: 'string', example: 'Respuestas guardadas correctamente' },
|
||||||
|
cuestionarioRespondidoId: { type: 'number', example: 1 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
static ApiSubmitResponse400 = ApiResponse({
|
||||||
|
status: 400,
|
||||||
|
description: 'Datos inválidos en la solicitud',
|
||||||
|
schema: {
|
||||||
|
properties: {
|
||||||
|
statusCode: { type: 'number', example: 400 },
|
||||||
|
message: { type: 'string', example: 'La opción con ID 999 no está asociada a la pregunta 101' },
|
||||||
|
error: { type: 'string', example: 'Bad Request' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
static ApiSubmitResponse404 = ApiResponse({
|
||||||
|
status: 404,
|
||||||
|
description: 'Cuestionario o pregunta no encontrada',
|
||||||
|
schema: {
|
||||||
|
properties: {
|
||||||
|
statusCode: { type: 'number', example: 404 },
|
||||||
|
message: { type: 'string', example: 'Cuestionario con ID 999 no encontrado' },
|
||||||
|
error: { type: 'string', example: 'Not Found' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
static get ApiReporteRespuestas() {
|
||||||
|
return applyDecorators(
|
||||||
|
ApiOperation({
|
||||||
|
summary: 'Descargar reporte de respuestas de un cuestionario',
|
||||||
|
description: `
|
||||||
|
Este endpoint permite descargar un archivo CSV con todas las respuestas de un cuestionario.
|
||||||
|
El reporte incluye información del participante, evento asociado, y el contenido de las respuestas.
|
||||||
|
Se generará un archivo CSV con los siguientes campos:
|
||||||
|
- ID Respuesta
|
||||||
|
- Fecha Respuesta
|
||||||
|
- ID Participante
|
||||||
|
- Correo Participante
|
||||||
|
- Evento
|
||||||
|
- Cuestionario
|
||||||
|
- Pregunta
|
||||||
|
- Respuesta
|
||||||
|
`
|
||||||
|
}),
|
||||||
|
ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Reporte de respuestas descargado exitosamente como archivo CSV'
|
||||||
|
}),
|
||||||
|
ApiResponse({
|
||||||
|
status: 404,
|
||||||
|
description: 'Cuestionario no encontrado'
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static ApiSubmitRespuestasExamples = {
|
||||||
|
communityExample: {
|
||||||
|
summary: 'Ejemplo de respuesta para un miembro de la comunidad',
|
||||||
|
value: {
|
||||||
|
id_formulario: 1,
|
||||||
|
correo: 'estudiante@acatlan.unam.mx',
|
||||||
|
respuestas: [
|
||||||
|
{ id_pregunta: 101, valor: 3 }, // Opción "Si" (usando el ID de la opción)
|
||||||
|
{ id_pregunta: 102, valor: 'estudiante@acatlan.unam.mx' }, // Correo electrónico
|
||||||
|
{ id_pregunta: 103, valor: '123456789' }, // Numero de cuenta
|
||||||
|
{ id_pregunta: 106, valor: 7 } // Opción "Prefiero no decirlo" (usando el ID de la opción)
|
||||||
|
],
|
||||||
|
fecha_envio: '2025-04-02T13:45:00'
|
||||||
|
},
|
||||||
|
description: 'Datos de un estudiante de la FES Acatlán con respuestas abiertas y opciones por ID.'
|
||||||
|
},
|
||||||
|
externalExample: {
|
||||||
|
summary: 'Ejemplo de respuesta para un participante externo con selección múltiple',
|
||||||
|
value: {
|
||||||
|
id_formulario: 1,
|
||||||
|
correo: 'externo@ejemplo.com',
|
||||||
|
respuestas: [
|
||||||
|
{ id_pregunta: 101, valor: 4 }, // Opción "No" (usando el ID de la opción)
|
||||||
|
{ id_pregunta: 102, valor: 'externo@ejemplo.com' }, // Correo electrónico
|
||||||
|
{ id_pregunta: 103, valor: 'Juan Carlos' }, // Nombre(s)
|
||||||
|
{ id_pregunta: 104, valor: 'Ramírez López' }, // Apellidos
|
||||||
|
{ id_pregunta: 105, valor: [2, 5, 8] }, // Selección múltiple (array de IDs de opciones)
|
||||||
|
{ id_pregunta: 106, valor: 7 }, // Opción "Prefiero no decirlo" (usando el ID de la opción)
|
||||||
|
{ id_pregunta: 107, valor: 'UAM Azcapotzalco' } // Institución de procedencia
|
||||||
|
],
|
||||||
|
fecha_envio: '2025-04-02T13:45:00'
|
||||||
|
},
|
||||||
|
description: 'Datos de un participante externo con respuestas abiertas, opciones por ID y selección múltiple.'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,9 +1,37 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { CuestionarioRespondidoService } from './cuestionario_respondido.service';
|
import { CuestionarioRespondidoService } from './cuestionario_respondido.service';
|
||||||
import { CuestionarioRespondidoController } from './cuestionario_respondido.controller';
|
import { CuestionarioRespondidoController } from './cuestionario_respondido.controller';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { CuestionarioRespondido } from './entities/cuestionario_respondido.entity';
|
||||||
|
import { CuestionarioModule } from '../cuestionario/cuestionario.module';
|
||||||
|
import { ParticipanteModule } from '../participante/participante.module';
|
||||||
|
import { Participante } from '../participante/participante.entity';
|
||||||
|
import { Cuestionario } from '../cuestionario/entities/cuestionario.entity';
|
||||||
|
import { Pregunta } from '../pregunta/entities/pregunta.entity';
|
||||||
|
import { RespuestaParticipanteAbierta } from '../respuesta_participante_abierta/entities/respuesta_participante_abierta.entity';
|
||||||
|
import { RespuestaParticipanteCerrada } from '../respuesta_participante_cerrada/entities/respuesta_participante_cerrada.entity';
|
||||||
|
import { PreguntaOpcion } from '../pregunta_opcion/entities/pregunta_opcion.entity';
|
||||||
|
import { Opcion } from '../opcion/entities/opcion.entity';
|
||||||
|
import { ValidacionesModule } from '../validaciones/validaciones.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([
|
||||||
|
CuestionarioRespondido,
|
||||||
|
Participante,
|
||||||
|
Cuestionario,
|
||||||
|
Pregunta,
|
||||||
|
RespuestaParticipanteAbierta,
|
||||||
|
RespuestaParticipanteCerrada,
|
||||||
|
PreguntaOpcion,
|
||||||
|
Opcion
|
||||||
|
]),
|
||||||
|
CuestionarioModule,
|
||||||
|
ParticipanteModule,
|
||||||
|
ValidacionesModule
|
||||||
|
],
|
||||||
controllers: [CuestionarioRespondidoController],
|
controllers: [CuestionarioRespondidoController],
|
||||||
providers: [CuestionarioRespondidoService],
|
providers: [CuestionarioRespondidoService],
|
||||||
|
exports: [TypeOrmModule]
|
||||||
})
|
})
|
||||||
export class CuestionarioRespondidoModule {}
|
export class CuestionarioRespondidoModule {}
|
||||||
|
|||||||
@@ -1,19 +1,382 @@
|
|||||||
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 { CreateCuestionarioRespondidoDto } from './dto/create-cuestionario_respondido.dto';
|
||||||
import { UpdateCuestionarioRespondidoDto } from './dto/update-cuestionario_respondido.dto';
|
import { UpdateCuestionarioRespondidoDto } from './dto/update-cuestionario_respondido.dto';
|
||||||
|
import { SubmitRespuestasDto } from './dto/submit-respuestas.dto';
|
||||||
|
import { CuestionarioRespondido } from './entities/cuestionario_respondido.entity';
|
||||||
|
import { Participante } from '../participante/participante.entity';
|
||||||
|
import { Cuestionario } from '../cuestionario/entities/cuestionario.entity';
|
||||||
|
import { Pregunta } from '../pregunta/entities/pregunta.entity';
|
||||||
|
import { RespuestaParticipanteAbierta } from '../respuesta_participante_abierta/entities/respuesta_participante_abierta.entity';
|
||||||
|
import { RespuestaParticipanteCerrada } from '../respuesta_participante_cerrada/entities/respuesta_participante_cerrada.entity';
|
||||||
|
import { PreguntaOpcion } from '../pregunta_opcion/entities/pregunta_opcion.entity';
|
||||||
|
import { Opcion } from '../opcion/entities/opcion.entity';
|
||||||
|
import { ValidadorRespuestasService } from '../validaciones/validador-respuestas.service';
|
||||||
|
import axios from 'axios';
|
||||||
|
import * as QRCode from 'qrcode';
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CuestionarioRespondidoService {
|
export class CuestionarioRespondidoService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(CuestionarioRespondido)
|
||||||
|
private cuestionarioRespondidoRepository: Repository<CuestionarioRespondido>,
|
||||||
|
@InjectRepository(Participante)
|
||||||
|
private participanteRepository: Repository<Participante>,
|
||||||
|
@InjectRepository(Cuestionario)
|
||||||
|
private cuestionarioRepository: Repository<Cuestionario>,
|
||||||
|
@InjectRepository(Pregunta)
|
||||||
|
private preguntaRepository: Repository<Pregunta>,
|
||||||
|
@InjectRepository(RespuestaParticipanteAbierta)
|
||||||
|
private respuestaAbiertaRepository: Repository<RespuestaParticipanteAbierta>,
|
||||||
|
@InjectRepository(RespuestaParticipanteCerrada)
|
||||||
|
private respuestaCerradaRepository: Repository<RespuestaParticipanteCerrada>,
|
||||||
|
@InjectRepository(PreguntaOpcion)
|
||||||
|
private preguntaOpcionRepository: Repository<PreguntaOpcion>,
|
||||||
|
@InjectRepository(Opcion)
|
||||||
|
private opcionRepository: Repository<Opcion>,
|
||||||
|
private dataSource: DataSource,
|
||||||
|
private validadorRespuestasService: ValidadorRespuestasService,
|
||||||
|
) {}
|
||||||
|
|
||||||
create(createCuestionarioRespondidoDto: CreateCuestionarioRespondidoDto) {
|
create(createCuestionarioRespondidoDto: CreateCuestionarioRespondidoDto) {
|
||||||
return 'This action adds a new cuestionarioRespondido';
|
return 'This action adds a new cuestionarioRespondido';
|
||||||
}
|
}
|
||||||
|
|
||||||
findAll() {
|
async submitRespuestas(submitDto: SubmitRespuestasDto) {
|
||||||
return `This action returns all cuestionarioRespondido`;
|
const queryRunner = this.dataSource.createQueryRunner();
|
||||||
|
await queryRunner.connect();
|
||||||
|
await queryRunner.startTransaction();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Verificar que el cuestionario existe
|
||||||
|
const cuestionario = await this.cuestionarioRepository.findOne({
|
||||||
|
where: { id_cuestionario: submitDto.id_formulario },
|
||||||
|
relations: ['evento']
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!cuestionario) {
|
||||||
|
throw new NotFoundException(`Cuestionario con ID ${submitDto.id_formulario} no encontrado`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Buscar o crear participante
|
||||||
|
let participante = await this.participanteRepository.findOne({
|
||||||
|
where: { correo: submitDto.correo }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!participante) {
|
||||||
|
participante = new Participante();
|
||||||
|
participante.correo = submitDto.correo;
|
||||||
|
participante.id_tipo_user = 1; // Usar un tipo de usuario por defecto
|
||||||
|
participante = await queryRunner.manager.save(participante);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Crear un nuevo registro de cuestionario respondido
|
||||||
|
const cuestionarioRespondido = new CuestionarioRespondido();
|
||||||
|
cuestionarioRespondido.cuestionario = cuestionario;
|
||||||
|
cuestionarioRespondido.participante = participante;
|
||||||
|
cuestionarioRespondido.fechaRespuesta = submitDto.fecha_envio ?
|
||||||
|
new Date(submitDto.fecha_envio) : new Date();
|
||||||
|
|
||||||
|
const savedCuestionarioRespondido = await queryRunner.manager.save(cuestionarioRespondido);
|
||||||
|
|
||||||
|
// 4. Procesar cada respuesta
|
||||||
|
for (const respuestaDto of submitDto.respuestas) {
|
||||||
|
// Buscar la pregunta
|
||||||
|
const pregunta = await this.preguntaRepository.findOne({
|
||||||
|
where: { id_pregunta: respuestaDto.id_pregunta },
|
||||||
|
relations: ['tipoPregunta']
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!pregunta) {
|
||||||
|
throw new NotFoundException(`Pregunta con ID ${respuestaDto.id_pregunta} no encontrada`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si la pregunta tiene una validación configurada y es una respuesta abierta, validarla
|
||||||
|
if (pregunta.validacion && typeof respuestaDto.valor === 'string') {
|
||||||
|
const resultadoValidacion = this.validadorRespuestasService.validarRespuesta(
|
||||||
|
respuestaDto.valor,
|
||||||
|
pregunta.validacion
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!resultadoValidacion.valido) {
|
||||||
|
throw new BadRequestException(`Validación fallida en pregunta ${pregunta.id_pregunta}: ${resultadoValidacion.mensaje}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determinar tipo de pregunta por ID
|
||||||
|
const esPreguntaAbierta = pregunta.id_tipo_pregunta === 2;
|
||||||
|
|
||||||
|
// O determinar tipo de pregunta por su nombre si existe
|
||||||
|
const esPreguntaTipoAbierta = pregunta.tipoPregunta &&
|
||||||
|
pregunta.tipoPregunta.tipo_pregunta === 'Abierta';
|
||||||
|
|
||||||
|
// Determinar tipo de pregunta y guardar respuesta adecuadamente
|
||||||
|
if (esPreguntaAbierta || esPreguntaTipoAbierta) {
|
||||||
|
// Respuesta abierta (texto)
|
||||||
|
// Convertir a string y truncar a 250 caracteres si es necesario
|
||||||
|
let respuestaTexto = String(respuestaDto.valor);
|
||||||
|
if (respuestaTexto.length > 250) {
|
||||||
|
respuestaTexto = respuestaTexto.substring(0, 250);
|
||||||
|
}
|
||||||
|
|
||||||
|
const respuestaAbierta = new RespuestaParticipanteAbierta();
|
||||||
|
respuestaAbierta.pregunta = pregunta;
|
||||||
|
respuestaAbierta.respuesta = respuestaTexto;
|
||||||
|
respuestaAbierta.cuestionarioRespondido = savedCuestionarioRespondido;
|
||||||
|
|
||||||
|
await queryRunner.manager.save(respuestaAbierta);
|
||||||
|
} else {
|
||||||
|
// Respuesta cerrada (opción)
|
||||||
|
// Verificar si es una respuesta múltiple (array de IDs)
|
||||||
|
const opcionIds = Array.isArray(respuestaDto.valor)
|
||||||
|
? respuestaDto.valor
|
||||||
|
: [respuestaDto.valor];
|
||||||
|
|
||||||
|
// Procesar cada ID de opción
|
||||||
|
for (const opcionId of opcionIds) {
|
||||||
|
// Buscar la relación entre pregunta y opción usando el ID de opción
|
||||||
|
const preguntaOpciones = await this.preguntaOpcionRepository
|
||||||
|
.createQueryBuilder('po')
|
||||||
|
.innerJoinAndSelect('po.opcion', 'opcion')
|
||||||
|
.where('po.id_pregunta = :preguntaId', { preguntaId: pregunta.id_pregunta })
|
||||||
|
.andWhere('opcion.id_opcion = :opcionId', { opcionId: opcionId })
|
||||||
|
.getMany();
|
||||||
|
|
||||||
|
if (preguntaOpciones.length === 0) {
|
||||||
|
throw new BadRequestException(`La opción con ID ${opcionId} no está asociada a la pregunta ${respuestaDto.id_pregunta}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guardar respuesta cerrada
|
||||||
|
const respuestaCerrada = new RespuestaParticipanteCerrada();
|
||||||
|
respuestaCerrada.preguntaOpcion = preguntaOpciones[0];
|
||||||
|
respuestaCerrada.cuestionarioRespondido = savedCuestionarioRespondido;
|
||||||
|
|
||||||
|
await queryRunner.manager.save(respuestaCerrada);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Variables para el QR y correo electrónico
|
||||||
|
let datosQR: {
|
||||||
|
id_participante: number;
|
||||||
|
id_evento: number;
|
||||||
|
} | null = null;
|
||||||
|
|
||||||
|
// Capturamos los datos para el QR
|
||||||
|
if (cuestionario.evento && cuestionario.evento.id_evento) {
|
||||||
|
datosQR = {
|
||||||
|
id_participante: participante.id_participante,
|
||||||
|
id_evento: cuestionario.evento.id_evento
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Primero completamos la transacción principal
|
||||||
|
await queryRunner.commitTransaction();
|
||||||
|
|
||||||
|
// Después de la transacción principal, intentamos registrar la relación participante-evento
|
||||||
|
// y enviar el correo como operaciones independientes
|
||||||
|
if (cuestionario.evento && cuestionario.evento.id_evento) {
|
||||||
|
try {
|
||||||
|
// Registrar participante en evento
|
||||||
|
try {
|
||||||
|
// Verificar si ya existe una relación participante-evento
|
||||||
|
const participanteEventoExistente = await this.dataSource.query(
|
||||||
|
`SELECT * FROM participante_evento WHERE id_participante = ? AND id_evento = ?`,
|
||||||
|
[participante.id_participante, cuestionario.evento.id_evento]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Si no existe, crear la relación
|
||||||
|
if (!participanteEventoExistente || participanteEventoExistente.length === 0) {
|
||||||
|
await this.dataSource.query(
|
||||||
|
`INSERT INTO participante_evento (id_participante, id_evento, fecha_inscripcion, estatus) VALUES (?, ?, ?, ?)`,
|
||||||
|
[participante.id_participante, cuestionario.evento.id_evento, new Date(), true]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (dbError) {
|
||||||
|
console.error('Error al registrar participante en evento:', dbError.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generar código QR
|
||||||
|
const qrJsonData = JSON.stringify(datosQR);
|
||||||
|
const qrBuffer = await QRCode.toBuffer(qrJsonData);
|
||||||
|
const qrDataUrl = await QRCode.toDataURL(qrJsonData);
|
||||||
|
|
||||||
|
console.log('QR Data URL:', qrDataUrl);
|
||||||
|
console.log("antes de enviar correo");
|
||||||
|
|
||||||
|
// Enviar correo con el QR
|
||||||
|
const urlApiCorreos = process.env.url_api_correos || 'http://localhost:3000';
|
||||||
|
|
||||||
|
await axios.post(`${urlApiCorreos}/mail/send`, {
|
||||||
|
to: participante.correo,
|
||||||
|
subject: 'Asistencia QR',
|
||||||
|
fecha_recibido: "2025-03-10T12:00:00Z",
|
||||||
|
html: `
|
||||||
|
<h1>¡Gracias por registrarte!</h1>
|
||||||
|
<p>Has completado exitosamente el formulario: ${cuestionario.nombre_form}</p>
|
||||||
|
<p>Este es tu QR para registrar asistencia:</p>
|
||||||
|
<img src="cid:qrCode" alt="Código QR" />
|
||||||
|
<p>Tus datos de registro:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Evento: ${cuestionario.evento.nombre_evento || 'Evento'}</li>
|
||||||
|
<li>Correo: ${participante.correo}</li>
|
||||||
|
<li>Fecha de registro: ${new Date().toLocaleString()}</li>
|
||||||
|
</ul>
|
||||||
|
`,
|
||||||
|
adjuntos: [
|
||||||
|
{
|
||||||
|
filename: 'qr.png',
|
||||||
|
content: qrBuffer.toString('base64'),
|
||||||
|
encoding: 'base64',
|
||||||
|
cid: 'qrCode'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Correo enviado exitosamente a ${participante.correo}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error después de la transacción principal:', error);
|
||||||
|
// No lanzamos el error para que no afecte la respuesta al usuario
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preparar respuesta
|
||||||
|
const response: any = {
|
||||||
|
success: true,
|
||||||
|
message: 'Respuestas guardadas correctamente',
|
||||||
|
cuestionarioRespondidoId: savedCuestionarioRespondido.idCuestionarioRespondido
|
||||||
|
};
|
||||||
|
|
||||||
|
// Incluir IDs para generar QR si hay evento asociado
|
||||||
|
if (datosQR) {
|
||||||
|
response.datos_qr = datosQR;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
// Solo hacemos rollback si no hemos hecho commit todavía
|
||||||
|
try {
|
||||||
|
await queryRunner.rollbackTransaction();
|
||||||
|
} catch (rollbackError) {
|
||||||
|
console.error('Error durante rollback:', rollbackError.message);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
await queryRunner.release();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
findOne(id: number) {
|
async findAll() {
|
||||||
return `This action returns a #${id} cuestionarioRespondido`;
|
// Obtener todos los cuestionarios respondidos con sus relaciones básicas
|
||||||
|
const cuestionariosRespondidos = await this.cuestionarioRespondidoRepository.find({
|
||||||
|
relations: ['participante', 'cuestionario', 'respuestasAbiertas', 'respuestasAbiertas.pregunta', 'respuestasCerradas']
|
||||||
|
});
|
||||||
|
|
||||||
|
// Para cada cuestionario respondido, obtener y formatear las respuestas cerradas
|
||||||
|
const resultados = await Promise.all(
|
||||||
|
cuestionariosRespondidos.map(async (cuestionarioRespondido) => {
|
||||||
|
// Obtener datos de respuestas cerradas con consulta SQL
|
||||||
|
const respuestasCerradas = await this.dataSource.query(`
|
||||||
|
SELECT
|
||||||
|
rpc.idRespuestaParticipanteCerrada,
|
||||||
|
rpc.id_pregunta_opcion,
|
||||||
|
po.id_pregunta,
|
||||||
|
p.pregunta,
|
||||||
|
p.id_tipo_pregunta,
|
||||||
|
o.id_opcion,
|
||||||
|
o.opcion
|
||||||
|
FROM respuesta_participante_cerrada rpc
|
||||||
|
LEFT JOIN pregunta_opcion po ON rpc.id_pregunta_opcion = po.id_pregunta_opcion
|
||||||
|
LEFT JOIN pregunta p ON po.id_pregunta = p.id_pregunta
|
||||||
|
LEFT JOIN opcion o ON po.id_opcion = o.id_opcion
|
||||||
|
WHERE rpc.id_cuestionario_respondido = ?
|
||||||
|
`, [cuestionarioRespondido.idCuestionarioRespondido]);
|
||||||
|
|
||||||
|
// Formatear respuestas cerradas
|
||||||
|
const respuestasCerradasFormateadas = respuestasCerradas.map(respuesta => ({
|
||||||
|
idRespuestaParticipanteCerrada: respuesta.idRespuestaParticipanteCerrada,
|
||||||
|
id_pregunta_opcion: respuesta.id_pregunta_opcion,
|
||||||
|
pregunta: {
|
||||||
|
id_pregunta: respuesta.id_pregunta,
|
||||||
|
texto_pregunta: respuesta.pregunta,
|
||||||
|
id_tipo_pregunta: respuesta.id_tipo_pregunta
|
||||||
|
},
|
||||||
|
opcion: {
|
||||||
|
id_opcion: respuesta.id_opcion,
|
||||||
|
opcion: respuesta.opcion
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Devolver cuestionario con respuestas formateadas
|
||||||
|
return {
|
||||||
|
...cuestionarioRespondido,
|
||||||
|
respuestasCerradas: respuestasCerradasFormateadas
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return resultados;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOne(id: number) {
|
||||||
|
// Obtener cuestionario respondido con sus relaciones básicas
|
||||||
|
const cuestionarioRespondido = await this.cuestionarioRespondidoRepository.findOne({
|
||||||
|
where: { idCuestionarioRespondido: id },
|
||||||
|
relations: [
|
||||||
|
'participante',
|
||||||
|
'cuestionario',
|
||||||
|
'respuestasAbiertas',
|
||||||
|
'respuestasAbiertas.pregunta',
|
||||||
|
'respuestasCerradas'
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!cuestionarioRespondido) {
|
||||||
|
throw new NotFoundException(`Cuestionario respondido con ID ${id} no encontrado`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtener directamente los datos de respuestas cerradas desde la base de datos
|
||||||
|
// usando una consulta SQL directa
|
||||||
|
const respuestasCerradas = await this.dataSource.query(`
|
||||||
|
SELECT
|
||||||
|
rpc.idRespuestaParticipanteCerrada,
|
||||||
|
rpc.id_pregunta_opcion,
|
||||||
|
po.id_pregunta,
|
||||||
|
p.pregunta,
|
||||||
|
p.id_tipo_pregunta,
|
||||||
|
o.id_opcion,
|
||||||
|
o.opcion
|
||||||
|
FROM respuesta_participante_cerrada rpc
|
||||||
|
LEFT JOIN pregunta_opcion po ON rpc.id_pregunta_opcion = po.id_pregunta_opcion
|
||||||
|
LEFT JOIN pregunta p ON po.id_pregunta = p.id_pregunta
|
||||||
|
LEFT JOIN opcion o ON po.id_opcion = o.id_opcion
|
||||||
|
WHERE rpc.id_cuestionario_respondido = ?
|
||||||
|
`, [id]);
|
||||||
|
|
||||||
|
console.log('Respuestas cerradas SQL:', JSON.stringify(respuestasCerradas, null, 2));
|
||||||
|
|
||||||
|
// Transformar las respuestas cerradas al formato deseado
|
||||||
|
const respuestasCerradasFormateadas = respuestasCerradas.map(respuesta => ({
|
||||||
|
idRespuestaParticipanteCerrada: respuesta.idRespuestaParticipanteCerrada,
|
||||||
|
id_pregunta_opcion: respuesta.id_pregunta_opcion,
|
||||||
|
pregunta: {
|
||||||
|
id_pregunta: respuesta.id_pregunta,
|
||||||
|
texto_pregunta: respuesta.pregunta,
|
||||||
|
id_tipo_pregunta: respuesta.id_tipo_pregunta
|
||||||
|
},
|
||||||
|
opcion: {
|
||||||
|
id_opcion: respuesta.id_opcion,
|
||||||
|
opcion: respuesta.opcion
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Crear el objeto de respuesta con el formato deseado
|
||||||
|
const resultado = {
|
||||||
|
...cuestionarioRespondido,
|
||||||
|
respuestasCerradas: respuestasCerradasFormateadas
|
||||||
|
};
|
||||||
|
|
||||||
|
return resultado;
|
||||||
}
|
}
|
||||||
|
|
||||||
update(id: number, updateCuestionarioRespondidoDto: UpdateCuestionarioRespondidoDto) {
|
update(id: number, updateCuestionarioRespondidoDto: UpdateCuestionarioRespondidoDto) {
|
||||||
@@ -23,4 +386,98 @@ export class CuestionarioRespondidoService {
|
|||||||
remove(id: number) {
|
remove(id: number) {
|
||||||
return `This action removes a #${id} cuestionarioRespondido`;
|
return `This action removes a #${id} cuestionarioRespondido`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async generarReporteRespuestas(idCuestionario: number): Promise<string> {
|
||||||
|
// Buscar el cuestionario para validar que existe
|
||||||
|
const cuestionario = await this.cuestionarioRepository.findOne({
|
||||||
|
where: { id_cuestionario: idCuestionario },
|
||||||
|
relations: ['evento'],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!cuestionario) {
|
||||||
|
throw new NotFoundException(`Cuestionario con ID ${idCuestionario} no encontrado`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtener todas las respuestas para este cuestionario con sus relaciones
|
||||||
|
const respuestas = await this.dataSource.query(`
|
||||||
|
SELECT
|
||||||
|
cr.id_cuestionario_respondido,
|
||||||
|
cr.fecha_respuesta,
|
||||||
|
p.id_participante,
|
||||||
|
p.correo AS correo_participante,
|
||||||
|
c.id_cuestionario,
|
||||||
|
c.nombre_form AS nombre_cuestionario,
|
||||||
|
e.id_evento,
|
||||||
|
e.nombre_evento,
|
||||||
|
preg.id_pregunta,
|
||||||
|
preg.pregunta,
|
||||||
|
-- Para respuestas abiertas
|
||||||
|
rpa.respuesta AS respuesta_abierta,
|
||||||
|
-- Para respuestas cerradas
|
||||||
|
o.id_opcion,
|
||||||
|
o.opcion AS respuesta_cerrada
|
||||||
|
FROM cuestionario_respondido cr
|
||||||
|
JOIN participante p ON cr.id_participante = p.id_participante
|
||||||
|
JOIN cuestionario c ON cr.id_cuestionario = c.id_cuestionario
|
||||||
|
LEFT JOIN evento e ON c.id_evento = e.id_evento
|
||||||
|
-- Respuestas abiertas
|
||||||
|
LEFT JOIN respuesta_participante_abierta rpa ON rpa.id_cuestionario_respondido = cr.id_cuestionario_respondido
|
||||||
|
LEFT JOIN pregunta preg_abierta ON rpa.id_pregunta = preg_abierta.id_pregunta
|
||||||
|
-- Respuestas cerradas
|
||||||
|
LEFT JOIN respuesta_participante_cerrada rpc ON rpc.id_cuestionario_respondido = cr.id_cuestionario_respondido
|
||||||
|
LEFT JOIN pregunta_opcion po ON rpc.id_pregunta_opcion = po.id_pregunta_opcion
|
||||||
|
LEFT JOIN pregunta preg ON po.id_pregunta = preg.id_pregunta
|
||||||
|
LEFT JOIN opcion o ON po.id_opcion = o.id_opcion
|
||||||
|
WHERE c.id_cuestionario = ?
|
||||||
|
ORDER BY cr.id_cuestionario_respondido, preg.id_pregunta
|
||||||
|
`, [idCuestionario]);
|
||||||
|
|
||||||
|
// Preparar los datos para el CSV
|
||||||
|
const datosReporte: string[][] = [];
|
||||||
|
|
||||||
|
// Agregar encabezados
|
||||||
|
const encabezados: string[] = [
|
||||||
|
'ID Respuesta',
|
||||||
|
'Fecha Respuesta',
|
||||||
|
'ID Participante',
|
||||||
|
'Correo Participante',
|
||||||
|
'Evento',
|
||||||
|
'Cuestionario',
|
||||||
|
'Pregunta',
|
||||||
|
'Respuesta'
|
||||||
|
];
|
||||||
|
|
||||||
|
datosReporte.push(encabezados);
|
||||||
|
|
||||||
|
// Procesar los resultados para el CSV
|
||||||
|
for (const fila of respuestas) {
|
||||||
|
const respuesta = fila.respuesta_abierta || fila.respuesta_cerrada || '';
|
||||||
|
|
||||||
|
datosReporte.push([
|
||||||
|
fila.id_cuestionario_respondido?.toString() || '',
|
||||||
|
new Date(fila.fecha_respuesta).toLocaleString(),
|
||||||
|
fila.id_participante?.toString() || '',
|
||||||
|
fila.correo_participante || '',
|
||||||
|
fila.nombre_evento || 'Sin evento',
|
||||||
|
fila.nombre_cuestionario || '',
|
||||||
|
fila.pregunta || '',
|
||||||
|
respuesta
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generar CSV de forma manual con valores separados por comas y líneas con saltos de línea
|
||||||
|
// Escapar valores que contengan comas para evitar problemas con el formato CSV
|
||||||
|
const csvContent = datosReporte.map(row =>
|
||||||
|
row.map(value => {
|
||||||
|
// Si el valor contiene comas, comillas o saltos de línea, encerrarlo en comillas dobles
|
||||||
|
// y escapar cualquier comilla doble dentro del valor
|
||||||
|
if (value.includes(',') || value.includes('"') || value.includes('\n')) {
|
||||||
|
return `"${value.replace(/"/g, '""')}"`;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}).join(',')
|
||||||
|
).join('\n');
|
||||||
|
|
||||||
|
return csvContent;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import {
|
||||||
|
IsArray,
|
||||||
|
IsDateString,
|
||||||
|
IsNotEmpty,
|
||||||
|
IsNumber,
|
||||||
|
IsOptional,
|
||||||
|
IsString,
|
||||||
|
ValidateNested,
|
||||||
|
ValidateIf,
|
||||||
|
MaxLength
|
||||||
|
} from 'class-validator';
|
||||||
|
import { Type, Transform } from 'class-transformer';
|
||||||
|
|
||||||
|
export class RespuestaDto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'ID de la pregunta respondida',
|
||||||
|
example: 101
|
||||||
|
})
|
||||||
|
@IsNumber()
|
||||||
|
id_pregunta: number;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Valor de la respuesta según tipo de pregunta',
|
||||||
|
examples: [
|
||||||
|
'Texto para pregunta abierta', // String para preguntas abiertas
|
||||||
|
5, // Número para preguntas cerradas (ID de opción)
|
||||||
|
[2, 7, 9] // Array para preguntas múltiples (IDs de opciones)
|
||||||
|
],
|
||||||
|
oneOf: [
|
||||||
|
{ type: 'string', description: 'Texto para pregunta abierta (máximo 250 caracteres)' },
|
||||||
|
{ type: 'number', description: 'ID de opción para pregunta cerrada' },
|
||||||
|
{
|
||||||
|
type: 'array',
|
||||||
|
items: { type: 'number' },
|
||||||
|
description: 'Array de IDs de opciones para pregunta multiple'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
@ValidateIf(o => typeof o.valor === 'string')
|
||||||
|
@MaxLength(250, { message: 'Las respuestas de texto no pueden exceder 250 caracteres' })
|
||||||
|
valor: string | number | number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SubmitRespuestasDto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'ID del formulario a responder',
|
||||||
|
example: 1
|
||||||
|
})
|
||||||
|
@IsNumber()
|
||||||
|
id_formulario: number;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Correo electrónico del participante',
|
||||||
|
example: 'usuario@ejemplo.com'
|
||||||
|
})
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
@MaxLength(250, { message: 'El correo electrónico no puede exceder 250 caracteres' })
|
||||||
|
correo: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Array de respuestas a las preguntas',
|
||||||
|
type: [RespuestaDto],
|
||||||
|
examples: [
|
||||||
|
{
|
||||||
|
id_pregunta: 101,
|
||||||
|
valor: 'Texto para pregunta abierta'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id_pregunta: 102,
|
||||||
|
valor: 5 // ID de la opción para pregunta cerrada simple
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id_pregunta: 103,
|
||||||
|
valor: [2, 7, 9] // Array de IDs de opciones para pregunta de selección múltiple
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
@IsArray()
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => RespuestaDto)
|
||||||
|
respuestas: RespuestaDto[];
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Fecha y hora del envío',
|
||||||
|
example: '2025-04-02T13:45:00'
|
||||||
|
})
|
||||||
|
@IsDateString()
|
||||||
|
@IsOptional()
|
||||||
|
fecha_envio?: string;
|
||||||
|
}
|
||||||
@@ -3,6 +3,8 @@ import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, OneToMany, JoinColum
|
|||||||
import { Cuestionario } from 'src/cuestionario/entities/cuestionario.entity';
|
import { Cuestionario } from 'src/cuestionario/entities/cuestionario.entity';
|
||||||
import { RespuestaParticipanteCerrada } from 'src/respuesta_participante_cerrada/entities/respuesta_participante_cerrada.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 { RespuestaParticipanteAbierta } from 'src/respuesta_participante_abierta/entities/respuesta_participante_abierta.entity';
|
||||||
|
import { Participante } from 'src/participante/participante.entity';
|
||||||
|
import { Opcion } from 'src/opcion/entities/opcion.entity';
|
||||||
|
|
||||||
@Entity('cuestionario_respondido')
|
@Entity('cuestionario_respondido')
|
||||||
export class CuestionarioRespondido {
|
export class CuestionarioRespondido {
|
||||||
@@ -15,18 +17,18 @@ export class CuestionarioRespondido {
|
|||||||
// Relación con Participante
|
// Relación con Participante
|
||||||
@ManyToOne(() => Participante, participante => participante.cuestionariosRespondidos)
|
@ManyToOne(() => Participante, participante => participante.cuestionariosRespondidos)
|
||||||
@JoinColumn({ name: 'id_participante' })
|
@JoinColumn({ name: 'id_participante' })
|
||||||
id_participante: Participante;
|
participante: Participante;
|
||||||
|
|
||||||
// Relación con Cuestionario
|
// Relación con Cuestionario
|
||||||
@ManyToOne(() => Cuestionario, cuestionario => cuestionario.id_cuestionario)
|
@ManyToOne(() => Cuestionario)
|
||||||
@JoinColumn({ name: 'id_cuestionario' })
|
@JoinColumn({ name: 'id_cuestionario' })
|
||||||
id_cuestionario: Cuestionario;
|
cuestionario: Cuestionario;
|
||||||
|
|
||||||
// Relación con RespuestasCerradas
|
// Relación con RespuestasCerradas
|
||||||
@OneToMany(() => RespuestaParticipanteCerrada, respuestaCerrada => respuestaCerrada.id_cuestionario_respondido)
|
@OneToMany(() => RespuestaParticipanteCerrada, respuestaCerrada => respuestaCerrada.cuestionarioRespondido)
|
||||||
respuestasCerradas: RespuestaParticipanteCerrada[];
|
respuestasCerradas: RespuestaParticipanteCerrada[];
|
||||||
|
|
||||||
// Relación con RespuestasAbiertas
|
// Relación con RespuestasAbiertas
|
||||||
@OneToMany(() => RespuestaParticipanteAbierta, respuestaAbierta => respuestaAbierta.id_cuestionario_respondido)
|
@OneToMany(() => RespuestaParticipanteAbierta, respuestaAbierta => respuestaAbierta.cuestionarioRespondido)
|
||||||
respuestasAbiertas: RespuestaParticipanteAbierta[];
|
respuestasAbiertas: RespuestaParticipanteAbierta[];
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,19 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { CuestionarioSeccionService } from './cuestionario_seccion.service';
|
import { CuestionarioSeccionService } from './cuestionario_seccion.service';
|
||||||
import { CuestionarioSeccionController } from './cuestionario_seccion.controller';
|
import { CuestionarioSeccionController } from './cuestionario_seccion.controller';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { CuestionarioSeccion } from './entities/cuestionario_seccion.entity';
|
||||||
|
import { CuestionarioModule } from '../cuestionario/cuestionario.module';
|
||||||
|
import { SeccionModule } from '../seccion/seccion.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([CuestionarioSeccion]),
|
||||||
|
CuestionarioModule,
|
||||||
|
SeccionModule
|
||||||
|
],
|
||||||
controllers: [CuestionarioSeccionController],
|
controllers: [CuestionarioSeccionController],
|
||||||
providers: [CuestionarioSeccionService],
|
providers: [CuestionarioSeccionService],
|
||||||
|
exports: [TypeOrmModule]
|
||||||
})
|
})
|
||||||
export class CuestionarioSeccionModule {}
|
export class CuestionarioSeccionModule {}
|
||||||
|
|||||||
@@ -11,11 +11,15 @@ export class CuestionarioSeccion {
|
|||||||
posicion: number;
|
posicion: number;
|
||||||
|
|
||||||
|
|
||||||
@ManyToOne(() => Cuestionario, (cuestionario) => cuestionario.id_cuestionario)
|
@ManyToOne(() => Cuestionario, (cuestionario) => cuestionario.cuestionarioSeccion)
|
||||||
|
cuestionario: Cuestionario;
|
||||||
|
|
||||||
@Column({ type: 'int' })
|
@Column({ type: 'int' })
|
||||||
id_cuestionario: number;
|
id_cuestionario: number;
|
||||||
|
|
||||||
@ManyToOne(() => Seccion, (seccion) => seccion.id_seccion)
|
@ManyToOne(() => Seccion, (seccion) => seccion.seccion)
|
||||||
|
seccion: Seccion;
|
||||||
|
|
||||||
@Column({type:'int'})
|
@Column({type:'int'})
|
||||||
id_seccion:number;
|
id_seccion:number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { INestApplication } from '@nestjs/common';
|
|||||||
@Module({})
|
@Module({})
|
||||||
export class DocsModule {
|
export class DocsModule {
|
||||||
static setupSwagger(app: INestApplication) {
|
static setupSwagger(app: INestApplication) {
|
||||||
const document = SwaggerModule.createDocument(app, swaggerConfig);
|
const document = SwaggerModule.createDocument(app, swaggerConfig);
|
||||||
SwaggerModule.setup('api-docs', app, document);
|
SwaggerModule.setup('api-docs', app, document);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,35 @@
|
|||||||
import { DocumentBuilder } from '@nestjs/swagger';
|
import { DocumentBuilder } from '@nestjs/swagger';
|
||||||
|
|
||||||
export const swaggerConfig = new DocumentBuilder()
|
export const swaggerConfig = new DocumentBuilder()
|
||||||
.setTitle('Sistema de Registro de Usuarios y Eventos')
|
.setTitle('Sistema de Formularios y Eventos CIDWA')
|
||||||
.setDescription('API para gestionar usuarios, eventos, asistencia y códigos QR')
|
.setDescription(`
|
||||||
|
API para la gestión de formularios, cuestionarios, eventos y asistencias.
|
||||||
|
|
||||||
|
## Funcionalidades principales
|
||||||
|
|
||||||
|
- **Cuestionarios**: Creación y gestión de formularios con secciones y preguntas
|
||||||
|
- **Eventos**: Registro y control de eventos
|
||||||
|
- **Participantes**: Gestión de usuarios y participantes
|
||||||
|
- **Asistencia**: Control de asistencia a eventos
|
||||||
|
- **QR**: Generación y validación de códigos QR
|
||||||
|
|
||||||
|
## Ejemplos de uso
|
||||||
|
|
||||||
|
La API permite crear cuestionarios complejos con secciones y diferentes tipos de preguntas.
|
||||||
|
Consulte la documentación de cada endpoint para ver ejemplos específicos.
|
||||||
|
`)
|
||||||
|
.addServer(process.env.SWAGGER_SERVER_URL || 'http://localhost:3000')
|
||||||
|
.addServer( 'http://localhost:3000')
|
||||||
|
|
||||||
|
|
||||||
.setVersion('1.0')
|
.setVersion('1.0')
|
||||||
|
.addTag('Cuestionarios')
|
||||||
|
.addTag('Secciones')
|
||||||
|
.addTag('Preguntas')
|
||||||
|
.addTag('Opciones')
|
||||||
.addTag('Usuarios')
|
.addTag('Usuarios')
|
||||||
.addTag('Eventos')
|
.addTag('Eventos')
|
||||||
.addTag('Asistencia')
|
.addTag('Asistencia')
|
||||||
.addTag('QR')
|
.addTag('QR')
|
||||||
|
.addBearerAuth()
|
||||||
.build();
|
.build();
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
import { Participante } from "src/participante/participante.entity";
|
||||||
import { ParticipanteEvento } from "src/participante_evento/participante_evento.entity";
|
import { ParticipanteEvento } from "src/participante_evento/participante_evento.entity";
|
||||||
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm";
|
import { Column, Entity, ManyToMany, OneToMany, PrimaryGeneratedColumn } from "typeorm";
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class Evento {
|
export class Evento {
|
||||||
@@ -26,13 +27,15 @@ export class Evento {
|
|||||||
@JoinColumn({ name: "id_administrador" })
|
@JoinColumn({ name: "id_administrador" })
|
||||||
administrador: Administrador;
|
administrador: Administrador;
|
||||||
|
|
||||||
@OneToMany(() => ParticipanteEvento, (pe) => pe.evento)
|
|
||||||
participantes: ParticipanteEvento[];
|
|
||||||
|
|
||||||
@OneToMany(() => Asistencia, (asistencia) => asistencia.evento)
|
@OneToMany(() => Asistencia, (asistencia) => asistencia.evento)
|
||||||
asistencias: Asistencia[];
|
asistencias: Asistencia[];
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@OneToMany(() => ParticipanteEvento, (participanteEvento) => participanteEvento.evento)
|
|
||||||
participantes: ParticipanteEvento[];
|
|
||||||
|
@OneToMany(() => ParticipanteEvento, participanteEvento => participanteEvento.evento)
|
||||||
|
participanteEventos: ParticipanteEvento[];
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,7 @@ import { Evento } from './evento.entity';
|
|||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([Evento])],
|
imports: [TypeOrmModule.forFeature([Evento])],
|
||||||
controllers: [EventoController],
|
controllers: [EventoController],
|
||||||
providers: [EventoService]
|
providers: [EventoService],
|
||||||
|
exports: [EventoService]
|
||||||
})
|
})
|
||||||
export class EventoModule {}
|
export class EventoModule {}
|
||||||
|
|||||||
+51
-1
@@ -1,8 +1,58 @@
|
|||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from '@nestjs/core';
|
||||||
import { AppModule } from './app.module';
|
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';
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const app = await NestFactory.create(AppModule);
|
const app = await NestFactory.create(AppModule);
|
||||||
await app.listen(process.env.PORT ?? 3000);
|
|
||||||
|
// Configuración de validación global
|
||||||
|
app.useGlobalPipes(
|
||||||
|
new ValidationPipe({
|
||||||
|
whitelist: true, // Elimina propiedades no decoradas
|
||||||
|
forbidNonWhitelisted: true, // Arroja error si hay propiedades no decoradas
|
||||||
|
transform: true, // Transforma los datos recibidos al tipo definido en el DTO
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// CORS configuration
|
||||||
|
app.enableCors({
|
||||||
|
origin: '*',
|
||||||
|
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
|
||||||
|
allowedHeaders: ['Content-Type', 'Authorization'],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Swagger setup
|
||||||
|
DocsModule.setupSwagger(app);
|
||||||
|
|
||||||
|
// Seed TipoPregunta data
|
||||||
|
const dataSource = app.get(DataSource);
|
||||||
|
const tiposPreguntaRepository = dataSource.getRepository(TipoPregunta);
|
||||||
|
|
||||||
|
// Check if data exists
|
||||||
|
const count = await tiposPreguntaRepository.count();
|
||||||
|
|
||||||
|
if (count === 0) {
|
||||||
|
// Create default tipos de pregunta
|
||||||
|
const tiposPregunta = [
|
||||||
|
{ tipo_pregunta: 'Texto' },
|
||||||
|
{ tipo_pregunta: 'Numero' },
|
||||||
|
{ tipo_pregunta: 'Radio' },
|
||||||
|
{ tipo_pregunta: 'Multiple' },
|
||||||
|
{ tipo_pregunta: 'Abierto' },
|
||||||
|
{ tipo_pregunta: 'Fecha' },
|
||||||
|
];
|
||||||
|
|
||||||
|
await tiposPreguntaRepository.save(tiposPregunta);
|
||||||
|
console.log('Tipos de pregunta creados');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the server
|
||||||
|
await app.listen(process.env.PORT ?? 4200);
|
||||||
|
console.log('API en ejecución en http://localhost:4200');
|
||||||
|
console.log('Swagger disponible en http://localhost:4200/api-docs');
|
||||||
}
|
}
|
||||||
bootstrap();
|
bootstrap();
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { IsString, MaxLength } from "class-validator";
|
import { IsString, MaxLength } from "class-validator";
|
||||||
|
|
||||||
export class CreateOpcionDto {
|
export class CreateOpcionDto {
|
||||||
|
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
@MaxLength(50)
|
@MaxLength(50)
|
||||||
opcion: string;
|
opcion: string;
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ export class Opcion {
|
|||||||
@Column({ type: String, nullable: false, length: 200, default: '' })
|
@Column({ type: String, nullable: false, length: 200, default: '' })
|
||||||
opcion: string;
|
opcion: string;
|
||||||
|
|
||||||
@OneToMany(() => PreguntaOpcion, (preguntaOpcion) => preguntaOpcion.id_opcion)
|
@OneToMany(() => PreguntaOpcion, (preguntaOpcion) => preguntaOpcion.opcion)
|
||||||
Preguntas: PreguntaOpcion[];
|
preguntasOpciones: PreguntaOpcion[];
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -2,32 +2,39 @@ import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/commo
|
|||||||
import { OpcionService } from './opcion.service';
|
import { OpcionService } from './opcion.service';
|
||||||
import { CreateOpcionDto } from './dto/create-opcion.dto';
|
import { CreateOpcionDto } from './dto/create-opcion.dto';
|
||||||
import { UpdateOpcionDto } from './dto/update-opcion.dto';
|
import { UpdateOpcionDto } from './dto/update-opcion.dto';
|
||||||
|
import { OpcionApiDocumentation } from './opcion.documentation';
|
||||||
|
|
||||||
@Controller('opcion')
|
@Controller('opcion')
|
||||||
|
@OpcionApiDocumentation.ApiController
|
||||||
export class OpcionController {
|
export class OpcionController {
|
||||||
constructor(private readonly opcionService: OpcionService) {}
|
constructor(private readonly opcionService: OpcionService) {}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
|
@OpcionApiDocumentation.ApiCreate
|
||||||
create(@Body() createOpcionDto: CreateOpcionDto) {
|
create(@Body() createOpcionDto: CreateOpcionDto) {
|
||||||
return this.opcionService.create(createOpcionDto);
|
return this.opcionService.create(createOpcionDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
|
@OpcionApiDocumentation.ApiGetAll
|
||||||
findAll() {
|
findAll() {
|
||||||
return this.opcionService.findAll();
|
return this.opcionService.findAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
|
@OpcionApiDocumentation.ApiGetOne
|
||||||
findOne(@Param('id') id: string) {
|
findOne(@Param('id') id: string) {
|
||||||
return this.opcionService.findOne(+id);
|
return this.opcionService.findOne(+id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Patch(':id')
|
@Patch(':id')
|
||||||
|
@OpcionApiDocumentation.ApiUpdate
|
||||||
update(@Param('id') id: string, @Body() updateOpcionDto: UpdateOpcionDto) {
|
update(@Param('id') id: string, @Body() updateOpcionDto: UpdateOpcionDto) {
|
||||||
return this.opcionService.update(+id, updateOpcionDto);
|
return this.opcionService.update(+id, updateOpcionDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
|
@OpcionApiDocumentation.ApiRemove
|
||||||
remove(@Param('id') id: string) {
|
remove(@Param('id') id: string) {
|
||||||
return this.opcionService.remove(+id);
|
return this.opcionService.remove(+id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
import { ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||||
|
import { applyDecorators } from '@nestjs/common';
|
||||||
|
|
||||||
|
export class OpcionApiDocumentation {
|
||||||
|
// Decoradores para toda la clase del controlador
|
||||||
|
static ApiController = ApiTags('Opciones');
|
||||||
|
|
||||||
|
// Documentación para crear una opción
|
||||||
|
static ApiCreate = applyDecorators(
|
||||||
|
ApiOperation({
|
||||||
|
summary: 'Crear una nueva opción',
|
||||||
|
description: 'Crea una nueva opción para una pregunta'
|
||||||
|
}),
|
||||||
|
ApiBody({
|
||||||
|
description: 'Datos de la opción a crear',
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
required: ['opcion'],
|
||||||
|
properties: {
|
||||||
|
opcion: { type: 'string', example: 'Si' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
ApiResponse({
|
||||||
|
status: 201,
|
||||||
|
description: 'Opción creada correctamente',
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id_opcion: { type: 'number', example: 1 },
|
||||||
|
opcion: { type: 'string', example: 'Si' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
ApiResponse({ status: 400, description: 'Datos de la opción inválidos' }),
|
||||||
|
ApiResponse({ status: 500, description: 'Error interno del servidor' })
|
||||||
|
);
|
||||||
|
|
||||||
|
// Documentación para obtener todas las opciones
|
||||||
|
static ApiGetAll = applyDecorators(
|
||||||
|
ApiOperation({
|
||||||
|
summary: 'Obtener todas las opciones',
|
||||||
|
description: 'Retorna una lista de todas las opciones registradas'
|
||||||
|
}),
|
||||||
|
ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Lista de opciones obtenida correctamente',
|
||||||
|
schema: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id_opcion: { type: 'number', example: 1 },
|
||||||
|
opcion: { type: 'string', example: 'Si' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
ApiResponse({ status: 500, description: 'Error interno del servidor' })
|
||||||
|
);
|
||||||
|
|
||||||
|
// Documentación para obtener una opción por ID
|
||||||
|
static ApiGetOne = applyDecorators(
|
||||||
|
ApiOperation({
|
||||||
|
summary: 'Obtener una opción por ID',
|
||||||
|
description: 'Retorna una opción específica por su ID'
|
||||||
|
}),
|
||||||
|
ApiParam({
|
||||||
|
name: 'id',
|
||||||
|
description: 'ID de la opción',
|
||||||
|
required: true,
|
||||||
|
type: 'number',
|
||||||
|
example: 1
|
||||||
|
}),
|
||||||
|
ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Opción obtenida correctamente',
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id_opcion: { type: 'number', example: 1 },
|
||||||
|
opcion: { type: 'string', example: 'Si' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
ApiResponse({ status: 404, description: 'Opción no encontrada' }),
|
||||||
|
ApiResponse({ status: 500, description: 'Error interno del servidor' })
|
||||||
|
);
|
||||||
|
|
||||||
|
// Documentación para actualizar una opción
|
||||||
|
static ApiUpdate = applyDecorators(
|
||||||
|
ApiOperation({
|
||||||
|
summary: 'Actualizar una opción',
|
||||||
|
description: 'Actualiza los datos de una opción existente'
|
||||||
|
}),
|
||||||
|
ApiParam({
|
||||||
|
name: 'id',
|
||||||
|
description: 'ID de la opción a actualizar',
|
||||||
|
required: true,
|
||||||
|
type: 'number',
|
||||||
|
example: 1
|
||||||
|
}),
|
||||||
|
ApiBody({
|
||||||
|
description: 'Datos a actualizar de la opción',
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
opcion: { type: 'string', example: 'Opción actualizada' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Opción actualizada correctamente',
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
affected: { type: 'number', example: 1 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
ApiResponse({ status: 400, description: 'Datos de actualización inválidos' }),
|
||||||
|
ApiResponse({ status: 404, description: 'Opción no encontrada' }),
|
||||||
|
ApiResponse({ status: 500, description: 'Error interno del servidor' })
|
||||||
|
);
|
||||||
|
|
||||||
|
// Documentación para eliminar una opción
|
||||||
|
static ApiRemove = applyDecorators(
|
||||||
|
ApiOperation({
|
||||||
|
summary: 'Eliminar una opción',
|
||||||
|
description: 'Elimina permanentemente una opción por su ID'
|
||||||
|
}),
|
||||||
|
ApiParam({
|
||||||
|
name: 'id',
|
||||||
|
description: 'ID de la opción a eliminar',
|
||||||
|
required: true,
|
||||||
|
type: 'number',
|
||||||
|
example: 1
|
||||||
|
}),
|
||||||
|
ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Opción eliminada correctamente',
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
affected: { type: 'number', example: 1 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
ApiResponse({ status: 404, description: 'Opción no encontrada' }),
|
||||||
|
ApiResponse({ status: 500, description: 'Error interno del servidor' })
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,9 +1,13 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { OpcionService } from './opcion.service';
|
import { OpcionService } from './opcion.service';
|
||||||
import { OpcionController } from './opcion.controller';
|
import { OpcionController } from './opcion.controller';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { Opcion } from './entities/opcion.entity';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
|
imports: [TypeOrmModule.forFeature([Opcion])], // Importa el repositorio de Opcion
|
||||||
controllers: [OpcionController],
|
controllers: [OpcionController],
|
||||||
providers: [OpcionService],
|
providers: [OpcionService],
|
||||||
|
exports: [OpcionService, TypeOrmModule]
|
||||||
})
|
})
|
||||||
export class OpcionModule {}
|
export class OpcionModule {}
|
||||||
|
|||||||
@@ -12,12 +12,16 @@ export class OpcionService {
|
|||||||
private repository: Repository<Opcion>,
|
private repository: Repository<Opcion>,
|
||||||
){}
|
){}
|
||||||
|
|
||||||
|
create(createOpcionDto: CreateOpcionDto) {
|
||||||
|
return this.repository.save(createOpcionDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
create():Promise<Opcion> {
|
create():Promise<Opcion> {
|
||||||
return this.repository.save(this.repository.create());
|
return this.repository.save(this.repository.create());
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
findAll() {
|
findAll() {
|
||||||
return `This action returns all opcion`;
|
return `This action returns all opcion`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,22 @@
|
|||||||
import { IsEmail } from "class-validator";
|
import { IsEmail, IsNotEmpty, IsNumber } from "class-validator";
|
||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
|
||||||
export class CreateParticipanteDto {
|
export class CreateParticipanteDto {
|
||||||
@IsEmail()
|
@ApiProperty({
|
||||||
correo: string
|
description: 'Correo electrónico del participante',
|
||||||
id_tipo_user: number
|
example: 'usuario@ejemplo.com',
|
||||||
|
required: true
|
||||||
|
})
|
||||||
|
@IsEmail({}, { message: 'El correo electrónico debe tener un formato válido' })
|
||||||
|
@IsNotEmpty({ message: 'El correo electrónico es requerido' })
|
||||||
|
correo: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'ID del tipo de usuario',
|
||||||
|
example: 1,
|
||||||
|
required: true
|
||||||
|
})
|
||||||
|
@IsNumber({}, { message: 'El ID del tipo de usuario debe ser un número' })
|
||||||
|
@IsNotEmpty({ message: 'El ID del tipo de usuario es requerido' })
|
||||||
|
id_tipo_user: number;
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,22 @@
|
|||||||
|
import { IsEmail, IsOptional, IsNumber } from "class-validator";
|
||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
|
||||||
export class UpdateParticipanteDto {
|
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;
|
||||||
}
|
}
|
||||||
@@ -3,58 +3,40 @@ import { Participante } from './participante.entity';
|
|||||||
import { ParticipanteService } from './participante.service';
|
import { ParticipanteService } from './participante.service';
|
||||||
import { CreateParticipanteDto } from './dto/create-participante.dto';
|
import { CreateParticipanteDto } from './dto/create-participante.dto';
|
||||||
import { UpdateParticipanteDto } from './dto/update.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')
|
@Controller('participante')
|
||||||
export class ParticipanteController {
|
export class ParticipanteController {
|
||||||
constructor(private participanteService: ParticipanteService) {}
|
constructor(private participanteService: ParticipanteService) {}
|
||||||
|
|
||||||
@ApiOperation({ summary: 'Obtener todos los participantes' })
|
@ParticipanteApiDocumentation.ApiGetAll
|
||||||
@ApiResponse({ status: 200, description: 'Lista de participantes obtenida correctamente.' })
|
|
||||||
@Get()
|
@Get()
|
||||||
getParticipantes(): Promise<Participante[]> {
|
getParticipantes(): Promise<Participante[]> {
|
||||||
return this.participanteService.getParticipantes()
|
return this.participanteService.getParticipantes();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ParticipanteApiDocumentation.ApiGetOne
|
||||||
@Get(':id')
|
@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) {
|
getParticipante(@Param('id', ParseIntPipe) id: number) {
|
||||||
return this.participanteService.getParticipante(id);
|
return this.participanteService.getParticipante(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ParticipanteApiDocumentation.ApiCreate
|
||||||
@Post()
|
@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) {
|
createParticipante(@Body() newParticipante: CreateParticipanteDto) {
|
||||||
return this.participanteService.createParticipante(newParticipante);
|
return this.participanteService.createParticipante(newParticipante);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ParticipanteApiDocumentation.ApiRemove
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
deleteParticipante(@Param('id', ParseIntPipe) id: number) {
|
deleteParticipante(@Param('id', ParseIntPipe) id: number) {
|
||||||
return this.participanteService.deleteParticipante(id)
|
return this.participanteService.deleteParticipante(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ParticipanteApiDocumentation.ApiUpdate
|
||||||
@Patch(':id')
|
@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);
|
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,6 +1,8 @@
|
|||||||
|
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 { ParticipanteEvento } from "src/participante_evento/participante_evento.entity";
|
||||||
import { TipoUser } from "src/tipo_user/tipo_user.entity";
|
import { TipoUser } from "src/tipo_user/tipo_user.entity";
|
||||||
import { Column, Entity, JoinColumn, ManyToOne, OneToMany, PrimaryGeneratedColumn } from "typeorm";
|
import { Column, Entity, JoinColumn, JoinTable, ManyToMany, ManyToOne, OneToMany, PrimaryGeneratedColumn } from "typeorm";
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class Participante {
|
export class Participante {
|
||||||
@@ -16,19 +18,32 @@ export class Participante {
|
|||||||
//Relacion con tipo usuario
|
//Relacion con tipo usuario
|
||||||
@ManyToOne(() => TipoUser, tipoUser => tipoUser.participante)
|
@ManyToOne(() => TipoUser, tipoUser => tipoUser.participante)
|
||||||
@JoinColumn({ name: 'participante_id' }) //nombre de la relacion
|
@JoinColumn({ name: 'participante_id' }) //nombre de la relacion
|
||||||
tipo_user: TipoUser[]
|
tipo_user: TipoUser
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ManyToOne(() => Administrador, (admin) => admin.eventos)
|
@ManyToOne(() => Administrador, (admin) => admin.eventos)
|
||||||
@JoinColumn({ name: "id_administrador" })
|
@JoinColumn({ name: "id_administrador" })
|
||||||
administrador: Administrador;
|
administrador: Administrador;
|
||||||
|
|
||||||
@OneToMany(() => ParticipanteEvento, (pe) => pe.evento)
|
@ManyToMany(() => Evento, evento => evento.participantes)
|
||||||
participantes: ParticipanteEvento[];
|
@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)
|
@OneToMany(() => Asistencia, (asistencia) => asistencia.evento)
|
||||||
asistencias: Asistencia[];
|
asistencias: Asistencia[];
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@OneToMany(() => CuestionarioRespondido, cuestionario => cuestionario.participante)
|
||||||
|
cuestionariosRespondidos: CuestionarioRespondido[];
|
||||||
|
|
||||||
|
@OneToMany(() => ParticipanteEvento, participanteEvento => participanteEvento.participante)
|
||||||
|
participanteEventos: ParticipanteEvento[];
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -8,6 +8,6 @@ import { Participante } from './participante.entity';
|
|||||||
imports: [TypeOrmModule.forFeature([Participante])],
|
imports: [TypeOrmModule.forFeature([Participante])],
|
||||||
controllers: [ParticipanteController],
|
controllers: [ParticipanteController],
|
||||||
providers: [ParticipanteService],
|
providers: [ParticipanteService],
|
||||||
exports: [ParticipanteService],
|
exports: [ParticipanteService, TypeOrmModule],
|
||||||
})
|
})
|
||||||
export class ParticipanteModule {}
|
export class ParticipanteModule {}
|
||||||
|
|||||||
@@ -1,62 +1,99 @@
|
|||||||
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
import { BadRequestException, ConflictException, Injectable, NotFoundException } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Participante } from './participante.entity';
|
import { Participante } from './participante.entity';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { CreateParticipanteDto } from './dto/create-participante.dto';
|
import { CreateParticipanteDto } from './dto/create-participante.dto';
|
||||||
import { UpdateAdminDto } from 'src/admin/dto/update.admin.dto';
|
|
||||||
import { UpdateParticipanteDto } from './dto/update.participante.dto';
|
import { UpdateParticipanteDto } from './dto/update.participante.dto';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ParticipanteService {
|
export class ParticipanteService {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(Participante) private participanteRepository: Repository<Participante>
|
@InjectRepository(Participante) private participanteRepository: Repository<Participante>
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async createParticipante(participante: CreateParticipanteDto) {
|
/**
|
||||||
|
* 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({
|
const participanteFound = await this.participanteRepository.findOne({
|
||||||
where: {
|
where: {
|
||||||
correo: participante.correo
|
correo: participante.correo
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
if (participanteFound)
|
if (participanteFound) {
|
||||||
return new HttpException('Participante already exists', HttpStatus.CONFLICT)
|
throw new ConflictException(`Ya existe un participante con el correo ${participante.correo}`);
|
||||||
|
}
|
||||||
|
|
||||||
return this.participanteRepository.save(participante)
|
const nuevoParticipante = this.participanteRepository.create(participante);
|
||||||
|
return this.participanteRepository.save(nuevoParticipante);
|
||||||
}
|
}
|
||||||
|
|
||||||
getParticipantes() {
|
/**
|
||||||
|
* Obtiene todos los participantes
|
||||||
|
* @returns Lista de participantes con sus relaciones
|
||||||
|
*/
|
||||||
|
async getParticipantes(): Promise<Participante[]> {
|
||||||
return this.participanteRepository.find({
|
return this.participanteRepository.find({
|
||||||
relations: ['tipo_user']
|
relations: ['tipo_user', 'participanteEventos']
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getParticipante(id_participante: number) {
|
/**
|
||||||
|
* 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({
|
const participanteFound = await this.participanteRepository.findOne({
|
||||||
where: {
|
where: {
|
||||||
id_participante
|
id_participante
|
||||||
},
|
},
|
||||||
relations: ['tipo_user']
|
relations: ['tipo_user', 'participanteEventos']
|
||||||
})
|
});
|
||||||
|
|
||||||
if (!participanteFound)
|
if (!participanteFound) {
|
||||||
return new HttpException('Participante not found', HttpStatus.NOT_FOUND);
|
throw new NotFoundException(`Participante con ID ${id_participante} no encontrado`);
|
||||||
|
}
|
||||||
|
|
||||||
return participanteFound;
|
return participanteFound;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Elimina un participante por su ID
|
||||||
|
* @param id_participante ID del participante a eliminar
|
||||||
|
* @returns Resultado de la eliminación
|
||||||
|
* @throws NotFoundException si no se encuentra el participante
|
||||||
|
*/
|
||||||
async deleteParticipante(id_participante: number) {
|
async deleteParticipante(id_participante: number) {
|
||||||
const result = await this.participanteRepository.delete({ id_participante })
|
const result = await this.participanteRepository.delete({ id_participante });
|
||||||
|
|
||||||
if (result.affected === 0) {
|
if (result.affected === 0) {
|
||||||
return new HttpException('Participante not found', HttpStatus.NOT_FOUND);
|
throw new NotFoundException(`Participante con ID ${id_participante} no encontrado`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return {
|
||||||
|
success: true,
|
||||||
|
message: `Participante con ID ${id_participante} eliminado correctamente`,
|
||||||
|
affected: result.affected
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateParticipante(id_participante: number, participante: UpdateParticipanteDto) {
|
/**
|
||||||
|
* 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({
|
const participanteFound = await this.participanteRepository.findOne({
|
||||||
where: {
|
where: {
|
||||||
id_participante
|
id_participante
|
||||||
@@ -64,11 +101,24 @@ export class ParticipanteService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!participanteFound) {
|
if (!participanteFound) {
|
||||||
return new HttpException('Participante not found', HttpStatus.NOT_FOUND)
|
throw new NotFoundException(`Participante con ID ${id_participante} no encontrado`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateParticipante = Object.assign(participanteFound, participante)
|
// Si se está actualizando el correo, verificar que no exista otro participante con ese correo
|
||||||
return this.participanteRepository.save(updateParticipante)
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,32 +3,182 @@ import { ParticipanteEventoService } from './participante_evento.service';
|
|||||||
import { ParticipanteEvento } from './participante_evento.entity';
|
import { ParticipanteEvento } from './participante_evento.entity';
|
||||||
import { CreateParticipanteEventoDto } from './dto/create-participante_evento.dto';
|
import { CreateParticipanteEventoDto } from './dto/create-participante_evento.dto';
|
||||||
import { UpdateParticipanteEventoDto } from './dto/update.participante_evento.dto';
|
import { UpdateParticipanteEventoDto } from './dto/update.participante_evento.dto';
|
||||||
|
import { ApiTags, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
@ApiTags('Participantes-Eventos')
|
||||||
@Controller('participante-evento')
|
@Controller('participante-evento')
|
||||||
export class ParticipanteEventoController {
|
export class ParticipanteEventoController {
|
||||||
|
|
||||||
constructor(private participanteEventoService: ParticipanteEventoService) {}
|
constructor(private participanteEventoService: ParticipanteEventoService) {}
|
||||||
|
|
||||||
|
@ApiOperation({ summary: 'Obtener participantes por evento' })
|
||||||
|
@ApiParam({ name: 'idEvento', description: 'ID del evento', type: 'number' })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Lista de participantes que están registrados en el evento',
|
||||||
|
schema: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id_participante_evento: { type: 'number' },
|
||||||
|
id_participante: { type: 'number' },
|
||||||
|
id_evento: { type: 'number' },
|
||||||
|
fecha_inscripcion: { type: 'string', format: 'date-time' },
|
||||||
|
estatus: { type: 'boolean' },
|
||||||
|
fecha_asistencia: { type: 'string', format: 'date-time', nullable: true },
|
||||||
|
participante: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id_participante: { type: 'number' },
|
||||||
|
correo: { type: 'string' },
|
||||||
|
id_tipo_user: { type: 'number' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 404, description: 'Evento no encontrado' })
|
||||||
|
@Get('evento/:idEvento')
|
||||||
|
getParticipantesPorEvento(@Param('idEvento', ParseIntPipe) idEvento: number) {
|
||||||
|
return this.participanteEventoService.getParticipantesPorEvento(idEvento);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({ summary: 'Obtener eventos por participante' })
|
||||||
|
@ApiParam({ name: 'idParticipante', description: 'ID del participante', type: 'number' })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Lista de eventos en los que está registrado el participante',
|
||||||
|
schema: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id_participante_evento: { type: 'number' },
|
||||||
|
id_participante: { type: 'number' },
|
||||||
|
id_evento: { type: 'number' },
|
||||||
|
fecha_inscripcion: { type: 'string', format: 'date-time' },
|
||||||
|
estatus: { type: 'boolean' },
|
||||||
|
fecha_asistencia: { type: 'string', format: 'date-time', nullable: true },
|
||||||
|
evento: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id_evento: { type: 'number' },
|
||||||
|
nombre_evento: { type: 'string' },
|
||||||
|
tipo_evento: { type: 'string' },
|
||||||
|
fecha_inicio: { type: 'string', format: 'date-time' },
|
||||||
|
fecha_fin: { type: 'string', format: 'date-time' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 404, description: 'Participante no encontrado' })
|
||||||
|
@Get('participante/:idParticipante')
|
||||||
|
getEventosPorParticipante(@Param('idParticipante', ParseIntPipe) idParticipante: number) {
|
||||||
|
return this.participanteEventoService.getEventosPorParticipante(idParticipante);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({ summary: 'Obtener un registro específico por IDs de participante y evento' })
|
||||||
|
@ApiParam({ name: 'idParticipante', description: 'ID del participante', type: 'number' })
|
||||||
|
@ApiParam({ name: 'idEvento', description: 'ID del evento', type: 'number' })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Registro participante-evento obtenido correctamente'
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 404, description: 'Registro no encontrado' })
|
||||||
|
@Get(':idParticipante/:idEvento')
|
||||||
|
getParticipanteEventoEspecifico(
|
||||||
|
@Param('idParticipante', ParseIntPipe) idParticipante: number,
|
||||||
|
@Param('idEvento', ParseIntPipe) idEvento: number
|
||||||
|
) {
|
||||||
|
return this.participanteEventoService.getParticipanteEventoEspecifico(idParticipante, idEvento)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({ summary: 'Obtener un registro participante-evento por ID' })
|
||||||
|
@ApiParam({ name: 'id', description: 'ID del participante', type: 'number' })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Registro participante-evento obtenido correctamente'
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 404, description: 'Registro no encontrado' })
|
||||||
|
@Get(':id')
|
||||||
|
getParticipanteEvento(@Param('id', ParseIntPipe) id: number) {
|
||||||
|
return this.participanteEventoService.getParticipanteEvento(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({ summary: 'Obtener todos los registros de participante-evento' })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Lista de todos los registros participante-evento con sus relaciones',
|
||||||
|
schema: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id_participante_evento: { type: 'number' },
|
||||||
|
id_participante: { type: 'number' },
|
||||||
|
id_evento: { type: 'number' },
|
||||||
|
fecha_inscripcion: { type: 'string', format: 'date-time' },
|
||||||
|
estatus: { type: 'boolean' },
|
||||||
|
fecha_asistencia: { type: 'string', format: 'date-time', nullable: true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@Get()
|
@Get()
|
||||||
getParticipantesEvento(): Promise<ParticipanteEvento[]> {
|
getParticipantesEvento(): Promise<ParticipanteEvento[]> {
|
||||||
return this.participanteEventoService.getParticipantesEvento()
|
return this.participanteEventoService.getParticipantesEvento()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@ApiOperation({ summary: 'Registrar un participante en un evento' })
|
||||||
getParticipanteEvento(@Param('id', ParseIntPipe) id: number) {
|
@ApiResponse({
|
||||||
return this.participanteEventoService.getParticipanteEvento(id)
|
status: 201,
|
||||||
}
|
description: 'Participante registrado en el evento correctamente'
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 409, description: 'El participante ya está registrado en este evento' })
|
||||||
@Post()
|
@Post()
|
||||||
createParticipanteEvento(@Body() newParticipanteEvento: CreateParticipanteEventoDto) {
|
createParticipanteEvento(@Body() newParticipanteEvento: CreateParticipanteEventoDto) {
|
||||||
return this.participanteEventoService.createParticipanteEvento(newParticipanteEvento)
|
return this.participanteEventoService.createParticipanteEvento(newParticipanteEvento)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ApiOperation({ summary: 'Registrar asistencia de un participante a un evento' })
|
||||||
|
@ApiParam({ name: 'idParticipante', description: 'ID del participante', type: 'number' })
|
||||||
|
@ApiParam({ name: 'idEvento', description: 'ID del evento', type: 'number' })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Asistencia registrada correctamente'
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 404, description: 'Registro no encontrado' })
|
||||||
|
@Post('asistencia/:idParticipante/:idEvento')
|
||||||
|
registrarAsistencia(
|
||||||
|
@Param('idParticipante', ParseIntPipe) idParticipante: number,
|
||||||
|
@Param('idEvento', ParseIntPipe) idEvento: number
|
||||||
|
) {
|
||||||
|
return this.participanteEventoService.registrarAsistencia(idParticipante, idEvento)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({ summary: 'Eliminar un registro participante-evento' })
|
||||||
|
@ApiParam({ name: 'id', description: 'ID del participante', type: 'number' })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Registro eliminado correctamente'
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 404, description: 'Registro no encontrado' })
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
deleteParticipanteEvento(@Param('id', ParseIntPipe) id: number) {
|
deleteParticipanteEvento(@Param('id', ParseIntPipe) id: number) {
|
||||||
return this.participanteEventoService.deleteParticipanteEvento(id)
|
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')
|
@Patch(':id')
|
||||||
updateParticipanteEvento(@Param('id', ParseIntPipe) id_participante: number, id_evento: number, @Body() participanteEvento: UpdateParticipanteEventoDto) {
|
updateParticipanteEvento(@Param('id', ParseIntPipe) id_participante: number, id_evento: number, @Body() participanteEvento: UpdateParticipanteEventoDto) {
|
||||||
return this.participanteEventoService.updateParticipanteEvento(id_participante, id_evento, participanteEvento)
|
return this.participanteEventoService.updateParticipanteEvento(id_participante, id_evento, participanteEvento)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Evento } from "src/evento/evento.entity";
|
import { Evento } from "src/evento/evento.entity";
|
||||||
|
import { Participante } from "src/participante/participante.entity";
|
||||||
import { Column, Entity, JoinColumn, ManyToOne, PrimaryColumn, PrimaryGeneratedColumn } from "typeorm";
|
import { Column, Entity, JoinColumn, ManyToOne, PrimaryColumn, PrimaryGeneratedColumn } from "typeorm";
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
@@ -14,20 +15,27 @@ export class ParticipanteEvento {
|
|||||||
@Column()
|
@Column()
|
||||||
estatus: boolean
|
estatus: boolean
|
||||||
|
|
||||||
|
@Column({ nullable: true, type: 'datetime' })
|
||||||
|
fecha_asistencia: Date
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@OneToOne(() => Qr, (qr) => qr.participanteEvento)
|
@OneToOne(() => Qr, (qr) => qr.participanteEvento)
|
||||||
qr: Qr;
|
qr: Qr;
|
||||||
|
|
||||||
@ManyToOne(() => Participante, (participante) => participante.eventos)
|
@OneToMany(() => ParticipanteEvento, (pe) => pe.evento)
|
||||||
@JoinColumn({ name: "id_participante" })
|
participantes: ParticipanteEvento[];
|
||||||
participante: Participante;
|
|
||||||
|
|
||||||
@ManyToOne(() => Evento, (evento) => evento.participantes)
|
@ManyToOne(() => Evento, (evento) => evento.participantes)
|
||||||
@JoinColumn({ name: "id_evento" })
|
@JoinColumn({ name: "id_evento" })
|
||||||
evento: Evento;
|
evento: Evento;
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ManyToOne(() => Evento, (evento) => evento.participantes)
|
@ManyToOne(() => Participante, participante => participante.participanteEventos)
|
||||||
|
@JoinColumn({ name: "id_participante" })
|
||||||
|
participante: Participante;
|
||||||
|
|
||||||
|
@ManyToOne(() => Evento, evento => evento.participanteEventos)
|
||||||
@JoinColumn({ name: "id_evento" })
|
@JoinColumn({ name: "id_evento" })
|
||||||
evento: Evento;
|
evento: Evento;
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -4,10 +4,12 @@ import { ParticipanteEventoController } from './participante_evento.controller';
|
|||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { ParticipanteEvento } from './participante_evento.entity';
|
import { ParticipanteEvento } from './participante_evento.entity';
|
||||||
import { Evento } from 'src/evento/evento.entity';
|
import { Evento } from 'src/evento/evento.entity';
|
||||||
|
import { Participante } from 'src/participante/participante.entity';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([ParticipanteEvento]), Evento],
|
imports: [TypeOrmModule.forFeature([ParticipanteEvento, Evento, Participante])],
|
||||||
controllers: [ParticipanteEventoController],
|
controllers: [ParticipanteEventoController],
|
||||||
providers: [ParticipanteEventoService]
|
providers: [ParticipanteEventoService],
|
||||||
|
exports: [ParticipanteEventoService],
|
||||||
})
|
})
|
||||||
export class ParticipanteEventoModule {}
|
export class ParticipanteEventoModule {}
|
||||||
|
|||||||
@@ -4,12 +4,16 @@ import { ParticipanteEvento } from './participante_evento.entity';
|
|||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { CreateParticipanteEventoDto } from './dto/create-participante_evento.dto';
|
import { CreateParticipanteEventoDto } from './dto/create-participante_evento.dto';
|
||||||
import { UpdateParticipanteEventoDto } from './dto/update.participante_evento.dto';
|
import { UpdateParticipanteEventoDto } from './dto/update.participante_evento.dto';
|
||||||
|
import { Evento } from 'src/evento/evento.entity';
|
||||||
|
import { Participante } from 'src/participante/participante.entity';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ParticipanteEventoService {
|
export class ParticipanteEventoService {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(ParticipanteEvento) private participanteEventoRepository: Repository<ParticipanteEvento>
|
@InjectRepository(ParticipanteEvento) private participanteEventoRepository: Repository<ParticipanteEvento>,
|
||||||
|
@InjectRepository(Evento) private eventoRepository: Repository<Evento>,
|
||||||
|
@InjectRepository(Participante) private participanteRepository: Repository<Participante>
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async createParticipanteEvento(participanteEvento: CreateParticipanteEventoDto) {
|
async createParticipanteEvento(participanteEvento: CreateParticipanteEventoDto) {
|
||||||
@@ -30,7 +34,7 @@ export class ParticipanteEventoService {
|
|||||||
|
|
||||||
getParticipantesEvento() {
|
getParticipantesEvento() {
|
||||||
return this.participanteEventoRepository.find({
|
return this.participanteEventoRepository.find({
|
||||||
relations: ['evento']
|
relations: ['evento', 'participante']
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,7 +43,7 @@ export class ParticipanteEventoService {
|
|||||||
where: {
|
where: {
|
||||||
id_participante
|
id_participante
|
||||||
},
|
},
|
||||||
relations: ['evento']
|
relations: ['evento', 'participante']
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!participante_eventoFound) {
|
if (!participante_eventoFound) {
|
||||||
@@ -49,6 +53,70 @@ export class ParticipanteEventoService {
|
|||||||
return participante_eventoFound
|
return participante_eventoFound
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getParticipanteEventoEspecifico(id_participante: number, id_evento: number) {
|
||||||
|
const participante_eventoFound = await this.participanteEventoRepository.findOne({
|
||||||
|
where: {
|
||||||
|
id_participante,
|
||||||
|
id_evento
|
||||||
|
},
|
||||||
|
relations: ['evento', 'participante']
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!participante_eventoFound) {
|
||||||
|
return new HttpException('Registro no encontrado', HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
return participante_eventoFound
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtiene todos los participantes de un evento específico
|
||||||
|
* @param id_evento ID del evento
|
||||||
|
* @returns Lista de registros participante-evento con información del participante
|
||||||
|
*/
|
||||||
|
async getParticipantesPorEvento(id_evento: number) {
|
||||||
|
// Verificar si el evento existe
|
||||||
|
const eventoExists = await this.eventoRepository.findOne({
|
||||||
|
where: { id_evento }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!eventoExists) {
|
||||||
|
throw new HttpException(`Evento con ID ${id_evento} no encontrado`, HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtener todos los registros participante-evento para este evento
|
||||||
|
const participantesEvento = await this.participanteEventoRepository.find({
|
||||||
|
where: { id_evento },
|
||||||
|
relations: ['participante']
|
||||||
|
});
|
||||||
|
|
||||||
|
return participantesEvento;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtiene todos los eventos de un participante específico
|
||||||
|
* @param id_participante ID del participante
|
||||||
|
* @returns Lista de registros participante-evento con información del evento
|
||||||
|
*/
|
||||||
|
async getEventosPorParticipante(id_participante: number) {
|
||||||
|
// Verificar si el participante existe
|
||||||
|
const participanteExists = await this.participanteRepository.findOne({
|
||||||
|
where: { id_participante }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!participanteExists) {
|
||||||
|
throw new HttpException(`Participante con ID ${id_participante} no encontrado`, HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtener todos los registros participante-evento para este participante
|
||||||
|
const eventosParticipante = await this.participanteEventoRepository.find({
|
||||||
|
where: { id_participante },
|
||||||
|
relations: ['evento']
|
||||||
|
});
|
||||||
|
|
||||||
|
return eventosParticipante;
|
||||||
|
}
|
||||||
|
|
||||||
async deleteParticipanteEvento(id_participante: number) {
|
async deleteParticipanteEvento(id_participante: number) {
|
||||||
const result = await this.participanteEventoRepository.delete({ id_participante })
|
const result = await this.participanteEventoRepository.delete({ id_participante })
|
||||||
|
|
||||||
@@ -74,4 +142,19 @@ export class ParticipanteEventoService {
|
|||||||
return this.participanteEventoRepository.save(participante_evento)
|
return this.participanteEventoRepository.save(participante_evento)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async registrarAsistencia(id_participante: number, id_evento: number) {
|
||||||
|
const participante_eventoFound = await this.participanteEventoRepository.findOne({
|
||||||
|
where: {
|
||||||
|
id_participante,
|
||||||
|
id_evento
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!participante_eventoFound) {
|
||||||
|
return new HttpException('Registro no encontrado', HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
participante_eventoFound.fecha_asistencia = new Date();
|
||||||
|
return this.participanteEventoRepository.save(participante_eventoFound);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1,115 @@
|
|||||||
export class CreatePreguntaDto {}
|
import { Type } from 'class-transformer';
|
||||||
|
import { IsArray, IsBoolean, IsEnum, IsNotEmpty, IsNumber, IsOptional, IsString, ValidateNested } from 'class-validator';
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export enum TipoPreguntaEnum {
|
||||||
|
CERRADA = 'Cerrada',
|
||||||
|
ABIERTA = 'Abierta',
|
||||||
|
MULTIPLE = 'Multiple'
|
||||||
|
}
|
||||||
|
|
||||||
|
export class OpcionDto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Valor de la opción',
|
||||||
|
example: 'Sí'
|
||||||
|
})
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
valor: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Límite de caracteres para respuestas de texto (solo aplica a preguntas tipo Abierta)',
|
||||||
|
example: 250,
|
||||||
|
required: false
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
limite?: number;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Tipo de pregunta',
|
||||||
|
enum: Object.values(TipoPreguntaEnum),
|
||||||
|
enumName: 'TipoPreguntaEnum',
|
||||||
|
example: TipoPreguntaEnum.CERRADA,
|
||||||
|
examples: {
|
||||||
|
'cerrada': {
|
||||||
|
summary: 'Pregunta de opción única',
|
||||||
|
value: TipoPreguntaEnum.CERRADA
|
||||||
|
},
|
||||||
|
'abierta': {
|
||||||
|
summary: 'Pregunta de texto libre',
|
||||||
|
value: TipoPreguntaEnum.ABIERTA
|
||||||
|
},
|
||||||
|
'multiple': {
|
||||||
|
summary: 'Pregunta de selección múltiple',
|
||||||
|
value: TipoPreguntaEnum.MULTIPLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsEnum(TipoPreguntaEnum, {
|
||||||
|
message: 'El tipo debe ser uno de los siguientes valores: Cerrada, Abierta, Multiple'
|
||||||
|
})
|
||||||
|
tipo: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Contador de opciones (se calcula automáticamente)',
|
||||||
|
required: false
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
contador_opcion?: number;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'ID de la opción de la que depende esta pregunta (para preguntas condicionadas)',
|
||||||
|
required: false,
|
||||||
|
example: null
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
id_opcion_dependiente?: number;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Tipo de validación para la respuesta',
|
||||||
|
required: false,
|
||||||
|
examples: ['cuenta_alumno', 'correo', 'telefono', 'nombre', 'entero', 'decimal'],
|
||||||
|
example: 'correo'
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
validacion?: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Lista de opciones para preguntas de tipo Cerrada o Multiple',
|
||||||
|
type: [OpcionDto],
|
||||||
|
required: false,
|
||||||
|
examples: [
|
||||||
|
{ valor: 'Sí' },
|
||||||
|
{ valor: 'No' }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => OpcionDto)
|
||||||
|
opciones?: OpcionDto[];
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,14 +20,20 @@ export class Pregunta {
|
|||||||
|
|
||||||
@ManyToOne(() => TipoPregunta, tipo => tipo.preguntas)
|
@ManyToOne(() => TipoPregunta, tipo => tipo.preguntas)
|
||||||
@JoinColumn({ name: 'id_tipo_pregunta' })
|
@JoinColumn({ name: 'id_tipo_pregunta' })
|
||||||
id_tipo_pregunta: TipoPregunta;
|
tipoPregunta: TipoPregunta;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
id_tipo_pregunta: number;
|
||||||
|
|
||||||
|
@Column({ type: 'int', nullable: true })
|
||||||
|
id_opcion_dependiente?: number;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column({ nullable: true })
|
||||||
id_opcion_dependiente: number;
|
validacion?: string;
|
||||||
|
|
||||||
@OneToMany(() => SeccionPregunta, seccionPregunta => seccionPregunta.id_pregunta)
|
@OneToMany(() => SeccionPregunta, seccionPregunta => seccionPregunta.pregunta)
|
||||||
seccionPreguntas: SeccionPregunta[];
|
seccionPreguntas: SeccionPregunta[];
|
||||||
|
|
||||||
@OneToMany(() => PreguntaOpcion, preguntaOpcion => preguntaOpcion.id_pregunta)
|
@OneToMany(() => PreguntaOpcion, preguntaOpcion => preguntaOpcion.pregunta)
|
||||||
opciones: PreguntaOpcion[];
|
opciones: PreguntaOpcion[];
|
||||||
}
|
}
|
||||||
@@ -2,32 +2,39 @@ import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/commo
|
|||||||
import { PreguntaService } from './pregunta.service';
|
import { PreguntaService } from './pregunta.service';
|
||||||
import { CreatePreguntaDto } from './dto/create-pregunta.dto';
|
import { CreatePreguntaDto } from './dto/create-pregunta.dto';
|
||||||
import { UpdatePreguntaDto } from './dto/update-pregunta.dto';
|
import { UpdatePreguntaDto } from './dto/update-pregunta.dto';
|
||||||
|
import { PreguntaApiDocumentation } from './pregunta.documentation';
|
||||||
|
|
||||||
@Controller('pregunta')
|
@Controller('pregunta')
|
||||||
|
@PreguntaApiDocumentation.ApiController
|
||||||
export class PreguntaController {
|
export class PreguntaController {
|
||||||
constructor(private readonly preguntaService: PreguntaService) {}
|
constructor(private readonly preguntaService: PreguntaService) {}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
|
@PreguntaApiDocumentation.ApiCreate
|
||||||
create(@Body() createPreguntaDto: CreatePreguntaDto) {
|
create(@Body() createPreguntaDto: CreatePreguntaDto) {
|
||||||
return this.preguntaService.create(createPreguntaDto);
|
return this.preguntaService.create(createPreguntaDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
|
@PreguntaApiDocumentation.ApiGetAll
|
||||||
findAll() {
|
findAll() {
|
||||||
return this.preguntaService.findAll();
|
return this.preguntaService.findAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
|
@PreguntaApiDocumentation.ApiGetOne
|
||||||
findOne(@Param('id') id: string) {
|
findOne(@Param('id') id: string) {
|
||||||
return this.preguntaService.findOne(+id);
|
return this.preguntaService.findOne(+id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Patch(':id')
|
@Patch(':id')
|
||||||
|
@PreguntaApiDocumentation.ApiUpdate
|
||||||
update(@Param('id') id: string, @Body() updatePreguntaDto: UpdatePreguntaDto) {
|
update(@Param('id') id: string, @Body() updatePreguntaDto: UpdatePreguntaDto) {
|
||||||
return this.preguntaService.update(+id, updatePreguntaDto);
|
return this.preguntaService.update(+id, updatePreguntaDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
|
@PreguntaApiDocumentation.ApiRemove
|
||||||
remove(@Param('id') id: string) {
|
remove(@Param('id') id: string) {
|
||||||
return this.preguntaService.remove(+id);
|
return this.preguntaService.remove(+id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,241 @@
|
|||||||
|
import { ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||||
|
import { applyDecorators } from '@nestjs/common';
|
||||||
|
import { TipoPreguntaEnum } from './dto/create-pregunta.dto';
|
||||||
|
|
||||||
|
export class PreguntaApiDocumentation {
|
||||||
|
// Decoradores para toda la clase del controlador
|
||||||
|
static ApiController = ApiTags('Preguntas');
|
||||||
|
|
||||||
|
// Documentación para crear una pregunta
|
||||||
|
static ApiCreate = applyDecorators(
|
||||||
|
ApiOperation({
|
||||||
|
summary: 'Crear una nueva pregunta',
|
||||||
|
description: 'Crea una nueva pregunta para una sección'
|
||||||
|
}),
|
||||||
|
ApiBody({
|
||||||
|
description: 'Datos de la pregunta a crear',
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
required: ['titulo', 'tipo'],
|
||||||
|
properties: {
|
||||||
|
titulo: { type: 'string', example: '¿Eres parte de la comunidad?' },
|
||||||
|
obligatoria: { type: 'boolean', example: true },
|
||||||
|
tipo: {
|
||||||
|
type: 'string',
|
||||||
|
enum: Object.values(TipoPreguntaEnum),
|
||||||
|
description: 'Tipo de pregunta: Cerrada (opción única), Abierta (texto libre), Multiple (varias opciones)',
|
||||||
|
example: TipoPreguntaEnum.CERRADA
|
||||||
|
},
|
||||||
|
contador_opcion: { type: 'number', example: 0 },
|
||||||
|
id_opcion_dependiente: { type: 'number', example: null },
|
||||||
|
validacion: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Tipo de validación para la respuesta (cuenta_alumno, correo, telefono, nombre, entero, decimal, etc)',
|
||||||
|
example: 'correo'
|
||||||
|
},
|
||||||
|
opciones: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
valor: { type: 'string', example: 'Si' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
examples: {
|
||||||
|
cerrada: {
|
||||||
|
summary: 'Pregunta de opción única',
|
||||||
|
value: {
|
||||||
|
titulo: '¿Eres parte de la comunidad de la FES Acatlán?',
|
||||||
|
obligatoria: true,
|
||||||
|
tipo: TipoPreguntaEnum.CERRADA,
|
||||||
|
validacion: null,
|
||||||
|
opciones: [
|
||||||
|
{ valor: 'Sí' },
|
||||||
|
{ valor: 'No' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
abierta: {
|
||||||
|
summary: 'Pregunta de texto libre',
|
||||||
|
value: {
|
||||||
|
titulo: '¿Qué mejorarías en nuestro servicio?',
|
||||||
|
obligatoria: false,
|
||||||
|
tipo: TipoPreguntaEnum.ABIERTA,
|
||||||
|
validacion: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
multiple: {
|
||||||
|
summary: 'Pregunta de selección múltiple',
|
||||||
|
value: {
|
||||||
|
titulo: '¿Qué aspectos del servicio fueron de tu agrado?',
|
||||||
|
obligatoria: false,
|
||||||
|
tipo: TipoPreguntaEnum.MULTIPLE,
|
||||||
|
validacion: null,
|
||||||
|
opciones: [
|
||||||
|
{ valor: 'Rapidez' },
|
||||||
|
{ valor: 'Atención al cliente' },
|
||||||
|
{ valor: 'Facilidad de uso' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
ApiResponse({
|
||||||
|
status: 201,
|
||||||
|
description: 'Pregunta creada correctamente',
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id_pregunta: { type: 'number', example: 1 },
|
||||||
|
pregunta: { type: 'string', example: '¿Eres parte de la comunidad?' },
|
||||||
|
obligatoria: { type: 'boolean', example: true },
|
||||||
|
contador_opcion: { type: 'number', example: 0 },
|
||||||
|
id_tipo_pregunta: { type: 'number', example: 1 },
|
||||||
|
id_opcion_dependiente: { type: 'number', example: null },
|
||||||
|
validacion: { type: 'string', example: 'correo' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
ApiResponse({ status: 400, description: 'Datos de la pregunta inválidos' }),
|
||||||
|
ApiResponse({ status: 500, description: 'Error interno del servidor' })
|
||||||
|
);
|
||||||
|
|
||||||
|
// Documentación para obtener todas las preguntas
|
||||||
|
static ApiGetAll = applyDecorators(
|
||||||
|
ApiOperation({
|
||||||
|
summary: 'Obtener todas las preguntas',
|
||||||
|
description: 'Retorna una lista de todas las preguntas registradas'
|
||||||
|
}),
|
||||||
|
ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Lista de preguntas obtenida correctamente',
|
||||||
|
schema: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id_pregunta: { type: 'number', example: 1 },
|
||||||
|
pregunta: { type: 'string', example: '¿Eres parte de la comunidad?' },
|
||||||
|
obligatoria: { type: 'boolean', example: true },
|
||||||
|
contador_opcion: { type: 'number', example: 0 },
|
||||||
|
id_tipo_pregunta: { type: 'number', example: 1 },
|
||||||
|
id_opcion_dependiente: { type: 'number', example: null },
|
||||||
|
validacion: { type: 'string', example: 'correo' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
ApiResponse({ status: 500, description: 'Error interno del servidor' })
|
||||||
|
);
|
||||||
|
|
||||||
|
// Documentación para obtener una pregunta por ID
|
||||||
|
static ApiGetOne = applyDecorators(
|
||||||
|
ApiOperation({
|
||||||
|
summary: 'Obtener una pregunta por ID',
|
||||||
|
description: 'Retorna una pregunta específica por su ID'
|
||||||
|
}),
|
||||||
|
ApiParam({
|
||||||
|
name: 'id',
|
||||||
|
description: 'ID de la pregunta',
|
||||||
|
required: true,
|
||||||
|
type: 'number',
|
||||||
|
example: 1
|
||||||
|
}),
|
||||||
|
ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Pregunta obtenida correctamente',
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id_pregunta: { type: 'number', example: 1 },
|
||||||
|
pregunta: { type: 'string', example: '¿Eres parte de la comunidad?' },
|
||||||
|
obligatoria: { type: 'boolean', example: true },
|
||||||
|
contador_opcion: { type: 'number', example: 0 },
|
||||||
|
id_tipo_pregunta: { type: 'number', example: 1 },
|
||||||
|
id_opcion_dependiente: { type: 'number', example: null },
|
||||||
|
validacion: { type: 'string', example: 'correo' },
|
||||||
|
opciones: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id_opcion: { type: 'number', example: 1 },
|
||||||
|
opcion: { type: 'string', example: 'Si' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
ApiResponse({ status: 404, description: 'Pregunta no encontrada' }),
|
||||||
|
ApiResponse({ status: 500, description: 'Error interno del servidor' })
|
||||||
|
);
|
||||||
|
|
||||||
|
// Documentación para actualizar una pregunta
|
||||||
|
static ApiUpdate = applyDecorators(
|
||||||
|
ApiOperation({
|
||||||
|
summary: 'Actualizar una pregunta',
|
||||||
|
description: 'Actualiza los datos de una pregunta existente'
|
||||||
|
}),
|
||||||
|
ApiParam({
|
||||||
|
name: 'id',
|
||||||
|
description: 'ID de la pregunta a actualizar',
|
||||||
|
required: true,
|
||||||
|
type: 'number',
|
||||||
|
example: 1
|
||||||
|
}),
|
||||||
|
ApiBody({
|
||||||
|
description: 'Datos a actualizar de la pregunta',
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
pregunta: { type: 'string', example: 'Pregunta actualizada' },
|
||||||
|
obligatoria: { type: 'boolean', example: false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Pregunta actualizada correctamente',
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
affected: { type: 'number', example: 1 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
ApiResponse({ status: 400, description: 'Datos de actualización inválidos' }),
|
||||||
|
ApiResponse({ status: 404, description: 'Pregunta no encontrada' }),
|
||||||
|
ApiResponse({ status: 500, description: 'Error interno del servidor' })
|
||||||
|
);
|
||||||
|
|
||||||
|
// Documentación para eliminar una pregunta
|
||||||
|
static ApiRemove = applyDecorators(
|
||||||
|
ApiOperation({
|
||||||
|
summary: 'Eliminar una pregunta',
|
||||||
|
description: 'Elimina permanentemente una pregunta por su ID'
|
||||||
|
}),
|
||||||
|
ApiParam({
|
||||||
|
name: 'id',
|
||||||
|
description: 'ID de la pregunta a eliminar',
|
||||||
|
required: true,
|
||||||
|
type: 'number',
|
||||||
|
example: 1
|
||||||
|
}),
|
||||||
|
ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Pregunta eliminada correctamente',
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
affected: { type: 'number', example: 1 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
ApiResponse({ status: 404, description: 'Pregunta no encontrada' }),
|
||||||
|
ApiResponse({ status: 500, description: 'Error interno del servidor' })
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,9 +1,25 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { PreguntaService } from './pregunta.service';
|
import { PreguntaService } from './pregunta.service';
|
||||||
import { PreguntaController } from './pregunta.controller';
|
import { PreguntaController } from './pregunta.controller';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { Pregunta } from './entities/pregunta.entity';
|
||||||
|
import { TipoPreguntaModule } from '../tipo_pregunta/tipo_pregunta.module';
|
||||||
|
import { TipoPregunta } from '../tipo_pregunta/entities/tipo_pregunta.entity';
|
||||||
|
import { Opcion } from '../opcion/entities/opcion.entity';
|
||||||
|
import { PreguntaOpcion } from '../pregunta_opcion/entities/pregunta_opcion.entity';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([
|
||||||
|
Pregunta,
|
||||||
|
TipoPregunta,
|
||||||
|
Opcion,
|
||||||
|
PreguntaOpcion
|
||||||
|
]),
|
||||||
|
TipoPreguntaModule
|
||||||
|
],
|
||||||
controllers: [PreguntaController],
|
controllers: [PreguntaController],
|
||||||
providers: [PreguntaService],
|
providers: [PreguntaService],
|
||||||
|
exports: [TypeOrmModule, PreguntaService]
|
||||||
})
|
})
|
||||||
export class PreguntaModule {}
|
export class PreguntaModule {}
|
||||||
|
|||||||
@@ -1,26 +1,256 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
import { CreatePreguntaDto } from './dto/create-pregunta.dto';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository, DataSource } from 'typeorm';
|
||||||
|
import { CreatePreguntaDto, OpcionDto } from './dto/create-pregunta.dto';
|
||||||
import { UpdatePreguntaDto } from './dto/update-pregunta.dto';
|
import { UpdatePreguntaDto } from './dto/update-pregunta.dto';
|
||||||
|
import { Pregunta } from './entities/pregunta.entity';
|
||||||
|
import { TipoPregunta } from '../tipo_pregunta/entities/tipo_pregunta.entity';
|
||||||
|
import { Opcion } from '../opcion/entities/opcion.entity';
|
||||||
|
import { PreguntaOpcion } from '../pregunta_opcion/entities/pregunta_opcion.entity';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PreguntaService {
|
export class PreguntaService {
|
||||||
create(createPreguntaDto: CreatePreguntaDto) {
|
constructor(
|
||||||
return 'This action adds a new pregunta';
|
@InjectRepository(Pregunta)
|
||||||
|
private preguntaRepository: Repository<Pregunta>,
|
||||||
|
@InjectRepository(TipoPregunta)
|
||||||
|
private tipoPreguntaRepository: Repository<TipoPregunta>,
|
||||||
|
@InjectRepository(Opcion)
|
||||||
|
private opcionRepository: Repository<Opcion>,
|
||||||
|
@InjectRepository(PreguntaOpcion)
|
||||||
|
private preguntaOpcionRepository: Repository<PreguntaOpcion>,
|
||||||
|
private dataSource: DataSource
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async create(createPreguntaDto: CreatePreguntaDto) {
|
||||||
|
const queryRunner = this.dataSource.createQueryRunner();
|
||||||
|
await queryRunner.connect();
|
||||||
|
await queryRunner.startTransaction();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Obtener el tipo de pregunta por su nombre
|
||||||
|
const tipoPregunta = await this.tipoPreguntaRepository.findOne({
|
||||||
|
where: { tipo_pregunta: createPreguntaDto.tipo }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!tipoPregunta) {
|
||||||
|
throw new Error(`Tipo de pregunta '${createPreguntaDto.tipo}' no encontrado`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crear pregunta
|
||||||
|
const pregunta = new Pregunta();
|
||||||
|
pregunta.pregunta = createPreguntaDto.titulo;
|
||||||
|
pregunta.obligatoria = createPreguntaDto.obligatoria || false;
|
||||||
|
pregunta.id_tipo_pregunta = tipoPregunta.id_tipo;
|
||||||
|
pregunta.id_opcion_dependiente = createPreguntaDto.id_opcion_dependiente || undefined;
|
||||||
|
pregunta.contador_opcion = createPreguntaDto.opciones?.length || 0;
|
||||||
|
pregunta.validacion = createPreguntaDto.validacion || undefined;
|
||||||
|
|
||||||
|
const savedPregunta = await queryRunner.manager.save(pregunta);
|
||||||
|
|
||||||
|
// Crear opciones para preguntas de tipo multiple/radio
|
||||||
|
if (createPreguntaDto.opciones && createPreguntaDto.opciones.length > 0) {
|
||||||
|
await this.createOpciones(
|
||||||
|
queryRunner,
|
||||||
|
savedPregunta.id_pregunta,
|
||||||
|
createPreguntaDto.opciones
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await queryRunner.commitTransaction();
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
pregunta: savedPregunta
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
await queryRunner.rollbackTransaction();
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
await queryRunner.release();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
findAll() {
|
private async createOpciones(
|
||||||
return `This action returns all pregunta`;
|
queryRunner: any,
|
||||||
|
preguntaId: number,
|
||||||
|
opciones: OpcionDto[]
|
||||||
|
) {
|
||||||
|
for (let i = 0; i < opciones.length; i++) {
|
||||||
|
const opcionDto = opciones[i];
|
||||||
|
|
||||||
|
// Crear opción
|
||||||
|
const opcion = new Opcion();
|
||||||
|
opcion.opcion = opcionDto.valor;
|
||||||
|
|
||||||
|
const savedOpcion = await queryRunner.manager.save(opcion);
|
||||||
|
|
||||||
|
// Vincular opción con pregunta
|
||||||
|
const preguntaOpcion = new PreguntaOpcion();
|
||||||
|
preguntaOpcion.id_pregunta = preguntaId;
|
||||||
|
preguntaOpcion.opcion = savedOpcion;
|
||||||
|
preguntaOpcion.posicion = i + 1;
|
||||||
|
|
||||||
|
await queryRunner.manager.save(preguntaOpcion);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
findOne(id: number) {
|
async findAll() {
|
||||||
return `This action returns a #${id} pregunta`;
|
return this.preguntaRepository.find();
|
||||||
}
|
}
|
||||||
|
|
||||||
update(id: number, updatePreguntaDto: UpdatePreguntaDto) {
|
async findOne(id: number) {
|
||||||
return `This action updates a #${id} pregunta`;
|
const pregunta = await this.preguntaRepository.findOne({
|
||||||
|
where: { id_pregunta: id }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!pregunta) {
|
||||||
|
throw new NotFoundException(`Pregunta con ID ${id} no encontrada`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return pregunta;
|
||||||
}
|
}
|
||||||
|
|
||||||
remove(id: number) {
|
async findWithOptions(id: number) {
|
||||||
return `This action removes a #${id} pregunta`;
|
const pregunta = await this.preguntaRepository.findOne({
|
||||||
|
where: { id_pregunta: id },
|
||||||
|
relations: ['tipoPregunta']
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!pregunta) {
|
||||||
|
throw new NotFoundException(`Pregunta con ID ${id} no encontrada`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtener las opciones para esta pregunta
|
||||||
|
const preguntaOpciones = await this.preguntaOpcionRepository.find({
|
||||||
|
where: { id_pregunta: id },
|
||||||
|
relations: ['opcion'],
|
||||||
|
order: { posicion: 'ASC' }
|
||||||
|
});
|
||||||
|
|
||||||
|
const opciones = preguntaOpciones.map(po => ({
|
||||||
|
id: po.opcion.id_opcion,
|
||||||
|
valor: po.opcion.opcion,
|
||||||
|
posicion: po.posicion
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
...pregunta,
|
||||||
|
tipo: pregunta.tipoPregunta?.tipo_pregunta,
|
||||||
|
opciones
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: number, updatePreguntaDto: UpdatePreguntaDto) {
|
||||||
|
const queryRunner = this.dataSource.createQueryRunner();
|
||||||
|
await queryRunner.connect();
|
||||||
|
await queryRunner.startTransaction();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const pregunta = await this.preguntaRepository.findOne({
|
||||||
|
where: { id_pregunta: id }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!pregunta) {
|
||||||
|
throw new NotFoundException(`Pregunta con ID ${id} no encontrada`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actualizar los campos básicos de la pregunta
|
||||||
|
if (updatePreguntaDto.titulo) pregunta.pregunta = updatePreguntaDto.titulo;
|
||||||
|
if (updatePreguntaDto.obligatoria !== undefined) pregunta.obligatoria = updatePreguntaDto.obligatoria;
|
||||||
|
if (updatePreguntaDto.id_opcion_dependiente !== undefined) pregunta.id_opcion_dependiente = updatePreguntaDto.id_opcion_dependiente;
|
||||||
|
if (updatePreguntaDto.validacion !== undefined) pregunta.validacion = updatePreguntaDto.validacion;
|
||||||
|
|
||||||
|
// Si se proporciona un nuevo tipo de pregunta, actualizarlo
|
||||||
|
if (updatePreguntaDto.tipo) {
|
||||||
|
const tipoPregunta = await this.tipoPreguntaRepository.findOne({
|
||||||
|
where: { tipo_pregunta: updatePreguntaDto.tipo }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!tipoPregunta) {
|
||||||
|
throw new Error(`Tipo de pregunta '${updatePreguntaDto.tipo}' no encontrado`);
|
||||||
|
}
|
||||||
|
|
||||||
|
pregunta.id_tipo_pregunta = tipoPregunta.id_tipo;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actualizar la pregunta
|
||||||
|
const updatedPregunta = await queryRunner.manager.save(pregunta);
|
||||||
|
|
||||||
|
// Si se proporcionan nuevas opciones, actualizarlas
|
||||||
|
if (updatePreguntaDto.opciones && updatePreguntaDto.opciones.length > 0) {
|
||||||
|
// Eliminar las opciones existentes
|
||||||
|
const existingOptions = await this.preguntaOpcionRepository.find({
|
||||||
|
where: { id_pregunta: id }
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const option of existingOptions) {
|
||||||
|
await queryRunner.manager.remove(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crear las nuevas opciones
|
||||||
|
await this.createOpciones(
|
||||||
|
queryRunner,
|
||||||
|
id,
|
||||||
|
updatePreguntaDto.opciones
|
||||||
|
);
|
||||||
|
|
||||||
|
// Actualizar el contador de opciones
|
||||||
|
updatedPregunta.contador_opcion = updatePreguntaDto.opciones.length;
|
||||||
|
await queryRunner.manager.save(updatedPregunta);
|
||||||
|
}
|
||||||
|
|
||||||
|
await queryRunner.commitTransaction();
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
pregunta: updatedPregunta
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
await queryRunner.rollbackTransaction();
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
await queryRunner.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove(id: number) {
|
||||||
|
const queryRunner = this.dataSource.createQueryRunner();
|
||||||
|
await queryRunner.connect();
|
||||||
|
await queryRunner.startTransaction();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const pregunta = await this.preguntaRepository.findOne({
|
||||||
|
where: { id_pregunta: id }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!pregunta) {
|
||||||
|
throw new NotFoundException(`Pregunta con ID ${id} no encontrada`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eliminar las relaciones con opciones
|
||||||
|
const preguntaOpciones = await this.preguntaOpcionRepository.find({
|
||||||
|
where: { id_pregunta: id }
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const po of preguntaOpciones) {
|
||||||
|
await queryRunner.manager.remove(po);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eliminar la pregunta
|
||||||
|
await queryRunner.manager.remove(pregunta);
|
||||||
|
|
||||||
|
await queryRunner.commitTransaction();
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: `Pregunta con ID ${id} eliminada correctamente`
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
await queryRunner.rollbackTransaction();
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
await queryRunner.release();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Opcion } from 'src/opcion/entities/opcion.entity';
|
import { Opcion } from 'src/opcion/entities/opcion.entity';
|
||||||
import { Pregunta } from 'src/pregunta/entities/pregunta.entity';
|
import { Pregunta } from 'src/pregunta/entities/pregunta.entity';
|
||||||
import { RespuestaParticipanteCerrada } from 'src/respuesta_participante_cerrada/entities/respuesta_participante_cerrada.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()
|
@Entity()
|
||||||
@@ -12,14 +12,20 @@ export class PreguntaOpcion {
|
|||||||
@Column()
|
@Column()
|
||||||
posicion: number;
|
posicion: number;
|
||||||
|
|
||||||
@ManyToOne(() => Pregunta, pregunta => pregunta.id_pregunta)
|
@ManyToOne(() => Pregunta)
|
||||||
|
@JoinColumn({ name: 'id_pregunta' })
|
||||||
|
pregunta: Pregunta;
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
id_pregunta: Pregunta;
|
id_pregunta: number;
|
||||||
|
|
||||||
@ManyToOne(() => Opcion, opcion => opcion.id_opcion)
|
@ManyToOne(() => Opcion, opcion => opcion.preguntasOpciones)
|
||||||
|
@JoinColumn({ name: 'id_opcion' })
|
||||||
|
opcion: Opcion;
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
id_opcion: Opcion;
|
id_opcion: number;
|
||||||
|
|
||||||
@OneToMany(()=> RespuestaParticipanteCerrada, respuestaParticipanteCerrada=>respuestaParticipanteCerrada.id_pregunta_opcion)
|
@OneToMany(()=> RespuestaParticipanteCerrada, respuestaParticipanteCerrada=>respuestaParticipanteCerrada.preguntaOpcion)
|
||||||
respuestaParticipanteCerrada:RespuestaParticipanteCerrada[];
|
respuestaParticipanteCerrada:RespuestaParticipanteCerrada[];
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,19 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { PreguntaOpcionService } from './pregunta_opcion.service';
|
import { PreguntaOpcionService } from './pregunta_opcion.service';
|
||||||
import { PreguntaOpcionController } from './pregunta_opcion.controller';
|
import { PreguntaOpcionController } from './pregunta_opcion.controller';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { PreguntaOpcion } from './entities/pregunta_opcion.entity';
|
||||||
|
import { OpcionModule } from '../opcion/opcion.module';
|
||||||
|
import { PreguntaModule } from '../pregunta/pregunta.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([PreguntaOpcion]),
|
||||||
|
OpcionModule,
|
||||||
|
PreguntaModule
|
||||||
|
],
|
||||||
controllers: [PreguntaOpcionController],
|
controllers: [PreguntaOpcionController],
|
||||||
providers: [PreguntaOpcionService],
|
providers: [PreguntaOpcionService],
|
||||||
|
exports: [TypeOrmModule]
|
||||||
})
|
})
|
||||||
export class PreguntaOpcionModule {}
|
export class PreguntaOpcionModule {}
|
||||||
|
|||||||
+54
-22
@@ -1,36 +1,68 @@
|
|||||||
import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post } from '@nestjs/common';
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Delete,
|
||||||
|
Get,
|
||||||
|
Param,
|
||||||
|
ParseIntPipe,
|
||||||
|
Patch,
|
||||||
|
Post,
|
||||||
|
Query,
|
||||||
|
} from '@nestjs/common';
|
||||||
import { QrService } from './qr.service';
|
import { QrService } from './qr.service';
|
||||||
import { Qr } from './qr.entity';
|
import { Qr } from './qr.entity';
|
||||||
import { CreateQrDto } from './dto/create-qr.dto';
|
import { CreateQrDto } from './dto/create-qr.dto';
|
||||||
import { UpdateQrDto } from './dto/update.qr.dto';
|
import { UpdateQrDto } from './dto/update.qr.dto';
|
||||||
|
import { ApiOperation, ApiParam, ApiQuery } from '@nestjs/swagger';
|
||||||
|
|
||||||
@Controller('qr')
|
@Controller('qr')
|
||||||
export class QrController {
|
export class QrController {
|
||||||
|
constructor(private qrService: QrService) {}
|
||||||
|
|
||||||
constructor(private qrService: QrService) {}
|
@Get('generate')
|
||||||
|
@ApiOperation({ summary: 'Genera un código QR a partir de un texto' })
|
||||||
|
@ApiQuery({
|
||||||
|
name: 'text',
|
||||||
|
required: true,
|
||||||
|
description: 'Texto a codificar en el QR',
|
||||||
|
})
|
||||||
|
async generateQRCode(@Query('text') text: string): Promise<string> {
|
||||||
|
return this.qrService.generateQRCode(text);
|
||||||
|
}
|
||||||
|
|
||||||
@Get()
|
@Get('asistencia/:idParticipante/:idEvento')
|
||||||
getQrs(): Promise<Qr[]> {
|
@ApiOperation({ summary: 'Genera un código QR para asistencia con IDs de participante y evento' })
|
||||||
return this.qrService.getQrs();
|
@ApiParam({ name: 'idParticipante', description: 'ID del participante' })
|
||||||
}
|
@ApiParam({ name: 'idEvento', description: 'ID del evento' })
|
||||||
|
async generateAsistenciaQR(
|
||||||
|
@Param('idParticipante', ParseIntPipe) idParticipante: number,
|
||||||
|
@Param('idEvento', ParseIntPipe) idEvento: number
|
||||||
|
): Promise<string> {
|
||||||
|
return this.qrService.generateAsistenciaQR(idParticipante, idEvento);
|
||||||
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get()
|
||||||
getQr(@Param('id', ParseIntPipe) id: number) {
|
getQrs(): Promise<Qr[]> {
|
||||||
return this.qrService.getQr(id);
|
return this.qrService.getQrs();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post() //en el body ValidationPipe
|
@Get(':id')
|
||||||
createQr(@Body() newQr: CreateQrDto) {
|
getQr(@Param('id', ParseIntPipe) id: number) {
|
||||||
return this.qrService.createQr(newQr)
|
return this.qrService.getQr(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':id')
|
@Post() //en el body ValidationPipe
|
||||||
deleteQr(@Param('id', ParseIntPipe) id:number) {
|
createQr(@Body() newQr: CreateQrDto) {
|
||||||
return this.qrService.deleteQr(id);
|
return this.qrService.createQr(newQr);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Patch(':id')
|
@Delete(':id')
|
||||||
updateQr(@Param('id', ParseIntPipe) id: number, @Body() qr: UpdateQrDto) {
|
deleteQr(@Param('id', ParseIntPipe) id: number) {
|
||||||
return this.qrService.updateQr(id, qr)
|
return this.qrService.deleteQr(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Patch(':id')
|
||||||
|
updateQr(@Param('id', ParseIntPipe) id: number, @Body() qr: UpdateQrDto) {
|
||||||
|
return this.qrService.updateQr(id, qr);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+76
-44
@@ -4,70 +4,102 @@ import { InjectRepository } from '@nestjs/typeorm';
|
|||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { CreateQrDto } from './dto/create-qr.dto';
|
import { CreateQrDto } from './dto/create-qr.dto';
|
||||||
import { UpdateQrDto } from './dto/update.qr.dto';
|
import { UpdateQrDto } from './dto/update.qr.dto';
|
||||||
|
// @ts-ignore
|
||||||
|
import * as QRCode from 'qrcode';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class QrService {
|
export class QrService {
|
||||||
|
constructor(@InjectRepository(Qr) private qrRepository: Repository<Qr>) {}
|
||||||
|
|
||||||
constructor(
|
async generateQRCode(text: string): Promise<string> {
|
||||||
@InjectRepository(Qr) private qrRepository: Repository<Qr>
|
return await QRCode.toDataURL(text);
|
||||||
) {}
|
}
|
||||||
|
|
||||||
async createQr(qr: CreateQrDto) {
|
async generateAsistenciaQR(id_participante: number, id_evento: number): Promise<string> {
|
||||||
const qrFound = await this.qrRepository.findOne({
|
// Crear un objeto JSON que contenga los IDs
|
||||||
where: {
|
const qrData = {
|
||||||
id_qr: qr.id_participante_evento
|
id_participante,
|
||||||
}
|
id_evento
|
||||||
})
|
};
|
||||||
|
|
||||||
|
// Convertir el objeto a una cadena JSON
|
||||||
|
const jsonStr = JSON.stringify(qrData);
|
||||||
|
|
||||||
|
// Generar el código QR con la cadena JSON
|
||||||
|
return await QRCode.toDataURL(jsonStr);
|
||||||
|
}
|
||||||
|
|
||||||
if (qrFound) {
|
async generateBuffer(text: string): Promise<Buffer> {
|
||||||
return new HttpException('Qr already exists', HttpStatus.CONFLICT)
|
return await QRCode.toBuffer(text); // Devuelve un buffer para adjuntar en email o stream
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.qrRepository.save(qr)
|
// Para enviar el QR por email
|
||||||
|
/* const qrBuffer = await this.qrService.generateBuffer('Texto a codificar');
|
||||||
|
await transporter.sendMail({
|
||||||
|
to: 'destinatario@example.com',
|
||||||
|
subject: 'Tu código QR',
|
||||||
|
html: '<p>Escanea el siguiente código:</p><img src="cid:qrcode"/>',
|
||||||
|
attachments: [{
|
||||||
|
filename: 'qrcode.png',
|
||||||
|
content: qrBuffer,
|
||||||
|
cid: 'qrcode',
|
||||||
|
}],
|
||||||
|
}); */
|
||||||
|
|
||||||
|
async createQr(qr: CreateQrDto) {
|
||||||
|
const qrFound = await this.qrRepository.findOne({
|
||||||
|
where: {
|
||||||
|
id_qr: qr.id_participante_evento,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (qrFound) {
|
||||||
|
return new HttpException('Qr already exists', HttpStatus.CONFLICT);
|
||||||
}
|
}
|
||||||
|
|
||||||
getQrs() {
|
return this.qrRepository.save(qr);
|
||||||
return this.qrRepository.find({})
|
}
|
||||||
}
|
|
||||||
|
|
||||||
async getQr(id_qr) {
|
getQrs() {
|
||||||
const qrFound = await this.qrRepository.findOne({
|
return this.qrRepository.find({});
|
||||||
where: {
|
}
|
||||||
id_qr
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!qrFound) {
|
async getQr(id_qr) {
|
||||||
return new HttpException('User not found', HttpStatus.NOT_FOUND);
|
const qrFound = await this.qrRepository.findOne({
|
||||||
}
|
where: {
|
||||||
|
id_qr,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return qrFound
|
if (!qrFound) {
|
||||||
|
return new HttpException('User not found', HttpStatus.NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteQr(id_qr: number) {
|
return qrFound;
|
||||||
const result = await this.qrRepository.delete({ id_qr })
|
}
|
||||||
|
|
||||||
if (result.affected === 0) {
|
async deleteQr(id_qr: number) {
|
||||||
return new HttpException('User not found', HttpStatus.NOT_FOUND);
|
const result = await this.qrRepository.delete({ id_qr });
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
if (result.affected === 0) {
|
||||||
|
return new HttpException('User not found', HttpStatus.NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateQr(id_qr: number, qr: UpdateQrDto) {
|
return result;
|
||||||
const qrFound = await this.qrRepository.findOne({
|
}
|
||||||
where: {
|
|
||||||
id_qr
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!qrFound) {
|
async updateQr(id_qr: number, qr: UpdateQrDto) {
|
||||||
return new HttpException('User not found', HttpStatus.NOT_FOUND)
|
const qrFound = await this.qrRepository.findOne({
|
||||||
}
|
where: {
|
||||||
|
id_qr,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const updateQr = Object.assign(qrFound, qr)
|
if (!qrFound) {
|
||||||
return this.qrRepository.save(updateQr)
|
return new HttpException('User not found', HttpStatus.NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updateQr = Object.assign(qrFound, qr);
|
||||||
|
return this.qrRepository.save(updateQr);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-7
@@ -14,17 +14,14 @@ export class RespuestaParticipanteAbierta {
|
|||||||
// Relación con CuestionarioRespondido
|
// Relación con CuestionarioRespondido
|
||||||
@ManyToOne(
|
@ManyToOne(
|
||||||
() => CuestionarioRespondido,
|
() => CuestionarioRespondido,
|
||||||
cuestionarioRespondido => cuestionarioRespondido.idCuestionarioRespondido
|
cuestionarioRespondido => cuestionarioRespondido.respuestasAbiertas
|
||||||
)
|
)
|
||||||
@JoinColumn({ name: 'id_cuestionario_respondido' })
|
@JoinColumn({ name: 'id_cuestionario_respondido' })
|
||||||
id_cuestionario_respondido: CuestionarioRespondido;
|
cuestionarioRespondido: CuestionarioRespondido;
|
||||||
|
|
||||||
@ManyToOne(
|
@ManyToOne(() => Pregunta)
|
||||||
() => Pregunta,
|
|
||||||
pregunta => pregunta.id_pregunta
|
|
||||||
)
|
|
||||||
@JoinColumn({ name: 'id_pregunta' })
|
@JoinColumn({ name: 'id_pregunta' })
|
||||||
id_pregunta: Pregunta;
|
pregunta: Pregunta;
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,19 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { RespuestaParticipanteAbiertaService } from './respuesta_participante_abierta.service';
|
import { RespuestaParticipanteAbiertaService } from './respuesta_participante_abierta.service';
|
||||||
import { RespuestaParticipanteAbiertaController } from './respuesta_participante_abierta.controller';
|
import { RespuestaParticipanteAbiertaController } from './respuesta_participante_abierta.controller';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { RespuestaParticipanteAbierta } from './entities/respuesta_participante_abierta.entity';
|
||||||
|
import { CuestionarioRespondidoModule } from '../cuestionario_respondido/cuestionario_respondido.module';
|
||||||
|
import { PreguntaModule } from '../pregunta/pregunta.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([RespuestaParticipanteAbierta]),
|
||||||
|
CuestionarioRespondidoModule,
|
||||||
|
PreguntaModule
|
||||||
|
],
|
||||||
controllers: [RespuestaParticipanteAbiertaController],
|
controllers: [RespuestaParticipanteAbiertaController],
|
||||||
providers: [RespuestaParticipanteAbiertaService],
|
providers: [RespuestaParticipanteAbiertaService],
|
||||||
|
exports: [TypeOrmModule]
|
||||||
})
|
})
|
||||||
export class RespuestaParticipanteAbiertaModule {}
|
export class RespuestaParticipanteAbiertaModule {}
|
||||||
|
|||||||
+3
-3
@@ -10,11 +10,11 @@ export class RespuestaParticipanteCerrada {
|
|||||||
|
|
||||||
@ManyToOne(() => CuestionarioRespondido, cuestionarioRespondido => cuestionarioRespondido.respuestasCerradas)
|
@ManyToOne(() => CuestionarioRespondido, cuestionarioRespondido => cuestionarioRespondido.respuestasCerradas)
|
||||||
@JoinColumn({ name: 'id_cuestionario_respondido' })
|
@JoinColumn({ name: 'id_cuestionario_respondido' })
|
||||||
id_cuestionario_respondido: CuestionarioRespondido;
|
cuestionarioRespondido: CuestionarioRespondido;
|
||||||
|
|
||||||
@ManyToOne(() => PreguntaOpcion, preguntaOpcion => preguntaOpcion.id_pregunta_opcion)
|
@ManyToOne(() => PreguntaOpcion, preguntaOpcion => preguntaOpcion.respuestaParticipanteCerrada)
|
||||||
@JoinColumn({ name: 'id_pregunta_opcion' })
|
@JoinColumn({ name: 'id_pregunta_opcion' })
|
||||||
id_pregunta_opcion: PreguntaOpcion;
|
preguntaOpcion: PreguntaOpcion;
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,19 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { RespuestaParticipanteCerradaService } from './respuesta_participante_cerrada.service';
|
import { RespuestaParticipanteCerradaService } from './respuesta_participante_cerrada.service';
|
||||||
import { RespuestaParticipanteCerradaController } from './respuesta_participante_cerrada.controller';
|
import { RespuestaParticipanteCerradaController } from './respuesta_participante_cerrada.controller';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { RespuestaParticipanteCerrada } from './entities/respuesta_participante_cerrada.entity';
|
||||||
|
import { PreguntaOpcionModule } from '../pregunta_opcion/pregunta_opcion.module';
|
||||||
|
import { CuestionarioRespondidoModule } from '../cuestionario_respondido/cuestionario_respondido.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([RespuestaParticipanteCerrada]),
|
||||||
|
PreguntaOpcionModule,
|
||||||
|
CuestionarioRespondidoModule
|
||||||
|
],
|
||||||
controllers: [RespuestaParticipanteCerradaController],
|
controllers: [RespuestaParticipanteCerradaController],
|
||||||
providers: [RespuestaParticipanteCerradaService],
|
providers: [RespuestaParticipanteCerradaService],
|
||||||
|
exports: [TypeOrmModule]
|
||||||
})
|
})
|
||||||
export class RespuestaParticipanteCerradaModule {}
|
export class RespuestaParticipanteCerradaModule {}
|
||||||
|
|||||||
@@ -1 +1,23 @@
|
|||||||
export class CreateSeccionDto {}
|
import { CreatePreguntaDto } from '../../pregunta/dto/create-pregunta.dto';
|
||||||
|
import { IsArray, IsNotEmpty, IsNumber, IsOptional, IsString, ValidateNested } from 'class-validator';
|
||||||
|
import { Type } from 'class-transformer';
|
||||||
|
|
||||||
|
export class CreateSeccionDto {
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsString()
|
||||||
|
titulo: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
descripcion?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
contador_pregunta?: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => CreatePreguntaDto)
|
||||||
|
preguntas?: CreatePreguntaDto[];
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export class Seccion {
|
|||||||
contador_pregunta: number;
|
contador_pregunta: number;
|
||||||
|
|
||||||
@Column({ type: 'text', nullable: true })
|
@Column({ type: 'text', nullable: true })
|
||||||
descripcion: string;
|
descripcion?: string;
|
||||||
|
|
||||||
@Column({ type: 'text', nullable: true })
|
@Column({ type: 'text', nullable: true })
|
||||||
titulo: string;
|
titulo: string;
|
||||||
@@ -19,6 +19,6 @@ export class Seccion {
|
|||||||
@OneToMany(() => CuestionarioSeccion,(cuestionario_seccion) => cuestionario_seccion.id_seccion )
|
@OneToMany(() => CuestionarioSeccion,(cuestionario_seccion) => cuestionario_seccion.id_seccion )
|
||||||
seccion:CuestionarioSeccion[];
|
seccion:CuestionarioSeccion[];
|
||||||
|
|
||||||
@OneToMany(()=> SeccionPregunta, (seccion_pregunta)=> seccion_pregunta.id_seccion)
|
@OneToMany(()=> SeccionPregunta, (seccion_pregunta)=> seccion_pregunta.seccion)
|
||||||
seccion_pregunta:SeccionPregunta[];
|
seccionPreguntas:SeccionPregunta[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,32 +2,39 @@ import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/commo
|
|||||||
import { SeccionService } from './seccion.service';
|
import { SeccionService } from './seccion.service';
|
||||||
import { CreateSeccionDto } from './dto/create-seccion.dto';
|
import { CreateSeccionDto } from './dto/create-seccion.dto';
|
||||||
import { UpdateSeccionDto } from './dto/update-seccion.dto';
|
import { UpdateSeccionDto } from './dto/update-seccion.dto';
|
||||||
|
import { SeccionApiDocumentation } from './seccion.documentation';
|
||||||
|
|
||||||
@Controller('seccion')
|
@Controller('seccion')
|
||||||
|
@SeccionApiDocumentation.ApiController
|
||||||
export class SeccionController {
|
export class SeccionController {
|
||||||
constructor(private readonly seccionService: SeccionService) {}
|
constructor(private readonly seccionService: SeccionService) {}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
|
@SeccionApiDocumentation.ApiCreate
|
||||||
create(@Body() createSeccionDto: CreateSeccionDto) {
|
create(@Body() createSeccionDto: CreateSeccionDto) {
|
||||||
return this.seccionService.create(createSeccionDto);
|
return this.seccionService.create(createSeccionDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
|
@SeccionApiDocumentation.ApiGetAll
|
||||||
findAll() {
|
findAll() {
|
||||||
return this.seccionService.findAll();
|
return this.seccionService.findAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
|
@SeccionApiDocumentation.ApiGetOne
|
||||||
findOne(@Param('id') id: string) {
|
findOne(@Param('id') id: string) {
|
||||||
return this.seccionService.findOne(+id);
|
return this.seccionService.findOne(+id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Patch(':id')
|
@Patch(':id')
|
||||||
|
@SeccionApiDocumentation.ApiUpdate
|
||||||
update(@Param('id') id: string, @Body() updateSeccionDto: UpdateSeccionDto) {
|
update(@Param('id') id: string, @Body() updateSeccionDto: UpdateSeccionDto) {
|
||||||
return this.seccionService.update(+id, updateSeccionDto);
|
return this.seccionService.update(+id, updateSeccionDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
|
@SeccionApiDocumentation.ApiRemove
|
||||||
remove(@Param('id') id: string) {
|
remove(@Param('id') id: string) {
|
||||||
return this.seccionService.remove(+id);
|
return this.seccionService.remove(+id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,185 @@
|
|||||||
|
import { ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||||
|
import { applyDecorators } from '@nestjs/common';
|
||||||
|
|
||||||
|
export class SeccionApiDocumentation {
|
||||||
|
// Decoradores para toda la clase del controlador
|
||||||
|
static ApiController = ApiTags('Secciones');
|
||||||
|
|
||||||
|
// Documentación para crear una sección
|
||||||
|
static ApiCreate = applyDecorators(
|
||||||
|
ApiOperation({
|
||||||
|
summary: 'Crear una nueva sección',
|
||||||
|
description: 'Crea una nueva sección para un cuestionario'
|
||||||
|
}),
|
||||||
|
ApiBody({
|
||||||
|
description: 'Datos de la sección a crear',
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
required: ['titulo'],
|
||||||
|
properties: {
|
||||||
|
titulo: { type: 'string', example: 'Información Personal' },
|
||||||
|
descripcion: {
|
||||||
|
type: 'string',
|
||||||
|
example: 'Proporciona tus datos personales para poder contactarte'
|
||||||
|
},
|
||||||
|
contador_pregunta: { type: 'number', example: 0 },
|
||||||
|
preguntas: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
titulo: { type: 'string', example: '¿Eres parte de la comunidad?' },
|
||||||
|
obligatoria: { type: 'boolean', example: true },
|
||||||
|
tipo: { type: 'string', example: 'Multiple' },
|
||||||
|
opciones: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
valor: { type: 'string', example: 'Si' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
ApiResponse({
|
||||||
|
status: 201,
|
||||||
|
description: 'Sección creada correctamente',
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id_seccion: { type: 'number', example: 1 },
|
||||||
|
titulo: { type: 'string', example: 'Información Personal' },
|
||||||
|
descripcion: { type: 'string' },
|
||||||
|
contador_pregunta: { type: 'number', example: 0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
ApiResponse({ status: 400, description: 'Datos de la sección inválidos' }),
|
||||||
|
ApiResponse({ status: 500, description: 'Error interno del servidor' })
|
||||||
|
);
|
||||||
|
|
||||||
|
// Documentación para obtener todas las secciones
|
||||||
|
static ApiGetAll = applyDecorators(
|
||||||
|
ApiOperation({
|
||||||
|
summary: 'Obtener todas las secciones',
|
||||||
|
description: 'Retorna una lista de todas las secciones registradas'
|
||||||
|
}),
|
||||||
|
ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Lista de secciones obtenida correctamente',
|
||||||
|
schema: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id_seccion: { type: 'number', example: 1 },
|
||||||
|
titulo: { type: 'string', example: 'Información Personal' },
|
||||||
|
descripcion: { type: 'string' },
|
||||||
|
contador_pregunta: { type: 'number', example: 0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
ApiResponse({ status: 500, description: 'Error interno del servidor' })
|
||||||
|
);
|
||||||
|
|
||||||
|
// Documentación para obtener una sección por ID
|
||||||
|
static ApiGetOne = applyDecorators(
|
||||||
|
ApiOperation({
|
||||||
|
summary: 'Obtener una sección por ID',
|
||||||
|
description: 'Retorna una sección específica por su ID'
|
||||||
|
}),
|
||||||
|
ApiParam({
|
||||||
|
name: 'id',
|
||||||
|
description: 'ID de la sección',
|
||||||
|
required: true,
|
||||||
|
type: 'number',
|
||||||
|
example: 1
|
||||||
|
}),
|
||||||
|
ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Sección obtenida correctamente',
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id_seccion: { type: 'number', example: 1 },
|
||||||
|
titulo: { type: 'string', example: 'Información Personal' },
|
||||||
|
descripcion: { type: 'string' },
|
||||||
|
contador_pregunta: { type: 'number', example: 0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
ApiResponse({ status: 404, description: 'Sección no encontrada' }),
|
||||||
|
ApiResponse({ status: 500, description: 'Error interno del servidor' })
|
||||||
|
);
|
||||||
|
|
||||||
|
// Documentación para actualizar una sección
|
||||||
|
static ApiUpdate = applyDecorators(
|
||||||
|
ApiOperation({
|
||||||
|
summary: 'Actualizar una sección',
|
||||||
|
description: 'Actualiza los datos de una sección existente'
|
||||||
|
}),
|
||||||
|
ApiParam({
|
||||||
|
name: 'id',
|
||||||
|
description: 'ID de la sección a actualizar',
|
||||||
|
required: true,
|
||||||
|
type: 'number',
|
||||||
|
example: 1
|
||||||
|
}),
|
||||||
|
ApiBody({
|
||||||
|
description: 'Datos a actualizar de la sección',
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
titulo: { type: 'string', example: 'Título actualizado' },
|
||||||
|
descripcion: { type: 'string', example: 'Descripción actualizada' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Sección actualizada correctamente',
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
affected: { type: 'number', example: 1 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
ApiResponse({ status: 400, description: 'Datos de actualización inválidos' }),
|
||||||
|
ApiResponse({ status: 404, description: 'Sección no encontrada' }),
|
||||||
|
ApiResponse({ status: 500, description: 'Error interno del servidor' })
|
||||||
|
);
|
||||||
|
|
||||||
|
// Documentación para eliminar una sección
|
||||||
|
static ApiRemove = applyDecorators(
|
||||||
|
ApiOperation({
|
||||||
|
summary: 'Eliminar una sección',
|
||||||
|
description: 'Elimina permanentemente una sección por su ID'
|
||||||
|
}),
|
||||||
|
ApiParam({
|
||||||
|
name: 'id',
|
||||||
|
description: 'ID de la sección a eliminar',
|
||||||
|
required: true,
|
||||||
|
type: 'number',
|
||||||
|
example: 1
|
||||||
|
}),
|
||||||
|
ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Sección eliminada correctamente',
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
affected: { type: 'number', example: 1 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
ApiResponse({ status: 404, description: 'Sección no encontrada' }),
|
||||||
|
ApiResponse({ status: 500, description: 'Error interno del servidor' })
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,9 +1,27 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { SeccionService } from './seccion.service';
|
import { SeccionService } from './seccion.service';
|
||||||
import { SeccionController } from './seccion.controller';
|
import { SeccionController } from './seccion.controller';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { Seccion } from './entities/seccion.entity';
|
||||||
|
import { Pregunta } from '../pregunta/entities/pregunta.entity';
|
||||||
|
import { SeccionPregunta } from '../seccion_pregunta/entities/seccion_pregunta.entity';
|
||||||
|
import { TipoPregunta } from '../tipo_pregunta/entities/tipo_pregunta.entity';
|
||||||
|
import { Opcion } from '../opcion/entities/opcion.entity';
|
||||||
|
import { PreguntaOpcion } from '../pregunta_opcion/entities/pregunta_opcion.entity';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([
|
||||||
|
Seccion,
|
||||||
|
Pregunta,
|
||||||
|
SeccionPregunta,
|
||||||
|
TipoPregunta,
|
||||||
|
Opcion,
|
||||||
|
PreguntaOpcion
|
||||||
|
])
|
||||||
|
],
|
||||||
controllers: [SeccionController],
|
controllers: [SeccionController],
|
||||||
providers: [SeccionService],
|
providers: [SeccionService],
|
||||||
|
exports: [TypeOrmModule, SeccionService]
|
||||||
})
|
})
|
||||||
export class SeccionModule {}
|
export class SeccionModule {}
|
||||||
|
|||||||
+190
-11
@@ -1,26 +1,205 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository, DataSource } from 'typeorm';
|
||||||
import { CreateSeccionDto } from './dto/create-seccion.dto';
|
import { CreateSeccionDto } from './dto/create-seccion.dto';
|
||||||
import { UpdateSeccionDto } from './dto/update-seccion.dto';
|
import { UpdateSeccionDto } from './dto/update-seccion.dto';
|
||||||
|
import { Seccion } from './entities/seccion.entity';
|
||||||
|
import { Pregunta } from '../pregunta/entities/pregunta.entity';
|
||||||
|
import { SeccionPregunta } from '../seccion_pregunta/entities/seccion_pregunta.entity';
|
||||||
|
import { TipoPregunta } from '../tipo_pregunta/entities/tipo_pregunta.entity';
|
||||||
|
import { Opcion } from '../opcion/entities/opcion.entity';
|
||||||
|
import { PreguntaOpcion } from '../pregunta_opcion/entities/pregunta_opcion.entity';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SeccionService {
|
export class SeccionService {
|
||||||
create(createSeccionDto: CreateSeccionDto) {
|
constructor(
|
||||||
return 'This action adds a new seccion';
|
@InjectRepository(Seccion)
|
||||||
|
private seccionRepository: Repository<Seccion>,
|
||||||
|
@InjectRepository(Pregunta)
|
||||||
|
private preguntaRepository: Repository<Pregunta>,
|
||||||
|
@InjectRepository(SeccionPregunta)
|
||||||
|
private seccionPreguntaRepository: Repository<SeccionPregunta>,
|
||||||
|
@InjectRepository(TipoPregunta)
|
||||||
|
private tipoPreguntaRepository: Repository<TipoPregunta>,
|
||||||
|
@InjectRepository(Opcion)
|
||||||
|
private opcionRepository: Repository<Opcion>,
|
||||||
|
@InjectRepository(PreguntaOpcion)
|
||||||
|
private preguntaOpcionRepository: Repository<PreguntaOpcion>,
|
||||||
|
private dataSource: DataSource
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async create(createSeccionDto: CreateSeccionDto) {
|
||||||
|
const queryRunner = this.dataSource.createQueryRunner();
|
||||||
|
await queryRunner.connect();
|
||||||
|
await queryRunner.startTransaction();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Crear sección
|
||||||
|
const seccion = new Seccion();
|
||||||
|
seccion.titulo = createSeccionDto.titulo;
|
||||||
|
seccion.descripcion = createSeccionDto.descripcion;
|
||||||
|
seccion.contador_pregunta = createSeccionDto.preguntas?.length || 0;
|
||||||
|
|
||||||
|
const savedSeccion = await queryRunner.manager.save(seccion);
|
||||||
|
|
||||||
|
// 2. Crear preguntas y vincularlas a la sección
|
||||||
|
if (createSeccionDto.preguntas && createSeccionDto.preguntas.length > 0) {
|
||||||
|
for (let i = 0; i < createSeccionDto.preguntas.length; i++) {
|
||||||
|
const preguntaDto = createSeccionDto.preguntas[i];
|
||||||
|
|
||||||
|
// Obtener el tipo de pregunta por su nombre
|
||||||
|
const tipoPregunta = await queryRunner.manager.findOne(TipoPregunta, {
|
||||||
|
where: { tipo_pregunta: preguntaDto.tipo },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!tipoPregunta) {
|
||||||
|
throw new Error(`Tipo de pregunta '${preguntaDto.tipo}' no encontrado`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crear pregunta
|
||||||
|
const pregunta = new Pregunta();
|
||||||
|
pregunta.pregunta = preguntaDto.titulo;
|
||||||
|
pregunta.obligatoria = preguntaDto.obligatoria || false;
|
||||||
|
pregunta.id_tipo_pregunta = tipoPregunta.id_tipo;
|
||||||
|
pregunta.id_opcion_dependiente = preguntaDto.id_opcion_dependiente || undefined;
|
||||||
|
pregunta.contador_opcion = preguntaDto.opciones?.length || 0;
|
||||||
|
|
||||||
|
const savedPregunta = await queryRunner.manager.save(pregunta);
|
||||||
|
|
||||||
|
// Vincular pregunta con sección
|
||||||
|
const seccionPregunta = new SeccionPregunta();
|
||||||
|
seccionPregunta.id_seccion = savedSeccion.id_seccion;
|
||||||
|
seccionPregunta.id_pregunta = savedPregunta.id_pregunta;
|
||||||
|
seccionPregunta.posicion = i + 1;
|
||||||
|
|
||||||
|
await queryRunner.manager.save(seccionPregunta);
|
||||||
|
|
||||||
|
// Crear opciones para preguntas de tipo multiple/radio
|
||||||
|
if (preguntaDto.opciones && preguntaDto.opciones.length > 0) {
|
||||||
|
for (let j = 0; j < preguntaDto.opciones.length; j++) {
|
||||||
|
const opcionDto = preguntaDto.opciones[j];
|
||||||
|
|
||||||
|
// Crear opción
|
||||||
|
const opcion = new Opcion();
|
||||||
|
opcion.opcion = opcionDto.valor;
|
||||||
|
|
||||||
|
const savedOpcion = await queryRunner.manager.save(opcion);
|
||||||
|
|
||||||
|
// Vincular opción con pregunta
|
||||||
|
const preguntaOpcion = new PreguntaOpcion();
|
||||||
|
preguntaOpcion.id_pregunta = savedPregunta.id_pregunta;
|
||||||
|
preguntaOpcion.opcion = savedOpcion;
|
||||||
|
preguntaOpcion.posicion = j + 1;
|
||||||
|
|
||||||
|
await queryRunner.manager.save(preguntaOpcion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await queryRunner.commitTransaction();
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
seccion: savedSeccion,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
await queryRunner.rollbackTransaction();
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
await queryRunner.release();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
findAll() {
|
async findAll() {
|
||||||
return `This action returns all seccion`;
|
return this.seccionRepository.find();
|
||||||
}
|
}
|
||||||
|
|
||||||
findOne(id: number) {
|
async findOne(id: number) {
|
||||||
return `This action returns a #${id} seccion`;
|
const seccion = await this.seccionRepository.findOne({
|
||||||
|
where: { id_seccion: id }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!seccion) {
|
||||||
|
throw new NotFoundException(`Sección con ID ${id} no encontrada`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return seccion;
|
||||||
}
|
}
|
||||||
|
|
||||||
update(id: number, updateSeccionDto: UpdateSeccionDto) {
|
async findWithPreguntas(id: number) {
|
||||||
return `This action updates a #${id} seccion`;
|
const seccion = await this.seccionRepository.findOne({
|
||||||
|
where: { id_seccion: id }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!seccion) {
|
||||||
|
throw new NotFoundException(`Sección con ID ${id} no encontrada`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtener las relaciones seccion-pregunta para esta sección
|
||||||
|
const seccionPreguntas = await this.seccionPreguntaRepository.find({
|
||||||
|
where: { id_seccion: id },
|
||||||
|
relations: ['pregunta']
|
||||||
|
});
|
||||||
|
|
||||||
|
// Extraer y organizar las preguntas
|
||||||
|
const preguntas = await Promise.all(
|
||||||
|
seccionPreguntas.map(async (sp) => {
|
||||||
|
const pregunta = await this.preguntaRepository.findOne({
|
||||||
|
where: { id_pregunta: sp.id_pregunta }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Obtener las opciones para preguntas de tipo multiple/radio
|
||||||
|
const preguntaOpciones = await this.preguntaOpcionRepository.find({
|
||||||
|
where: { id_pregunta: sp.id_pregunta },
|
||||||
|
relations: ['opcion'],
|
||||||
|
order: { posicion: 'ASC' }
|
||||||
|
});
|
||||||
|
|
||||||
|
const opciones = preguntaOpciones.map(po => po.opcion);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...pregunta,
|
||||||
|
posicion: sp.posicion,
|
||||||
|
opciones
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Ordenar las preguntas por posición
|
||||||
|
preguntas.sort((a, b) => a.posicion - b.posicion);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...seccion,
|
||||||
|
preguntas
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
remove(id: number) {
|
async update(id: number, updateSeccionDto: UpdateSeccionDto) {
|
||||||
return `This action removes a #${id} seccion`;
|
const seccion = await this.seccionRepository.findOne({
|
||||||
|
where: { id_seccion: id }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!seccion) {
|
||||||
|
throw new NotFoundException(`Sección con ID ${id} no encontrada`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actualizar solo los campos proporcionados
|
||||||
|
if (updateSeccionDto.titulo) seccion.titulo = updateSeccionDto.titulo;
|
||||||
|
if (updateSeccionDto.descripcion !== undefined) seccion.descripcion = updateSeccionDto.descripcion;
|
||||||
|
|
||||||
|
return this.seccionRepository.save(seccion);
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove(id: number) {
|
||||||
|
const seccion = await this.seccionRepository.findOne({
|
||||||
|
where: { id_seccion: id }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!seccion) {
|
||||||
|
throw new NotFoundException(`Sección con ID ${id} no encontrada`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.seccionRepository.remove(seccion);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,11 +11,15 @@ export class SeccionPregunta {
|
|||||||
@Column({ type: 'tinyint' })
|
@Column({ type: 'tinyint' })
|
||||||
posicion: number;
|
posicion: number;
|
||||||
|
|
||||||
@ManyToOne(() => Pregunta, (pregunta) => pregunta.id_pregunta)
|
@ManyToOne(() => Pregunta, (pregunta) => pregunta.seccionPreguntas)
|
||||||
|
pregunta: Pregunta;
|
||||||
|
|
||||||
@Column({ type: 'int' })
|
@Column({ type: 'int' })
|
||||||
id_pregunta: number;
|
id_pregunta: number;
|
||||||
|
|
||||||
@ManyToOne(() => Seccion, (seccion) => seccion.id_seccion)
|
@ManyToOne(() => Seccion, (seccion) => seccion.seccionPreguntas)
|
||||||
|
seccion: Seccion;
|
||||||
|
|
||||||
@Column({ type: 'int' })
|
@Column({ type: 'int' })
|
||||||
id_seccion: number;
|
id_seccion: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,19 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { SeccionPreguntaService } from './seccion_pregunta.service';
|
import { SeccionPreguntaService } from './seccion_pregunta.service';
|
||||||
import { SeccionPreguntaController } from './seccion_pregunta.controller';
|
import { SeccionPreguntaController } from './seccion_pregunta.controller';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { SeccionPregunta } from './entities/seccion_pregunta.entity';
|
||||||
|
import { PreguntaModule } from '../pregunta/pregunta.module';
|
||||||
|
import { SeccionModule } from '../seccion/seccion.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([SeccionPregunta]),
|
||||||
|
PreguntaModule,
|
||||||
|
SeccionModule
|
||||||
|
],
|
||||||
controllers: [SeccionPreguntaController],
|
controllers: [SeccionPreguntaController],
|
||||||
providers: [SeccionPreguntaService],
|
providers: [SeccionPreguntaService],
|
||||||
|
exports: [TypeOrmModule]
|
||||||
})
|
})
|
||||||
export class SeccionPreguntaModule {}
|
export class SeccionPreguntaModule {}
|
||||||
|
|||||||
@@ -9,6 +9,6 @@ export class TipoPregunta {
|
|||||||
@Column()
|
@Column()
|
||||||
tipo_pregunta: string;
|
tipo_pregunta: string;
|
||||||
|
|
||||||
@OneToMany(() => Pregunta, pregunta => pregunta.id_tipo_pregunta)
|
@OneToMany(() => Pregunta, pregunta => pregunta.tipoPregunta)
|
||||||
preguntas: Pregunta[];
|
preguntas: Pregunta[];
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { Controller } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Controller('tipo_pregunta')
|
||||||
|
export class TipoPreguntaController {
|
||||||
|
// Métodos del controlador
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { TipoPreguntaService } from './tipo_pregunta.service';
|
import { TipoPreguntaService } from './tipo_pregunta.service';
|
||||||
import { TipoPreguntaController } from './tipo_pregunta.controller';
|
import { TipoPreguntaController } from './tipo_pregunta.controller';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { TipoPregunta } from './entities/tipo_pregunta.entity';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
|
imports: [TypeOrmModule.forFeature([TipoPregunta])],
|
||||||
controllers: [TipoPreguntaController],
|
controllers: [TipoPreguntaController],
|
||||||
providers: [TipoPreguntaService],
|
providers: [TipoPreguntaService],
|
||||||
|
exports: [TipoPreguntaService, TypeOrmModule]
|
||||||
})
|
})
|
||||||
export class TipoPreguntaModule {}
|
export class TipoPreguntaModule {}
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { Injectable, OnModuleInit } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { CreateTipoPreguntaDto } from './dto/create-tipo_pregunta.dto';
|
||||||
|
import { UpdateTipoPreguntaDto } from './dto/update-tipo_pregunta.dto';
|
||||||
|
import { TipoPregunta } from './entities/tipo_pregunta.entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TipoPreguntaService implements OnModuleInit {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(TipoPregunta)
|
||||||
|
private tipoPreguntaRepository: Repository<TipoPregunta>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async onModuleInit() {
|
||||||
|
await this.seedTipoPreguntas();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async seedTipoPreguntas() {
|
||||||
|
const defaultTipos = [
|
||||||
|
{ tipo_pregunta: 'Cerrada' },
|
||||||
|
{ tipo_pregunta: 'Abierta' },
|
||||||
|
{ tipo_pregunta: 'Multiple' },
|
||||||
|
{ tipo_pregunta: 'Texto' }
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const tipo of defaultTipos) {
|
||||||
|
const existingTipo = await this.tipoPreguntaRepository.findOne({
|
||||||
|
where: { tipo_pregunta: tipo.tipo_pregunta },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existingTipo) {
|
||||||
|
await this.tipoPreguntaRepository.save(tipo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
create(createTipoPreguntaDto: CreateTipoPreguntaDto) {
|
||||||
|
return this.tipoPreguntaRepository.save(createTipoPreguntaDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
findAll() {
|
||||||
|
return this.tipoPreguntaRepository.find();
|
||||||
|
}
|
||||||
|
|
||||||
|
findOne(id: number) {
|
||||||
|
return this.tipoPreguntaRepository.findOne({
|
||||||
|
where: { id_tipo: id }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
update(id: number, updateTipoPreguntaDto: UpdateTipoPreguntaDto) {
|
||||||
|
return this.tipoPreguntaRepository.update(id, updateTipoPreguntaDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(id: number) {
|
||||||
|
return this.tipoPreguntaRepository.delete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ValidadorRespuestasService } from './validador-respuestas.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [ValidadorRespuestasService],
|
||||||
|
exports: [ValidadorRespuestasService]
|
||||||
|
})
|
||||||
|
export class ValidacionesModule {}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { validarRespuesta, ResultadoValidacion } from './validador';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Servicio para validar respuestas de cuestionarios
|
||||||
|
* Utiliza los validadores específicos según el tipo de validación requerido
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class ValidadorRespuestasService {
|
||||||
|
/**
|
||||||
|
* Valida una respuesta de acuerdo al tipo de validación de la pregunta
|
||||||
|
* @param respuesta Texto de la respuesta a validar
|
||||||
|
* @param tipoValidacion Tipo de validación ('correo', 'telefono', 'nombre', etc.)
|
||||||
|
* @returns Resultado de la validación (válido y mensaje)
|
||||||
|
*/
|
||||||
|
validarRespuesta(respuesta: string, tipoValidacion: string): ResultadoValidacion {
|
||||||
|
// Si no hay tipo de validación, la respuesta se considera válida
|
||||||
|
if (!tipoValidacion) {
|
||||||
|
return {
|
||||||
|
valido: true,
|
||||||
|
mensaje: 'No se requiere validación específica'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return validarRespuesta(respuesta, tipoValidacion);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valida múltiples respuestas basadas en sus tipos de validación
|
||||||
|
* @param respuestas Array de respuestas a validar
|
||||||
|
* @param tiposValidacion Array de tipos de validación correspondientes
|
||||||
|
* @returns Object con resultados de validación usando las posiciones como claves
|
||||||
|
*/
|
||||||
|
validarRespuestas(
|
||||||
|
respuestas: string[],
|
||||||
|
tiposValidacion: string[]
|
||||||
|
): { [key: number]: ResultadoValidacion } {
|
||||||
|
const resultados: { [key: number]: ResultadoValidacion } = {};
|
||||||
|
|
||||||
|
respuestas.forEach((respuesta, index) => {
|
||||||
|
if (index < tiposValidacion.length) {
|
||||||
|
resultados[index] = this.validarRespuesta(respuesta, tiposValidacion[index]);
|
||||||
|
} else {
|
||||||
|
// Si no hay un tipo de validación correspondiente, se asume válida
|
||||||
|
resultados[index] = {
|
||||||
|
valido: true,
|
||||||
|
mensaje: 'No se requiere validación específica'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return resultados;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Comprueba si todas las respuestas son válidas
|
||||||
|
* @param resultados Resultados de validación
|
||||||
|
* @returns true si todas las validaciones son correctas, false en caso contrario
|
||||||
|
*/
|
||||||
|
todasValidas(resultados: { [key: number]: ResultadoValidacion }): boolean {
|
||||||
|
return Object.values(resultados).every(resultado => resultado.valido);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,290 @@
|
|||||||
|
/**
|
||||||
|
* Módulo de validación para respuestas de cuestionarios
|
||||||
|
* Este módulo puede ser utilizado tanto en backend (NestJS) como en frontend (Angular, React, etc.)
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Interfaz para el resultado de validación
|
||||||
|
export interface ResultadoValidacion {
|
||||||
|
valido: boolean;
|
||||||
|
mensaje: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clase base para todos los validadores
|
||||||
|
export abstract class Validador {
|
||||||
|
abstract validar(valor: string): ResultadoValidacion;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clase para validar correos electrónicos
|
||||||
|
export class ValidadorCorreo extends Validador {
|
||||||
|
validar(valor: string): ResultadoValidacion {
|
||||||
|
if (!valor) {
|
||||||
|
return {
|
||||||
|
valido: false,
|
||||||
|
mensaje: 'El correo electrónico es requerido'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expresión regular para validar correos
|
||||||
|
// Valida el formato básico de correos: usuario@dominio.extensión
|
||||||
|
const regex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
|
||||||
|
|
||||||
|
if (!regex.test(valor)) {
|
||||||
|
return {
|
||||||
|
valido: false,
|
||||||
|
mensaje: 'El formato del correo electrónico no es válido'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Para correos institucionales (opcional)
|
||||||
|
if (this.validarDominioInstitucional) {
|
||||||
|
const dominios = ['acatlan.unam.mx', 'unam.mx', 'comunidad.unam.mx'];
|
||||||
|
const dominio = valor.split('@')[1];
|
||||||
|
|
||||||
|
if (!dominios.includes(dominio)) {
|
||||||
|
return {
|
||||||
|
valido: false,
|
||||||
|
mensaje: 'Debe ser un correo institucional (@acatlan.unam.mx, @unam.mx, @comunidad.unam.mx)'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valido: true,
|
||||||
|
mensaje: 'Correo electrónico válido'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Propiedad que permite configurar si se validan sólo correos institucionales
|
||||||
|
constructor(public validarDominioInstitucional: boolean = false) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clase para validar números telefónicos
|
||||||
|
export class ValidadorTelefono extends Validador {
|
||||||
|
validar(valor: string): ResultadoValidacion {
|
||||||
|
if (!valor) {
|
||||||
|
return {
|
||||||
|
valido: false,
|
||||||
|
mensaje: 'El número telefónico es requerido'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eliminar espacios, guiones y paréntesis para la validación
|
||||||
|
const numeroLimpio = valor.replace(/[\s\-()]/g, '');
|
||||||
|
|
||||||
|
// Verificar que solo contenga dígitos
|
||||||
|
if (!/^\d+$/.test(numeroLimpio)) {
|
||||||
|
return {
|
||||||
|
valido: false,
|
||||||
|
mensaje: 'El número telefónico solo debe contener dígitos'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar longitud (para México, 10 dígitos)
|
||||||
|
if (numeroLimpio.length !== 10) {
|
||||||
|
return {
|
||||||
|
valido: false,
|
||||||
|
mensaje: 'El número telefónico debe tener 10 dígitos'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valido: true,
|
||||||
|
mensaje: 'Número telefónico válido'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clase para validar nombres
|
||||||
|
export class ValidadorNombre extends Validador {
|
||||||
|
validar(valor: string): ResultadoValidacion {
|
||||||
|
const regex = new RegExp(
|
||||||
|
'/^[A-Za-zÁÉÍÓÚáéíóúÑñ]+(?: [A-Za-zÁÉÍÓÚáéíóúÑñ]+)*$/'
|
||||||
|
);
|
||||||
|
if (!valor) {
|
||||||
|
return {
|
||||||
|
valido: false,
|
||||||
|
mensaje: 'El nombre es requerido'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar longitud mínima
|
||||||
|
if (valor.trim().length < 2) {
|
||||||
|
return {
|
||||||
|
valido: false,
|
||||||
|
mensaje: 'El nombre debe tener al menos 2 caracteres'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar que solo contenga letras y espacios
|
||||||
|
if (regex.test(valor)) {
|
||||||
|
return {
|
||||||
|
valido: false,
|
||||||
|
mensaje: 'El nombre solo debe contener letras y espacios'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valido: true,
|
||||||
|
mensaje: 'Nombre válido'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clase para validar números enteros
|
||||||
|
export class ValidadorEntero extends Validador {
|
||||||
|
constructor(
|
||||||
|
private min: number = Number.MIN_SAFE_INTEGER,
|
||||||
|
private max: number = Number.MAX_SAFE_INTEGER
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
validar(valor: string): ResultadoValidacion {
|
||||||
|
if (!valor) {
|
||||||
|
return {
|
||||||
|
valido: false,
|
||||||
|
mensaje: 'El valor numérico es requerido'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar que sea un número entero
|
||||||
|
if (!/^-?\d+$/.test(valor)) {
|
||||||
|
return {
|
||||||
|
valido: false,
|
||||||
|
mensaje: 'El valor debe ser un número entero'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const numero = parseInt(valor, 10);
|
||||||
|
|
||||||
|
// Verificar rango
|
||||||
|
if (numero < this.min || numero > this.max) {
|
||||||
|
return {
|
||||||
|
valido: false,
|
||||||
|
mensaje: `El valor debe estar entre ${this.min} y ${this.max}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valido: true,
|
||||||
|
mensaje: 'Número entero válido'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clase para validar números decimales
|
||||||
|
export class ValidadorDecimal extends Validador {
|
||||||
|
constructor(
|
||||||
|
private min: number = Number.MIN_SAFE_INTEGER,
|
||||||
|
private max: number = Number.MAX_SAFE_INTEGER,
|
||||||
|
private decimales: number = 2
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
validar(valor: string): ResultadoValidacion {
|
||||||
|
if (!valor) {
|
||||||
|
return {
|
||||||
|
valido: false,
|
||||||
|
mensaje: 'El valor numérico es requerido'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar que sea un número decimal válido
|
||||||
|
if (!/^-?\d+(\.\d+)?$/.test(valor)) {
|
||||||
|
return {
|
||||||
|
valido: false,
|
||||||
|
mensaje: 'El valor debe ser un número decimal válido'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const numero = parseFloat(valor);
|
||||||
|
|
||||||
|
// Verificar rango
|
||||||
|
if (numero < this.min || numero > this.max) {
|
||||||
|
return {
|
||||||
|
valido: false,
|
||||||
|
mensaje: `El valor debe estar entre ${this.min} y ${this.max}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar precisión decimal
|
||||||
|
const partes = valor.split('.');
|
||||||
|
if (partes.length > 1 && partes[1].length > this.decimales) {
|
||||||
|
return {
|
||||||
|
valido: false,
|
||||||
|
mensaje: `El valor debe tener máximo ${this.decimales} decimales`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valido: true,
|
||||||
|
mensaje: 'Número decimal válido'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clase para validar cuentas de alumno (formato específico UNAM - 9 dígitos)
|
||||||
|
export class ValidadorCuentaAlumno extends Validador {
|
||||||
|
validar(valor: string): ResultadoValidacion {
|
||||||
|
if (!valor) {
|
||||||
|
return {
|
||||||
|
valido: false,
|
||||||
|
mensaje: 'La cuenta de alumno es requerida'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Formato de cuenta UNAM: 9 dígitos para todos los alumnos
|
||||||
|
if (!/^\d{9}$/.test(valor)) {
|
||||||
|
return {
|
||||||
|
valido: false,
|
||||||
|
mensaje: 'La cuenta de alumno debe tener exactamente 9 dígitos numéricos'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
valido: true,
|
||||||
|
mensaje: 'Cuenta de alumno válida'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fábrica de validadores para crear el validador apropiado según el tipo
|
||||||
|
export class FabricaValidadores {
|
||||||
|
static crear(tipo: string): Validador | null {
|
||||||
|
switch (tipo?.toLowerCase()) {
|
||||||
|
case 'correo':
|
||||||
|
return new ValidadorCorreo();
|
||||||
|
case 'correo_institucional':
|
||||||
|
return new ValidadorCorreo(true);
|
||||||
|
case 'telefono':
|
||||||
|
return new ValidadorTelefono();
|
||||||
|
case 'nombre':
|
||||||
|
return new ValidadorNombre();
|
||||||
|
case 'entero':
|
||||||
|
return new ValidadorEntero();
|
||||||
|
case 'decimal':
|
||||||
|
return new ValidadorDecimal();
|
||||||
|
case 'cuenta_alumno':
|
||||||
|
return new ValidadorCuentaAlumno();
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Función auxiliar para validar respuestas basada en el tipo de validación
|
||||||
|
export function validarRespuesta(respuesta: string, tipoValidacion: string): ResultadoValidacion {
|
||||||
|
const validador = FabricaValidadores.crear(tipoValidacion);
|
||||||
|
|
||||||
|
if (!validador) {
|
||||||
|
return {
|
||||||
|
valido: true, // Si no hay validador, consideramos válida la respuesta
|
||||||
|
mensaje: 'No se requiere validación específica'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return validador.validar(respuesta);
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
export const feriaSexualidad = {
|
||||||
|
nombre_form: 'Registro para la Feria de la Sexualidad - FES Acatlán',
|
||||||
|
descripcion:
|
||||||
|
'La Feria de la Sexualidad es un espacio seguro e informativo dirigido a toda la comunidad universitaria. Este registro nos ayudará a organizar mejor las actividades, talleres y charlas, así como conocer tus intereses. Por favor, completa el siguiente formulario para confirmar tu participación.',
|
||||||
|
fecha_inicio: '2025-03-07T00:00:01',
|
||||||
|
fecha_fin: '2025-03-18T23:59:59',
|
||||||
|
id_tipo_cuestionario: 1,
|
||||||
|
evento: 'Feria de la Sexualidad',
|
||||||
|
secciones: [
|
||||||
|
{
|
||||||
|
titulo: 'Información Personal',
|
||||||
|
descripcion:
|
||||||
|
'Proporciona tus datos personales para poder contactarte y enviarte información relevante sobre la Feria de la Sexualidad.',
|
||||||
|
preguntas: [
|
||||||
|
{
|
||||||
|
titulo: '¿Eres parte de la comunidad de la FES Acatlán?',
|
||||||
|
tipo: 'Cerrada',
|
||||||
|
opciones: [{ valor: 'Si' }, { valor: 'No' }],
|
||||||
|
obligatoria: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
titulo: 'Correo electrónico',
|
||||||
|
obligatoria: true,
|
||||||
|
tipo: 'Abierta',
|
||||||
|
limite: 250,
|
||||||
|
validacion: 'correo',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
titulo: 'Numero de cuenta',
|
||||||
|
obligatoria: true,
|
||||||
|
tipo: 'Abierta',
|
||||||
|
limite: 250,
|
||||||
|
validacion: 'cuenta_alumno',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
titulo: 'Nombre(s)',
|
||||||
|
obligatoria: true,
|
||||||
|
tipo: 'Abierta',
|
||||||
|
limite: 250,
|
||||||
|
validacion: 'nombre',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
titulo: 'Apellidos',
|
||||||
|
obligatoria: true,
|
||||||
|
tipo: 'Abierta',
|
||||||
|
limite: 250,
|
||||||
|
validacion: 'nombre',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
titulo: 'Género',
|
||||||
|
obligatoria: true,
|
||||||
|
tipo: 'Cerrada',
|
||||||
|
opciones: [
|
||||||
|
{ valor: 'Masculino' },
|
||||||
|
{ valor: 'Femenino' },
|
||||||
|
{ valor: 'Prefiero no decirlo' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
titulo: 'Institución de procedencia',
|
||||||
|
tipo: 'Abierta',
|
||||||
|
obligatoria: true,
|
||||||
|
limite: 250,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
export const cuestionario_feria = {
|
||||||
|
tipo_cuestionario: {
|
||||||
|
id_tipo_cuestionario: 1,
|
||||||
|
tipo_cuestionario: 'Encuesta',
|
||||||
|
},
|
||||||
|
cuestionario: {
|
||||||
|
id_cuestionario: 10,
|
||||||
|
nombre_form:
|
||||||
|
'Registro para la Feria de la Sexualidad - Comunidad FES Acatlán',
|
||||||
|
contador_secciones: 2,
|
||||||
|
descripcion:
|
||||||
|
'La Feria de la Sexualidad es un espacio seguro e informativo dirigido a toda la comunidad universitaria. Este registro nos ayudará a organizar mejor las actividades, talleres y charlas, así como conocer tus intereses. Por favor, completa el siguiente formulario para confirmar tu participación.',
|
||||||
|
editable: true,
|
||||||
|
fecha_inicio: '2025-03-26T00:00:00',
|
||||||
|
fecha_fin: '2025-03-31T23:59:59',
|
||||||
|
id_cuestionario_original: null,
|
||||||
|
id_tipo_cuestionario: 1,
|
||||||
|
secciones: [
|
||||||
|
{
|
||||||
|
id_cuestionario_seccion: 1,
|
||||||
|
posicion: 1,
|
||||||
|
seccion: {
|
||||||
|
id_seccion: 100,
|
||||||
|
contador_pregunta: 2,
|
||||||
|
descripcion: 'Preguntas generales',
|
||||||
|
titulo: 'General',
|
||||||
|
},
|
||||||
|
preguntas: [
|
||||||
|
{
|
||||||
|
id_seccion_pregunta: 1000,
|
||||||
|
posicion: 1,
|
||||||
|
pregunta: {
|
||||||
|
id_pregunta: 10000,
|
||||||
|
pregunta: '¿Cómo calificaría nuestro servicio?',
|
||||||
|
contador_opcion: 4,
|
||||||
|
obligatoria: true,
|
||||||
|
id_tipo_pregunta: 1,
|
||||||
|
id_opcion_dependiente: null,
|
||||||
|
tipo_pregunta: {
|
||||||
|
id_tipo: 1,
|
||||||
|
tipo_pregunta: 'Cerrada',
|
||||||
|
},
|
||||||
|
opciones: [
|
||||||
|
{
|
||||||
|
id_pregunta_opcion: 50000,
|
||||||
|
posicion: 1,
|
||||||
|
id_opcion: 90000,
|
||||||
|
opcion: {
|
||||||
|
id_opcion: 90000,
|
||||||
|
opcion: 'Excelente',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id_pregunta_opcion: 50001,
|
||||||
|
posicion: 2,
|
||||||
|
id_opcion: 90001,
|
||||||
|
opcion: {
|
||||||
|
id_opcion: 90001,
|
||||||
|
opcion: 'Bueno',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id_pregunta_opcion: 50002,
|
||||||
|
posicion: 3,
|
||||||
|
id_opcion: 90002,
|
||||||
|
opcion: {
|
||||||
|
id_opcion: 90002,
|
||||||
|
opcion: 'Regular',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id_pregunta_opcion: 50003,
|
||||||
|
posicion: 4,
|
||||||
|
id_opcion: 90003,
|
||||||
|
opcion: {
|
||||||
|
id_opcion: 90003,
|
||||||
|
opcion: 'Malo',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id_seccion_pregunta: 1001,
|
||||||
|
posicion: 2,
|
||||||
|
pregunta: {
|
||||||
|
id_pregunta: 10001,
|
||||||
|
pregunta: '¿Recomendaría nuestro servicio?',
|
||||||
|
contador_opcion: 2,
|
||||||
|
obligatoria: true,
|
||||||
|
id_tipo_pregunta: 1,
|
||||||
|
id_opcion_dependiente: null,
|
||||||
|
tipo_pregunta: {
|
||||||
|
id_tipo: 1,
|
||||||
|
tipo_pregunta: 'Cerrada',
|
||||||
|
},
|
||||||
|
opciones: [
|
||||||
|
{
|
||||||
|
id_pregunta_opcion: 50004,
|
||||||
|
posicion: 1,
|
||||||
|
id_opcion: 90004,
|
||||||
|
opcion: {
|
||||||
|
id_opcion: 90004,
|
||||||
|
opcion: 'Sí',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id_pregunta_opcion: 50005,
|
||||||
|
posicion: 2,
|
||||||
|
id_opcion: 90005,
|
||||||
|
opcion: {
|
||||||
|
id_opcion: 90005,
|
||||||
|
opcion: 'No',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id_cuestionario_seccion: 2,
|
||||||
|
posicion: 2,
|
||||||
|
seccion: {
|
||||||
|
id_seccion: 101,
|
||||||
|
contador_pregunta: 1,
|
||||||
|
descripcion: 'Preguntas adicionales para conocer más detalles',
|
||||||
|
titulo: 'Adicionales',
|
||||||
|
},
|
||||||
|
preguntas: [
|
||||||
|
{
|
||||||
|
id_seccion_pregunta: 1002,
|
||||||
|
posicion: 1,
|
||||||
|
pregunta: {
|
||||||
|
id_pregunta: 10002,
|
||||||
|
pregunta: '¿Qué mejorarías en nuestro servicio?',
|
||||||
|
contador_opcion: 0,
|
||||||
|
obligatoria: false,
|
||||||
|
id_tipo_pregunta: 2,
|
||||||
|
id_opcion_dependiente: null,
|
||||||
|
tipo_pregunta: {
|
||||||
|
id_tipo: 2,
|
||||||
|
tipo_pregunta: 'Abierta',
|
||||||
|
},
|
||||||
|
opciones: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id_seccion_pregunta: 1003,
|
||||||
|
posicion: 3,
|
||||||
|
pregunta: {
|
||||||
|
id_pregunta: 10003,
|
||||||
|
pregunta: '¿Qué aspectos del servicio fueron de tu agrado?',
|
||||||
|
contador_opcion: 3,
|
||||||
|
obligatoria: false,
|
||||||
|
id_tipo_pregunta: 3, // Puedes usar este ID para distinguir múltiples
|
||||||
|
id_opcion_dependiente: null,
|
||||||
|
tipo_pregunta: {
|
||||||
|
id_tipo: 3,
|
||||||
|
tipo_pregunta: 'Multiple',
|
||||||
|
},
|
||||||
|
opciones: [
|
||||||
|
{
|
||||||
|
id_pregunta_opcion: 50006,
|
||||||
|
posicion: 1,
|
||||||
|
id_opcion: 90006,
|
||||||
|
opcion: {
|
||||||
|
id_opcion: 90006,
|
||||||
|
opcion: 'Rapidez',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id_pregunta_opcion: 50007,
|
||||||
|
posicion: 2,
|
||||||
|
id_opcion: 90007,
|
||||||
|
opcion: {
|
||||||
|
id_opcion: 90007,
|
||||||
|
opcion: 'Atención al cliente',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id_pregunta_opcion: 50008,
|
||||||
|
posicion: 3,
|
||||||
|
id_opcion: 90008,
|
||||||
|
opcion: {
|
||||||
|
id_opcion: 90008,
|
||||||
|
opcion: 'Facilidad de uso',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
// Script para cargar un formulario desde un archivo JSON
|
||||||
|
|
||||||
|
const axios = require('axios');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const API_URL = 'http://localhost:4200';
|
||||||
|
|
||||||
|
async function loadFormFromFile(filePath) {
|
||||||
|
try {
|
||||||
|
// Leer el archivo JSON
|
||||||
|
const formData = require(filePath);
|
||||||
|
|
||||||
|
console.log('Enviando datos del formulario al servidor...');
|
||||||
|
console.log('Datos:', JSON.stringify(formData, null, 2));
|
||||||
|
|
||||||
|
// Enviar los datos al endpoint
|
||||||
|
const response = await axios.post(`${API_URL}/cuestionario`, formData);
|
||||||
|
|
||||||
|
console.log('Formulario creado exitosamente:');
|
||||||
|
console.log(JSON.stringify(response.data, null, 2));
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error al crear el formulario:');
|
||||||
|
if (error.response) {
|
||||||
|
console.error('Status:', error.response.status);
|
||||||
|
console.error('Data:', JSON.stringify(error.response.data, null, 2));
|
||||||
|
} else {
|
||||||
|
console.error(error.message);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verificar si se proporcionó un argumento de archivo
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
const filePath = args[0] || './crear_formulario_feria';
|
||||||
|
|
||||||
|
// Ejecutar la función
|
||||||
|
loadFormFromFile(filePath)
|
||||||
|
.then(() => console.log('Proceso completado'))
|
||||||
|
.catch(() => {
|
||||||
|
console.log('Proceso fallido');
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
const comunidad = {
|
||||||
|
id_formulario: 1,
|
||||||
|
respuestas: [
|
||||||
|
{ id_pregunta: 101, valor: 'Si' }, // ¿Eres parte de la comunidad de la FES Acatlán?
|
||||||
|
{ id_pregunta: 102, valor: '123456789' }, // Numero de cuenta
|
||||||
|
{ id_pregunta: 106, valor: 'Prefiero no decirlo' },
|
||||||
|
],
|
||||||
|
fecha_envio: '2025-04-02T13:45:00',
|
||||||
|
};
|
||||||
|
|
||||||
|
const externos = {
|
||||||
|
id_formulario: 1,
|
||||||
|
respuestas: [
|
||||||
|
{ id_pregunta: 101, valor: 'No' }, // ¿Eres parte de la comunidad de la FES Acatlán?
|
||||||
|
{ id_pregunta: 103, valor: 'Juan Carlos' }, // Nombre(s)
|
||||||
|
{ id_pregunta: 104, valor: 'Ramírez López' }, // Apellidos
|
||||||
|
{ id_pregunta: 106, valor: 'Prefiero no decirlo' }, // genero
|
||||||
|
{ id_pregunta: 107, valor: 'UAM Azcapotzalco' }, // Institución de procedencia
|
||||||
|
],
|
||||||
|
fecha_envio: '2025-04-02T13:45:00',
|
||||||
|
};
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { feriaSexualidad } from './crear_formulario_feria';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
// Asumiendo que el servidor está corriendo en localhost:4200
|
||||||
|
const API_URL = 'http://localhost:4200';
|
||||||
|
|
||||||
|
async function crearFormulario() {
|
||||||
|
try {
|
||||||
|
const response = await axios.post(`${API_URL}/cuestionario`, feriaSexualidad);
|
||||||
|
console.log('Formulario creado exitosamente:');
|
||||||
|
console.log(JSON.stringify(response.data, null, 2));
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error al crear el formulario:');
|
||||||
|
if (error.response) {
|
||||||
|
console.error('Status:', error.response.status);
|
||||||
|
console.error('Data:', error.response.data);
|
||||||
|
} else {
|
||||||
|
console.error(error.message);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ejecutar la función
|
||||||
|
crearFormulario()
|
||||||
|
.then(() => console.log('Proceso completado'))
|
||||||
|
.catch(() => console.log('Proceso fallido'));
|
||||||
Reference in New Issue
Block a user