diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..9619991 --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/README.md b/README.md index c17103c..35f1236 100644 --- a/README.md +++ b/README.md @@ -97,3 +97,25 @@ Nest is an MIT-licensed open source project. It can grow thanks to the sponsors ## License Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE). + + + +# usar validadores en el front end + + import { validarRespuesta, FabricaValidadores } from + './validaciones/validador'; + + // To validate a single response + const respuesta = 'user@example.com'; + const tipoValidacion = 'correo'; + const resultado = validarRespuesta(respuesta, tipoValidacion); + + if (resultado.valido) { + // proceed + } else { + // show error message: resultado.mensaje + } + + // Or directly use the validators + const validadorCorreo = new ValidadorCorreo(); + const resultadoCorreo = validadorCorreo.validar('user@example.com'); \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..d933344 --- /dev/null +++ b/docker-compose.yml @@ -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: diff --git a/env.example.txt b/env.example.txt new file mode 100644 index 0000000..14564ff --- /dev/null +++ b/env.example.txt @@ -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 \ No newline at end of file diff --git a/init-scripts/init-alumnosdb.sql b/init-scripts/init-alumnosdb.sql new file mode 100644 index 0000000..8a179e0 --- /dev/null +++ b/init-scripts/init-alumnosdb.sql @@ -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'); \ No newline at end of file diff --git a/notas_santi.txt b/notas_santi.txt new file mode 100644 index 0000000..eca430e --- /dev/null +++ b/notas_santi.txt @@ -0,0 +1,128 @@ +import { Module } from '@nestjs/common'; +import { AppController } from './app.controller'; +import { AppService } from './app.service'; +import { AdminModule } from './admin/admin.module'; +import {TypeOrmModule } from '@nestjs/typeorm' +import { PostsModule } from './posts/posts.module'; +import { TipoUserModule } from './tipo_user/tipo_user.module'; +import { EventoModule } from './evento/evento.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'; + + +@Module({ + imports: [ + TypeOrmModule.forRoot({ + type: 'mysql', + host: 'localhost', + port: 3306, //3306 + username: 'root', + password: 'admin', //admin + database: 'nestdb', + entities: [__dirname + '/**/*.entity{.ts,.js}'], + synchronize: true, + //extra + retryAttempts: 10, // Intentos para reconectar + retryDelay: 3000, // Tiempo entre reintentos + }), + AdminModule, + PostsModule, + EventoModule, + TipoUserModule, + ParticipanteModule, + QrModule, + AdministradorModule, + AsistenciaModule, + ParticipanteEventoModule, + ], + controllers: [AppController], + providers: [AppService], +}) +export class AppModule {} + +// + +{ + "name": "nestjs-mysql", + "version": "0.0.1", + "description": "", + "author": "", + "private": true, + "license": "UNLICENSED", + "scripts": { + "build": "nest build", + "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", + "start": "nest start", + "start:dev": "nest start --watch", + "start:debug": "nest start --debug --watch", + "start:prod": "node dist/main", + "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", + "test": "jest", + "test:watch": "jest --watch", + "test:cov": "jest --coverage", + "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", + "test:e2e": "jest --config ./test/jest-e2e.json" + }, + "dependencies": { + "@nestjs/common": "^11.0.1", + "@nestjs/core": "^11.0.1", + "@nestjs/platform-express": "^11.0.1", + "@nestjs/swagger": "^11.1.0", + "@nestjs/typeorm": "^11.0.0", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.1", + "jsonwebtoken": "^9.0.2", + "mysql2": "^3.13.0", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.1", + "swagger-ui-express": "^5.0.1", + "typeorm": "^0.3.21" + }, + "devDependencies": { + "@eslint/eslintrc": "^3.2.0", + "@eslint/js": "^9.18.0", + "@nestjs/cli": "^11.0.0", + "@nestjs/schematics": "^11.0.0", + "@nestjs/testing": "^11.0.1", + "@swc/cli": "^0.6.0", + "@swc/core": "^1.10.7", + "@types/express": "^5.0.0", + "@types/jest": "^29.5.14", + "@types/node": "^22.10.7", + "@types/supertest": "^6.0.2", + "eslint": "^9.18.0", + "eslint-config-prettier": "^10.0.1", + "eslint-plugin-prettier": "^5.2.2", + "globals": "^16.0.0", + "jest": "^29.7.0", + "prettier": "^3.4.2", + "source-map-support": "^0.5.21", + "supertest": "^7.0.0", + "ts-jest": "^29.2.5", + "ts-loader": "^9.5.2", + "ts-node": "^10.9.2", + "tsconfig-paths": "^4.2.0", + "typescript": "^5.7.3", + "typescript-eslint": "^8.20.0" + }, + "jest": { + "moduleFileExtensions": [ + "js", + "json", + "ts" + ], + "rootDir": "src", + "testRegex": ".*\\.spec\\.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + }, + "collectCoverageFrom": [ + "**/*.(t|j)s" + ], + "coverageDirectory": "../coverage", + "testEnvironment": "node" + } +} diff --git a/package-lock.json b/package-lock.json index d47efe7..8fc828d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,13 +9,19 @@ "version": "0.0.1", "license": "UNLICENSED", "dependencies": { - "@nestjs/common": "^11.0.1", + "@nestjs/common": "^11.0.12", "@nestjs/config": "^4.0.2", "@nestjs/core": "^11.0.1", "@nestjs/mapped-types": "*", "@nestjs/platform-express": "^11.0.1", + "@nestjs/swagger": "^11.1.0", "@nestjs/typeorm": "^11.0.0", + "axios": "^1.8.4", + "class-transformer": "^0.5.1", "class-validator": "^0.14.1", + "dotenv": "^16.4.7", + "mysql2": "^3.14.0", + "qrcode": "^1.5.4", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", "typeorm": "^0.3.21" @@ -31,6 +37,7 @@ "@types/express": "^5.0.0", "@types/jest": "^29.5.14", "@types/node": "^22.10.7", + "@types/qrcode": "^1.5.5", "@types/supertest": "^6.0.2", "eslint": "^9.18.0", "eslint-config-prettier": "^10.0.1", @@ -44,7 +51,7 @@ "ts-loader": "^9.5.2", "ts-node": "^10.9.2", "tsconfig-paths": "^4.2.0", - "typescript": "^5.7.3", + "typescript": "^5.8.2", "typescript-eslint": "^8.20.0" } }, @@ -1906,6 +1913,12 @@ "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": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@napi-rs/nice/-/nice-1.0.1.tgz", @@ -2495,6 +2508,39 @@ "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": { "version": "11.0.12", "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-11.0.12.tgz", @@ -2613,6 +2659,13 @@ "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": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", @@ -3231,6 +3284,16 @@ "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": { "version": "6.9.18", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", @@ -4115,7 +4178,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, "license": "Python-2.0" }, "node_modules/array-timsort": { @@ -4143,9 +4205,28 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, "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": { "version": "1.6.7", "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", @@ -4636,7 +4717,6 @@ "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -4746,6 +4826,12 @@ "dev": true, "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": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.1.tgz", @@ -4911,7 +4997,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "license": "MIT", "dependencies": { "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": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -5242,12 +5336,20 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "license": "MIT", "engines": { "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": { "version": "2.0.0", "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_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": { "version": "16.4.7", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", @@ -5477,7 +5585,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -6255,6 +6362,26 @@ "dev": true, "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": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -6368,7 +6495,6 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", - "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -6394,7 +6520,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -6404,7 +6529,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "license": "MIT", "dependencies": { "mime-db": "1.52.0" @@ -6499,6 +6623,15 @@ "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": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -6749,7 +6882,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -7077,6 +7209,12 @@ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "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": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -7912,7 +8050,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -8125,6 +8262,12 @@ "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": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", @@ -8148,6 +8291,21 @@ "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": { "version": "0.30.17", "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_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": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -8733,7 +8944,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -8790,7 +9000,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -9008,6 +9217,15 @@ "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": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -9108,6 +9326,12 @@ "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": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -9135,6 +9359,148 @@ ], "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": { "version": "6.13.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", @@ -9307,6 +9673,12 @@ "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": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -9637,6 +10009,11 @@ "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": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", @@ -9662,6 +10039,12 @@ "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": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -9883,6 +10266,15 @@ "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": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -10220,6 +10612,15 @@ "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": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", @@ -11459,6 +11860,12 @@ "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": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -11473,7 +11880,6 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -11527,7 +11933,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -11537,7 +11942,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" diff --git a/package.json b/package.json index b3ae839..29753f5 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "scripts": { "build": "nest build", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", - "start": "nest start", + "start": "source ~/.nvm/nvm.sh && nvm use 22.10.0 && npm install && nest start", "start:dev": "nest start --watch", "start:debug": "nest start --debug --watch", "start:prod": "node dist/main", @@ -20,13 +20,19 @@ "test:e2e": "jest --config ./test/jest-e2e.json" }, "dependencies": { - "@nestjs/common": "^11.0.1", + "@nestjs/common": "^11.0.12", "@nestjs/config": "^4.0.2", "@nestjs/core": "^11.0.1", "@nestjs/mapped-types": "*", "@nestjs/platform-express": "^11.0.1", + "@nestjs/swagger": "^11.1.0", "@nestjs/typeorm": "^11.0.0", + "axios": "^1.8.4", + "class-transformer": "^0.5.1", "class-validator": "^0.14.1", + "dotenv": "^16.4.7", + "mysql2": "^3.14.0", + "qrcode": "^1.5.4", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", "typeorm": "^0.3.21" @@ -42,6 +48,7 @@ "@types/express": "^5.0.0", "@types/jest": "^29.5.14", "@types/node": "^22.10.7", + "@types/qrcode": "^1.5.5", "@types/supertest": "^6.0.2", "eslint": "^9.18.0", "eslint-config-prettier": "^10.0.1", @@ -55,7 +62,7 @@ "ts-loader": "^9.5.2", "ts-node": "^10.9.2", "tsconfig-paths": "^4.2.0", - "typescript": "^5.7.3", + "typescript": "^5.8.2", "typescript-eslint": "^8.20.0" }, "jest": { diff --git a/src/administrador/administrador.controller.ts b/src/administrador/administrador.controller.ts new file mode 100644 index 0000000..f0b8cb0 --- /dev/null +++ b/src/administrador/administrador.controller.ts @@ -0,0 +1,57 @@ +import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post } from '@nestjs/common'; +import { AdministradorService } from './administrador.service'; +import { Administrador } from './administrador.entity'; +import { CreateAdministradorDto } from './dto/create-administrador.dto'; +import { UpdateAdministradorDto } from './dto/update.administrador.dto'; +import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiBody } from '@nestjs/swagger'; + +@ApiTags('Administradores') // Agrupa los endpoints en Swagger +@Controller('administrador') +export class AdministradorController { + + constructor(private administradorService: AdministradorService) {} + + @Get() + @ApiOperation({ summary: 'Obtener todos los administradores' }) + @ApiResponse({ status: 200, description: 'Lista de administradores obtenida correctamente.' }) + getAdministradores(): Promise { + return this.administradorService.getAdministradores(); + } + + @Get(':id') + @ApiOperation({ summary: 'Obtener un administrador por ID' }) + @ApiParam({ name: 'id', description: 'ID del administrador', example: 1 }) + @ApiResponse({ status: 200, description: 'Administrador obtenido correctamente.' }) + @ApiResponse({ status: 404, description: 'Administrador no encontrado.' }) + getAdministrador(@Param('id', ParseIntPipe) id: number) { + return this.administradorService.getAdministrador(id) + } + + @Post() + @ApiOperation({ summary: 'Registrar un nuevo administrador' }) + @ApiBody({ + description: 'Datos del administrador a registrar', + schema: { + type: 'object', + properties: { + id_tipo_user: { type: 'integer', example: 1 } + } + } + }) + @ApiResponse({ status: 201, description: 'Administrador registrado exitosamente.' }) + @ApiResponse({ status: 400, description: 'Datos inválidos.' }) + createAdministrador(@Body() newAdministrador: CreateAdministradorDto) { + return this.administradorService.createAdministrador(newAdministrador) + } + + @Delete(':id') + deleteAdministrador(@Param('id', ParseIntPipe) id: number) { + return this.administradorService.deleteAdministrador(id) + } + + @Patch(':id') + updateAdministrador(@Param('id', ParseIntPipe) id: number, @Body() administrador: UpdateAdministradorDto) { + return this.administradorService.updateAdministrador(id, administrador) + } + +} diff --git a/src/administrador/administrador.entity.ts b/src/administrador/administrador.entity.ts new file mode 100644 index 0000000..483ba96 --- /dev/null +++ b/src/administrador/administrador.entity.ts @@ -0,0 +1,28 @@ +import { TipoUser } from "src/tipo_user/tipo_user.entity"; +import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from "typeorm"; + +@Entity() +export class Administrador { + @PrimaryGeneratedColumn() + id_admnistrador: number + + /* + @ManyToOne(() => TipoUser, (tipoUser) => tipoUser.administradores) + @JoinColumn({ name: "id_tipo_user" }) + tipoUser: TipoUser; + */ + //Relacion con tipo_user + @Column() + id_tipo_user: number + + @ManyToOne(() => TipoUser, (tipoUser) => tipoUser.administrador) + tipoUser: TipoUser[] + + /* + @OneToMany(() => Evento, (evento) => evento.administrador) + eventos: Evento[]; + + @OneToMany(() => Asistencia, (asistencia) => asistencia.administrador) + asistencias: Asistencia[]; + */ +} \ No newline at end of file diff --git a/src/administrador/administrador.module.ts b/src/administrador/administrador.module.ts new file mode 100644 index 0000000..17ed104 --- /dev/null +++ b/src/administrador/administrador.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { AdministradorService } from './administrador.service'; +import { AdministradorController } from './administrador.controller'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Administrador } from './administrador.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([Administrador])], + controllers: [AdministradorController], + providers: [AdministradorService], + exports: [AdministradorService], +}) +export class AdministradorModule {} diff --git a/src/administrador/administrador.service.ts b/src/administrador/administrador.service.ts new file mode 100644 index 0000000..2dce554 --- /dev/null +++ b/src/administrador/administrador.service.ts @@ -0,0 +1,79 @@ +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Administrador } from './administrador.entity'; +import { Repository } from 'typeorm'; +import { CreateAdministradorDto } from './dto/create-administrador.dto'; +import { UpdateAdministradorDto } from './dto/update.administrador.dto'; + +@Injectable() +export class AdministradorService { + + constructor( + @InjectRepository(Administrador) private administradorRepository: Repository + ) {} + + async createAdministrador(administrador: CreateAdministradorDto) { + + //revisar el where + const administradorFound = await this.administradorRepository.findOne({ + where: { + id_admnistrador: administrador.id_tipo_user + } + }) + + if (administradorFound) { + return new HttpException('Administrador already exists', HttpStatus.CONFLICT) + } + + + //Falta regresar el return + //return this.administradorRepository.save(administradorFound) + } + + getAdministradores() { + return this.administradorRepository.find({ + relations: ['tipoUser'] + }) + } + + async getAdministrador(id_admnistrador: number) { + const administradorFound = await this.administradorRepository.findOne({ + where: { + id_admnistrador + }, + relations: ['tipoUser'] + }) + + if (!Administrador) { + return new HttpException('User not found', HttpStatus.NOT_FOUND); + } + + return administradorFound + } + + async deleteAdministrador(id_admnistrador: number) { + const result = await this.administradorRepository.delete({ id_admnistrador }) + + if (result.affected === 0) { + return new HttpException('User not found', HttpStatus.NOT_FOUND); + } + + return result + } + + async updateAdministrador(id_admnistrador: number, administrador: UpdateAdministradorDto) { + const administradorFound = await this.administradorRepository.findOne({ + where: { + id_admnistrador + } + }) + + if (!administradorFound) { + return new HttpException('User not found', HttpStatus.NOT_FOUND) + } + + const updateAdministrador = Object.assign(administradorFound, administrador) + return this.administradorRepository.save(updateAdministrador) + } + +} diff --git a/src/administrador/dto/create-administrador.dto.ts b/src/administrador/dto/create-administrador.dto.ts new file mode 100644 index 0000000..fedb2b3 --- /dev/null +++ b/src/administrador/dto/create-administrador.dto.ts @@ -0,0 +1,3 @@ +export class CreateAdministradorDto { + id_tipo_user: number +} \ No newline at end of file diff --git a/src/administrador/dto/update.administrador.dto.ts b/src/administrador/dto/update.administrador.dto.ts new file mode 100644 index 0000000..f1355f2 --- /dev/null +++ b/src/administrador/dto/update.administrador.dto.ts @@ -0,0 +1,3 @@ +export class UpdateAdministradorDto { + id_tipo_user?: number +} \ No newline at end of file diff --git a/src/alumnos/alumnos.controller.ts b/src/alumnos/alumnos.controller.ts new file mode 100644 index 0000000..61b8a9c --- /dev/null +++ b/src/alumnos/alumnos.controller.ts @@ -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 { + const alumno = await this.alumnosService.findByCuenta(cuenta); + if (!alumno) { + throw new NotFoundException(`No se encontró alumno con número de cuenta ${cuenta}`); + } + return alumno; + } +} diff --git a/src/alumnos/alumnos.documentation.ts b/src/alumnos/alumnos.documentation.ts new file mode 100644 index 0000000..1c56307 --- /dev/null +++ b/src/alumnos/alumnos.documentation.ts @@ -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', + }, +}; \ No newline at end of file diff --git a/src/alumnos/alumnos.module.ts b/src/alumnos/alumnos.module.ts new file mode 100644 index 0000000..d5f26ba --- /dev/null +++ b/src/alumnos/alumnos.module.ts @@ -0,0 +1,26 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { AlumnosController } from './alumnos.controller'; +import { AlumnosService } from './alumnos.service'; +import { RegistroAlumno } from './entities/registro-alumno.entity'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([RegistroAlumno], 'alumnosConnection'), + TypeOrmModule.forRoot({ + name: 'alumnosConnection', + type: 'mysql', + host: process.env.ALUMNOS_DB_HOST || 'mariadb', + port: parseInt(process.env.ALUMNOS_DB_PORT || '3306'), + username: process.env.ALUMNOS_DB_USERNAME || 'alumnosuser', + password: process.env.ALUMNOS_DB_PASSWORD || 'alumnospass', + database: process.env.ALUMNOS_DB_DATABASE || 'datos_feria_registro', + entities: [RegistroAlumno], + synchronize: false, + }), + ], + controllers: [AlumnosController], + providers: [AlumnosService], + exports: [AlumnosService], +}) +export class AlumnosModule {} diff --git a/src/alumnos/alumnos.service.ts b/src/alumnos/alumnos.service.ts new file mode 100644 index 0000000..bda130a --- /dev/null +++ b/src/alumnos/alumnos.service.ts @@ -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, + ) {} + + async findByCuenta(cuenta: string): Promise { + return this.registroAlumnoRepository.findOne({ + where: { id_ncuenta: parseInt(cuenta, 10) }, + }); + } +} diff --git a/src/alumnos/entities/registro-alumno.entity.ts b/src/alumnos/entities/registro-alumno.entity.ts new file mode 100644 index 0000000..fa0568f --- /dev/null +++ b/src/alumnos/entities/registro-alumno.entity.ts @@ -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; +} diff --git a/src/app.module.ts b/src/app.module.ts index 3d16193..78b893c 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,3 +1,4 @@ +//import { Module } from '@nestjs/common; import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; @@ -15,14 +16,24 @@ import { CuestionarioRespondidoModule } from './cuestionario_respondido/cuestion import { RespuestaParticipanteAbiertaModule } from './respuesta_participante_abierta/respuesta_participante_abierta.module'; import { TypeOrmModule } from '@nestjs/typeorm'; import { ConfigModule } from '@nestjs/config'; +import { EventoModule } from './evento/evento.module'; +import { TipoUserModule } from './tipo_user/tipo_user.module'; +import { ParticipanteModule } from './participante/participante.module'; +import { QrModule } from './qr/qr.module'; +import { AdministradorModule } from './administrador/administrador.module'; +import { AsistenciaModule } from './asistencia/asistencia.module'; +import { ParticipanteEventoModule } from './participante_evento/participante_evento.module'; +import { ValidacionesModule } from './validaciones/validaciones.module'; +import { AlumnosModule } from './alumnos/alumnos.module'; @Module({ imports: [ ConfigModule.forRoot(), TypeOrmModule.forRoot({ + type: 'mysql', - host: process.env.db_host, + host: process.env.db_host, //process.env.db_host username: process.env.db_username, database: process.env.db_database, password: process.env.db_password, @@ -32,6 +43,23 @@ import { ConfigModule } from '@nestjs/config'; // logging: true, // Habilita los logs para depuración autoLoadEntities: true, // Carga automáticamente las entidades + //esta es mi base de datos local + + /* + type: 'mysql', + host: 'localhost', + port: 3306, //3306 + username: 'root', + password: 'admin', //admin + database: 'cidwa', //nestdb + */ + // entities: [__dirname + '/**/*.entity{.ts,.js}'], + // synchronize: true, + + + //extra + retryAttempts: 10, // Intentos para reconectar + retryDelay: 3000, // Tiempo entre reintentos }), CuestionarioModule, TipoCuestionarioModule, @@ -43,7 +71,17 @@ import { ConfigModule } from '@nestjs/config'; OpcionModule, PreguntaOpcionModule, RespuestaParticipanteCerradaModule, CuestionarioRespondidoModule, - RespuestaParticipanteAbiertaModule], + RespuestaParticipanteAbiertaModule, + EventoModule, + TipoUserModule, + ParticipanteModule, + QrModule, + AdministradorModule, + AsistenciaModule, + ParticipanteEventoModule, + ValidacionesModule, + AlumnosModule, + ], controllers: [AppController], providers: [AppService], diff --git a/src/asistencia/asistencia.controller.ts b/src/asistencia/asistencia.controller.ts new file mode 100644 index 0000000..1107ab8 --- /dev/null +++ b/src/asistencia/asistencia.controller.ts @@ -0,0 +1,37 @@ +import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post } from '@nestjs/common'; +import { AsistenciaService } from './asistencia.service'; +import { Asistencia } from './asistencia.entity'; +import { CreateAsistenciaDto } from './dto/create-asistencia.dto'; +import { UpdateAsistenciaDto } from './dto/update.asistencia.dto'; + +@Controller('asistencia') +export class AsistenciaController { + + constructor(private asistenciaService: AsistenciaService) {} + + @Get() + getAsistencias(): Promise { + return this.asistenciaService.getAsistencias() + } + + @Get(':id') + getAsistencia(@Param('id', ParseIntPipe) id: number) { + return this.asistenciaService.getAsistencia(id) + } + + @Post() + createAsistencia(@Body() newAsistencia: CreateAsistenciaDto) { + return this.asistenciaService.createAsistencia(newAsistencia) + } + + @Delete() + deleteAsistencia(@Param('id', ParseIntPipe) id: number) { + return this.asistenciaService.deleteAsistencia(id) + } + + @Patch(':id') + updateAsistencia(@Param('id', ParseIntPipe) id: number, @Body() asistencia: UpdateAsistenciaDto) { + return this.asistenciaService.updateAsistencia(id, asistencia) + } + +} diff --git a/src/asistencia/asistencia.entity.ts b/src/asistencia/asistencia.entity.ts new file mode 100644 index 0000000..83c5dc5 --- /dev/null +++ b/src/asistencia/asistencia.entity.ts @@ -0,0 +1,41 @@ +import { Column, Entity, PrimaryGeneratedColumn } from "typeorm"; + +@Entity() +export class Asistencia { + @PrimaryGeneratedColumn() + id_asistecia: number + + //({type: 'datetime', default: () => 'CURRENT_TIMESTAMP' } + @Column({type: 'datetime', default: () => 'CURRENT_TIMESTAMP'}) + fecha_asistencia: Date + + @Column() + metodo: boolean + + @Column() + estado: boolean + + //relaciones con las otras tablas + @Column() + id_participante: number + + @Column() + id_evento: number + + @Column() + id_administrador: number + + /* + @ManyToOne(() => Administrador, (admin) => admin.asistencias) + @JoinColumn({ name: "id_administrador" }) + administrador: Administrador; + + @ManyToOne(() => Evento, (evento) => evento.asistencias) + @JoinColumn({ name: "id_evento" }) + evento: Evento; + + @ManyToOne(() => Participante, (participante) => participante.asistencias) + @JoinColumn({ name: "id_participante" }) + participante: Participante; + */ +} \ No newline at end of file diff --git a/src/asistencia/asistencia.module.ts b/src/asistencia/asistencia.module.ts new file mode 100644 index 0000000..f16c339 --- /dev/null +++ b/src/asistencia/asistencia.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { AsistenciaService } from './asistencia.service'; +import { AsistenciaController } from './asistencia.controller'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Asistencia } from './asistencia.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([Asistencia])], + controllers: [AsistenciaController], + providers: [AsistenciaService] +}) +export class AsistenciaModule {} diff --git a/src/asistencia/asistencia.service.ts b/src/asistencia/asistencia.service.ts new file mode 100644 index 0000000..1732a79 --- /dev/null +++ b/src/asistencia/asistencia.service.ts @@ -0,0 +1,77 @@ +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Asistencia } from './asistencia.entity'; +import { Repository } from 'typeorm'; +import { CreateAsistenciaDto } from './dto/create-asistencia.dto'; +import { UpdateAsistenciaDto } from './dto/update.asistencia.dto'; + +@Injectable() +export class AsistenciaService { + + constructor( + @InjectRepository(Asistencia) private asistenciaRepository: Repository + ) {} + + async createAsistencia(asistencia: CreateAsistenciaDto) { + + const asistenciaFound = await this.asistenciaRepository.findOne({ + where: { + fecha_asistencia: asistencia.fecha_asistencia, + metodo: asistencia.metodo, + estado: asistencia.estdao + } + }) + + if (asistenciaFound) { + return new HttpException('User already exists', HttpStatus.CONFLICT) + } + + return this.asistenciaRepository.save(asistencia) + } + + getAsistencias() { + return this.asistenciaRepository.find({}) + } + + async getAsistencia(id_asistecia: number) { + const asistenciaFound = await this.asistenciaRepository.findOne({ + where: { + id_asistecia + } + }) + + if (!asistenciaFound) { + return new HttpException('User not found', HttpStatus.NOT_FOUND); + } + + return asistenciaFound + } + + async deleteAsistencia(id_asistecia: number) { + const result = await this.asistenciaRepository.delete({ id_asistecia }) + + if (result.affected === 0) { + return new HttpException('User not found', HttpStatus.NOT_FOUND); + } + + return result + } + + + async updateAsistencia(id_asistecia: number, asistencia: UpdateAsistenciaDto) { + const asistenciaFound = await this.asistenciaRepository.findOne({ + where: { + id_asistecia + } + }) + + if (!asistenciaFound) { + return new HttpException('User not found', HttpStatus.NOT_FOUND) + } + + const updateAsistencia = Object.assign(asistenciaFound, asistencia) + return this.asistenciaRepository.save(updateAsistencia) + } + + +} diff --git a/src/asistencia/dto/create-asistencia.dto.ts b/src/asistencia/dto/create-asistencia.dto.ts new file mode 100644 index 0000000..e3da47e --- /dev/null +++ b/src/asistencia/dto/create-asistencia.dto.ts @@ -0,0 +1,5 @@ +export class CreateAsistenciaDto { + fecha_asistencia: Date + metodo: boolean + estdao: boolean +} \ No newline at end of file diff --git a/src/asistencia/dto/update.asistencia.dto.ts b/src/asistencia/dto/update.asistencia.dto.ts new file mode 100644 index 0000000..23588e4 --- /dev/null +++ b/src/asistencia/dto/update.asistencia.dto.ts @@ -0,0 +1,5 @@ +export class UpdateAsistenciaDto { + fecha_asistencia?: Date + metodo?: boolean + estado?: boolean +} \ No newline at end of file diff --git a/src/cuestionario/cuestionario.controller.ts b/src/cuestionario/cuestionario.controller.ts index 82adea6..f62f04f 100644 --- a/src/cuestionario/cuestionario.controller.ts +++ b/src/cuestionario/cuestionario.controller.ts @@ -2,32 +2,46 @@ import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/commo import { CuestionarioService } from './cuestionario.service'; import { CreateCuestionarioDto } from './dto/create-cuestionario.dto'; import { UpdateCuestionarioDto } from './dto/update-cuestionario.dto'; +import { CuestionarioApiDocumentation } from './cuestionario.documentation'; +import { ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; @Controller('cuestionario') +@CuestionarioApiDocumentation.ApiController export class CuestionarioController { constructor(private readonly cuestionarioService: CuestionarioService) {} @Post() + @CuestionarioApiDocumentation.ApiCreate create(@Body() createCuestionarioDto: CreateCuestionarioDto) { return this.cuestionarioService.create(createCuestionarioDto); } @Get() + @CuestionarioApiDocumentation.ApiGetAll findAll() { return this.cuestionarioService.findAll(); } @Get(':id') + @CuestionarioApiDocumentation.ApiGetOne findOne(@Param('id') id: string) { return this.cuestionarioService.findOne(+id); } + + @Get(':id/formulario') + @CuestionarioApiDocumentation.ApiGetFormulario + findFormulario(@Param('id') id: string) { + return this.cuestionarioService.findCompleto(+id); + } @Patch(':id') + @CuestionarioApiDocumentation.ApiUpdate update(@Param('id') id: string, @Body() updateCuestionarioDto: UpdateCuestionarioDto) { return this.cuestionarioService.update(+id, updateCuestionarioDto); } @Delete(':id') + @CuestionarioApiDocumentation.ApiRemove remove(@Param('id') id: string) { return this.cuestionarioService.remove(+id); } diff --git a/src/cuestionario/cuestionario.documentation.ts b/src/cuestionario/cuestionario.documentation.ts new file mode 100644 index 0000000..8caf16a --- /dev/null +++ b/src/cuestionario/cuestionario.documentation.ts @@ -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' }) + ); +} \ No newline at end of file diff --git a/src/cuestionario/cuestionario.module.ts b/src/cuestionario/cuestionario.module.ts index 478b3d6..b759268 100644 --- a/src/cuestionario/cuestionario.module.ts +++ b/src/cuestionario/cuestionario.module.ts @@ -1,9 +1,36 @@ import { Module } from '@nestjs/common'; import { CuestionarioService } from './cuestionario.service'; 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({ + imports: [ + TypeOrmModule.forFeature([ + Cuestionario, + Seccion, + CuestionarioSeccion, + Pregunta, + SeccionPregunta, + Opcion, + PreguntaOpcion, + TipoPregunta, + TipoCuestionario, + Evento + ]) + ], controllers: [CuestionarioController], - providers: [CuestionarioService], + providers: [CuestionarioService, EventoService], + exports: [TypeOrmModule, CuestionarioService] }) export class CuestionarioModule {} diff --git a/src/cuestionario/cuestionario.service.ts b/src/cuestionario/cuestionario.service.ts index 6a8f5ac..f3e05ad 100644 --- a/src/cuestionario/cuestionario.service.ts +++ b/src/cuestionario/cuestionario.service.ts @@ -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 { 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() export class CuestionarioService { - create(createCuestionarioDto: CreateCuestionarioDto) { - return 'This action adds a new cuestionario'; + constructor( + @InjectRepository(Cuestionario) + private cuestionarioRepository: Repository, + @InjectRepository(Seccion) + private seccionRepository: Repository, + @InjectRepository(CuestionarioSeccion) + private cuestionarioSeccionRepository: Repository, + @InjectRepository(Pregunta) + private preguntaRepository: Repository, + @InjectRepository(SeccionPregunta) + private seccionPreguntaRepository: Repository, + @InjectRepository(Opcion) + private opcionRepository: Repository, + @InjectRepository(PreguntaOpcion) + private preguntaOpcionRepository: Repository, + @InjectRepository(TipoPregunta) + private tipoPreguntaRepository: Repository, + @InjectRepository(Evento) + private eventoRepository: Repository, + 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() { - return `This action returns all cuestionario`; + return this.cuestionarioRepository.find(); } - findOne(id: number) { - return `This action returns a #${id} cuestionario`; + async findOne(id: number) { + 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) { - return `This action updates a #${id} cuestionario`; + async update(id: number, updateCuestionarioDto: UpdateCuestionarioDto) { + // Si el DTO incluye el campo "evento" (string), debemos procesarlo aparte + if (updateCuestionarioDto.evento) { + // Buscar o crear evento + let evento = await this.eventoRepository.findOne({ + where: { nombre_evento: updateCuestionarioDto.evento } + }); + + if (!evento) { + // Crear el evento + const nuevoEvento = { + nombre_evento: updateCuestionarioDto.evento, + tipo_evento: 'Predeterminado', + fecha_inicio: new Date(), + fecha_fin: new Date(Date.now() + 86400000) + }; + + evento = await this.eventoRepository.save(nuevoEvento); + } + + // Asignar el ID del evento al DTO de actualización + updateCuestionarioDto.id_evento = evento.id_evento; + + // Eliminar la propiedad evento para evitar conflictos + delete updateCuestionarioDto.evento; + } + + // Preparar los datos para la actualización + const updateData: any = { ...updateCuestionarioDto }; + + return this.cuestionarioRepository.update(id, updateData); } remove(id: number) { - return `This action removes a #${id} cuestionario`; + return this.cuestionarioRepository.delete(id); } } diff --git a/src/cuestionario/dto/create-cuestionario.dto.ts b/src/cuestionario/dto/create-cuestionario.dto.ts index d752739..ee07e94 100644 --- a/src/cuestionario/dto/create-cuestionario.dto.ts +++ b/src/cuestionario/dto/create-cuestionario.dto.ts @@ -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[]; +} diff --git a/src/cuestionario/dto/formulario.schema.ts b/src/cuestionario/dto/formulario.schema.ts new file mode 100644 index 0000000..f1478be --- /dev/null +++ b/src/cuestionario/dto/formulario.schema.ts @@ -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; +} \ No newline at end of file diff --git a/src/cuestionario/dto/update-cuestionario.dto.ts b/src/cuestionario/dto/update-cuestionario.dto.ts index f275e73..3f72844 100644 --- a/src/cuestionario/dto/update-cuestionario.dto.ts +++ b/src/cuestionario/dto/update-cuestionario.dto.ts @@ -1,4 +1,15 @@ import { PartialType } from '@nestjs/mapped-types'; import { CreateCuestionarioDto } from './create-cuestionario.dto'; +import { IsNumber, IsOptional } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; -export class UpdateCuestionarioDto extends PartialType(CreateCuestionarioDto) {} +export class UpdateCuestionarioDto extends PartialType(CreateCuestionarioDto) { + @ApiProperty({ + description: 'ID del evento asociado', + example: 1, + required: false + }) + @IsOptional() + @IsNumber() + id_evento?: number; +} diff --git a/src/cuestionario/entities/cuestionario.entity.ts b/src/cuestionario/entities/cuestionario.entity.ts index f6add87..535d2b2 100644 --- a/src/cuestionario/entities/cuestionario.entity.ts +++ b/src/cuestionario/entities/cuestionario.entity.ts @@ -1,6 +1,7 @@ import { CuestionarioSeccion } from 'src/cuestionario_seccion/entities/cuestionario_seccion.entity'; +import { Evento } from 'src/evento/evento.entity'; import { TipoCuestionario } from 'src/tipo_cuestionario/entities/tipo_cuestionario.entity'; -import { Entity, PrimaryGeneratedColumn, Column, OneToMany, ManyToMany, ManyToOne } from 'typeorm'; +import { Entity, PrimaryGeneratedColumn, Column, OneToMany, ManyToMany, ManyToOne, JoinColumn } from 'typeorm'; @@ -16,29 +17,34 @@ export class Cuestionario { contador_secciones: number; @Column({ type: 'text', nullable: true }) - descripcion: string; + descripcion?: string; @Column({ type: 'boolean', default: true }) editable: boolean; @Column({ type: 'datetime', nullable: true }) - fecha_fin: Date; + fecha_fin?: Date; @Column({ type: 'datetime', nullable: true }) - fecha_inicio: Date; + fecha_inicio?: Date; @OneToMany(() => CuestionarioSeccion,(cuestionario_seccion) => cuestionario_seccion.id_cuestionario) cuestionarioSeccion:CuestionarioSeccion[] - @OneToMany(()=> Cuestionario, (cuestionario) => cuestionario.id_cuestionario) - @Column({ type: 'int' }) + @Column({ type: 'int', nullable: true }) id_cuestionario_original: number; - @OneToMany(()=> TipoCuestionario, (tipo_cuestionario) => tipo_cuestionario.id_tipo_cuestionario ) @Column({ type: 'int' }) id_tipo_cuestionario: number; - @ManyToOne(()=> Cuestionario, (cuestionario) => cuestionario.id_cuestionario) - Cuestionario: Cuestionario[]; + @ManyToOne(()=> Cuestionario) + cuestionarioOriginal: Cuestionario; + + @Column({ type: 'int', nullable: true }) + id_evento: number; + + @ManyToOne(() => Evento) + @JoinColumn({ name: 'id_evento' }) + evento: Evento; } diff --git a/src/cuestionario_respondido/cuestionario_respondido.controller.ts b/src/cuestionario_respondido/cuestionario_respondido.controller.ts index 654f88a..647c6e1 100644 --- a/src/cuestionario_respondido/cuestionario_respondido.controller.ts +++ b/src/cuestionario_respondido/cuestionario_respondido.controller.ts @@ -2,7 +2,11 @@ import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/commo import { CuestionarioRespondidoService } from './cuestionario_respondido.service'; import { CreateCuestionarioRespondidoDto } from './dto/create-cuestionario_respondido.dto'; import { UpdateCuestionarioRespondidoDto } from './dto/update-cuestionario_respondido.dto'; +import { SubmitRespuestasDto } from './dto/submit-respuestas.dto'; +import { ApiBody, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { CuestionarioRespondidoApiDocumentation } from './cuestionario_respondido.documentation'; +@ApiTags('Cuestionario Respondido') @Controller('cuestionario-respondido') export class CuestionarioRespondidoController { constructor(private readonly cuestionarioRespondidoService: CuestionarioRespondidoService) {} @@ -12,6 +16,22 @@ export class CuestionarioRespondidoController { return this.cuestionarioRespondidoService.create(createCuestionarioRespondidoDto); } + @Post('submit') + @CuestionarioRespondidoApiDocumentation.ApiSubmitRespuestas + @ApiBody({ + type: SubmitRespuestasDto, + examples: { + comunidad: CuestionarioRespondidoApiDocumentation.ApiSubmitRespuestasExamples.communityExample, + externos: CuestionarioRespondidoApiDocumentation.ApiSubmitRespuestasExamples.externalExample + } + }) + @CuestionarioRespondidoApiDocumentation.ApiSubmitResponse201 + @CuestionarioRespondidoApiDocumentation.ApiSubmitResponse400 + @CuestionarioRespondidoApiDocumentation.ApiSubmitResponse404 + submitRespuestas(@Body() submitRespuestasDto: SubmitRespuestasDto) { + return this.cuestionarioRespondidoService.submitRespuestas(submitRespuestasDto); + } + @Get() findAll() { return this.cuestionarioRespondidoService.findAll(); diff --git a/src/cuestionario_respondido/cuestionario_respondido.documentation.ts b/src/cuestionario_respondido/cuestionario_respondido.documentation.ts new file mode 100644 index 0000000..982d89f --- /dev/null +++ b/src/cuestionario_respondido/cuestionario_respondido.documentation.ts @@ -0,0 +1,95 @@ +import { ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger'; + +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 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.' + } + }; +} \ No newline at end of file diff --git a/src/cuestionario_respondido/cuestionario_respondido.module.ts b/src/cuestionario_respondido/cuestionario_respondido.module.ts index 49b24eb..d0cdb61 100644 --- a/src/cuestionario_respondido/cuestionario_respondido.module.ts +++ b/src/cuestionario_respondido/cuestionario_respondido.module.ts @@ -1,9 +1,37 @@ import { Module } from '@nestjs/common'; import { CuestionarioRespondidoService } from './cuestionario_respondido.service'; import { CuestionarioRespondidoController } from './cuestionario_respondido.controller'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { CuestionarioRespondido } from './entities/cuestionario_respondido.entity'; +import { CuestionarioModule } from '../cuestionario/cuestionario.module'; +import { ParticipanteModule } from '../participante/participante.module'; +import { Participante } from '../participante/participante.entity'; +import { Cuestionario } from '../cuestionario/entities/cuestionario.entity'; +import { Pregunta } from '../pregunta/entities/pregunta.entity'; +import { RespuestaParticipanteAbierta } from '../respuesta_participante_abierta/entities/respuesta_participante_abierta.entity'; +import { RespuestaParticipanteCerrada } from '../respuesta_participante_cerrada/entities/respuesta_participante_cerrada.entity'; +import { PreguntaOpcion } from '../pregunta_opcion/entities/pregunta_opcion.entity'; +import { Opcion } from '../opcion/entities/opcion.entity'; +import { ValidacionesModule } from '../validaciones/validaciones.module'; @Module({ + imports: [ + TypeOrmModule.forFeature([ + CuestionarioRespondido, + Participante, + Cuestionario, + Pregunta, + RespuestaParticipanteAbierta, + RespuestaParticipanteCerrada, + PreguntaOpcion, + Opcion + ]), + CuestionarioModule, + ParticipanteModule, + ValidacionesModule + ], controllers: [CuestionarioRespondidoController], providers: [CuestionarioRespondidoService], + exports: [TypeOrmModule] }) export class CuestionarioRespondidoModule {} diff --git a/src/cuestionario_respondido/cuestionario_respondido.service.ts b/src/cuestionario_respondido/cuestionario_respondido.service.ts index ac9461c..44c9f64 100644 --- a/src/cuestionario_respondido/cuestionario_respondido.service.ts +++ b/src/cuestionario_respondido/cuestionario_respondido.service.ts @@ -1,19 +1,337 @@ -import { Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository, DataSource } from 'typeorm'; import { CreateCuestionarioRespondidoDto } from './dto/create-cuestionario_respondido.dto'; import { UpdateCuestionarioRespondidoDto } from './dto/update-cuestionario_respondido.dto'; +import { SubmitRespuestasDto } from './dto/submit-respuestas.dto'; +import { CuestionarioRespondido } from './entities/cuestionario_respondido.entity'; +import { Participante } from '../participante/participante.entity'; +import { Cuestionario } from '../cuestionario/entities/cuestionario.entity'; +import { Pregunta } from '../pregunta/entities/pregunta.entity'; +import { RespuestaParticipanteAbierta } from '../respuesta_participante_abierta/entities/respuesta_participante_abierta.entity'; +import { RespuestaParticipanteCerrada } from '../respuesta_participante_cerrada/entities/respuesta_participante_cerrada.entity'; +import { PreguntaOpcion } from '../pregunta_opcion/entities/pregunta_opcion.entity'; +import { Opcion } from '../opcion/entities/opcion.entity'; +import { ValidadorRespuestasService } from '../validaciones/validador-respuestas.service'; +import axios from 'axios'; +import * as QRCode from 'qrcode'; @Injectable() export class CuestionarioRespondidoService { + constructor( + @InjectRepository(CuestionarioRespondido) + private cuestionarioRespondidoRepository: Repository, + @InjectRepository(Participante) + private participanteRepository: Repository, + @InjectRepository(Cuestionario) + private cuestionarioRepository: Repository, + @InjectRepository(Pregunta) + private preguntaRepository: Repository, + @InjectRepository(RespuestaParticipanteAbierta) + private respuestaAbiertaRepository: Repository, + @InjectRepository(RespuestaParticipanteCerrada) + private respuestaCerradaRepository: Repository, + @InjectRepository(PreguntaOpcion) + private preguntaOpcionRepository: Repository, + @InjectRepository(Opcion) + private opcionRepository: Repository, + private dataSource: DataSource, + private validadorRespuestasService: ValidadorRespuestasService, + ) {} + create(createCuestionarioRespondidoDto: CreateCuestionarioRespondidoDto) { return 'This action adds a new cuestionarioRespondido'; } - findAll() { - return `This action returns all cuestionarioRespondido`; + async submitRespuestas(submitDto: SubmitRespuestasDto) { + const queryRunner = this.dataSource.createQueryRunner(); + await queryRunner.connect(); + await queryRunner.startTransaction(); + + try { + // 1. Verificar que el cuestionario existe + const cuestionario = await this.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: ` +

¡Gracias por registrarte!

+

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

+

Este es tu QR para registrar asistencia:

+ Código QR +

Tus datos de registro:

+
    +
  • Evento: ${cuestionario.evento.nombre_evento || 'Evento'}
  • +
  • Correo: ${participante.correo}
  • +
  • Fecha de registro: ${new Date().toLocaleString()}
  • +
+ `, + 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) { - return `This action returns a #${id} cuestionarioRespondido`; + findAll() { + return this.cuestionarioRespondidoRepository.find({ + relations: ['participante', 'cuestionario', 'respuestasAbiertas', 'respuestasCerradas'] + }); + } + + 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.texto_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.texto_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) { diff --git a/src/cuestionario_respondido/dto/submit-respuestas.dto.ts b/src/cuestionario_respondido/dto/submit-respuestas.dto.ts new file mode 100644 index 0000000..4781f76 --- /dev/null +++ b/src/cuestionario_respondido/dto/submit-respuestas.dto.ts @@ -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; +} \ No newline at end of file diff --git a/src/cuestionario_respondido/entities/cuestionario_respondido.entity.ts b/src/cuestionario_respondido/entities/cuestionario_respondido.entity.ts index 85611b0..bd44499 100644 --- a/src/cuestionario_respondido/entities/cuestionario_respondido.entity.ts +++ b/src/cuestionario_respondido/entities/cuestionario_respondido.entity.ts @@ -1 +1,33 @@ -export class CuestionarioRespondido {} +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, OneToMany, JoinColumn } from 'typeorm'; + +import { Cuestionario } from 'src/cuestionario/entities/cuestionario.entity'; +import { RespuestaParticipanteCerrada } from 'src/respuesta_participante_cerrada/entities/respuesta_participante_cerrada.entity'; +import { RespuestaParticipanteAbierta } from 'src/respuesta_participante_abierta/entities/respuesta_participante_abierta.entity'; +import { Participante } from 'src/participante/participante.entity'; + +@Entity('cuestionario_respondido') +export class CuestionarioRespondido { + @PrimaryGeneratedColumn({ name: 'id_cuestionario_respondido' }) + idCuestionarioRespondido: number; + + @Column({ name: 'fecha_respuesta', type: 'timestamp' }) + fechaRespuesta: Date; + + // Relación con Participante + @ManyToOne(() => Participante, participante => participante.cuestionariosRespondidos) + @JoinColumn({ name: 'id_participante' }) + participante: Participante; + + // Relación con Cuestionario + @ManyToOne(() => Cuestionario) + @JoinColumn({ name: 'id_cuestionario' }) + cuestionario: Cuestionario; + + // Relación con RespuestasCerradas + @OneToMany(() => RespuestaParticipanteCerrada, respuestaCerrada => respuestaCerrada.cuestionarioRespondido) + respuestasCerradas: RespuestaParticipanteCerrada[]; + + // Relación con RespuestasAbiertas + @OneToMany(() => RespuestaParticipanteAbierta, respuestaAbierta => respuestaAbierta.cuestionarioRespondido) + respuestasAbiertas: RespuestaParticipanteAbierta[]; +} \ No newline at end of file diff --git a/src/cuestionario_seccion/cuestionario_seccion.module.ts b/src/cuestionario_seccion/cuestionario_seccion.module.ts index d3635ed..8d70889 100644 --- a/src/cuestionario_seccion/cuestionario_seccion.module.ts +++ b/src/cuestionario_seccion/cuestionario_seccion.module.ts @@ -1,9 +1,19 @@ import { Module } from '@nestjs/common'; import { CuestionarioSeccionService } from './cuestionario_seccion.service'; import { CuestionarioSeccionController } from './cuestionario_seccion.controller'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { CuestionarioSeccion } from './entities/cuestionario_seccion.entity'; +import { CuestionarioModule } from '../cuestionario/cuestionario.module'; +import { SeccionModule } from '../seccion/seccion.module'; @Module({ + imports: [ + TypeOrmModule.forFeature([CuestionarioSeccion]), + CuestionarioModule, + SeccionModule + ], controllers: [CuestionarioSeccionController], providers: [CuestionarioSeccionService], + exports: [TypeOrmModule] }) export class CuestionarioSeccionModule {} diff --git a/src/cuestionario_seccion/entities/cuestionario_seccion.entity.ts b/src/cuestionario_seccion/entities/cuestionario_seccion.entity.ts index 7bd9b70..9efcd9a 100644 --- a/src/cuestionario_seccion/entities/cuestionario_seccion.entity.ts +++ b/src/cuestionario_seccion/entities/cuestionario_seccion.entity.ts @@ -11,11 +11,15 @@ export class CuestionarioSeccion { posicion: number; - @ManyToOne(() => Cuestionario, (cuestionario) => cuestionario.id_cuestionario) + @ManyToOne(() => Cuestionario, (cuestionario) => cuestionario.cuestionarioSeccion) + cuestionario: Cuestionario; + @Column({ type: 'int' }) id_cuestionario: number; - @ManyToOne(() => Seccion, (seccion) => seccion.id_seccion) + @ManyToOne(() => Seccion, (seccion) => seccion.seccion) + seccion: Seccion; + @Column({type:'int'}) id_seccion:number; } diff --git a/src/docs/docs.module.ts b/src/docs/docs.module.ts new file mode 100644 index 0000000..21a9935 --- /dev/null +++ b/src/docs/docs.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { SwaggerModule } from '@nestjs/swagger'; +import { swaggerConfig } from './swagger.config'; +import { INestApplication } from '@nestjs/common'; + +@Module({}) +export class DocsModule { + static setupSwagger(app: INestApplication) { + const document = SwaggerModule.createDocument(app, swaggerConfig); + SwaggerModule.setup('api-docs', app, document); + } +} diff --git a/src/docs/swagger.config.ts b/src/docs/swagger.config.ts new file mode 100644 index 0000000..94993e1 --- /dev/null +++ b/src/docs/swagger.config.ts @@ -0,0 +1,35 @@ +import { DocumentBuilder } from '@nestjs/swagger'; + +export const swaggerConfig = new DocumentBuilder() + .setTitle('Sistema de Formularios y Eventos CIDWA') + .setDescription(` + API para la gestión de formularios, cuestionarios, eventos y asistencias. + + ## Funcionalidades principales + + - **Cuestionarios**: Creación y gestión de formularios con secciones y preguntas + - **Eventos**: Registro y control de eventos + - **Participantes**: Gestión de usuarios y participantes + - **Asistencia**: Control de asistencia a eventos + - **QR**: Generación y validación de códigos QR + + ## Ejemplos de uso + + La API permite crear cuestionarios complejos con secciones y diferentes tipos de preguntas. + Consulte la documentación de cada endpoint para ver ejemplos específicos. + `) + .addServer(process.env.SWAGGER_SERVER_URL || 'http://localhost:3000') + .addServer( 'http://localhost:3000') + + + .setVersion('1.0') + .addTag('Cuestionarios') + .addTag('Secciones') + .addTag('Preguntas') + .addTag('Opciones') + .addTag('Usuarios') + .addTag('Eventos') + .addTag('Asistencia') + .addTag('QR') + .addBearerAuth() + .build(); diff --git a/src/evento/dto/create-evento.dto.ts b/src/evento/dto/create-evento.dto.ts new file mode 100644 index 0000000..5fa9fd6 --- /dev/null +++ b/src/evento/dto/create-evento.dto.ts @@ -0,0 +1,11 @@ +export class CreateEventoDto { + tipo_evento: string; + + nombre_evento: string; + + fecha_inicio: Date; + + fecha_fin: Date; + + //agregar el id del administrador +} \ No newline at end of file diff --git a/src/evento/dto/update.evento.dto.ts b/src/evento/dto/update.evento.dto.ts new file mode 100644 index 0000000..f85579b --- /dev/null +++ b/src/evento/dto/update.evento.dto.ts @@ -0,0 +1,6 @@ +export class UpdateEventoDto { + tipo_evento?: string + nombre_evento: string + fecha_inicio?: Date + fecha_fin?: Date +} \ No newline at end of file diff --git a/src/evento/evento.controller.ts b/src/evento/evento.controller.ts new file mode 100644 index 0000000..0586840 --- /dev/null +++ b/src/evento/evento.controller.ts @@ -0,0 +1,37 @@ +import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post } from '@nestjs/common'; +import { EventoService } from './evento.service'; +import { Evento } from './evento.entity'; +import { CreateEventoDto } from './dto/create-evento.dto'; +import { UpdateEventoDto } from './dto/update.evento.dto'; + +@Controller('evento') +export class EventoController { + constructor(private eventoService: EventoService) {} + + @Get() + getEventos(): Promise { + return this.eventoService.getEventos() + } + + @Get(':id') + getEvento(@Param('id', ParseIntPipe) id: number) { + return this.eventoService.getEvento(id) + } + + @Post() + createEvento(@Body() newEvento: CreateEventoDto) { + return this.eventoService.createEvento(newEvento) + } + + @Delete(':id') + deleteEvento(@Param('id', ParseIntPipe) id: number) { + return this.eventoService.deleteEvento(id) + } + //@Param(':id', ParseIntPipe) id: number, @Body() tipoUser: UpdateTipoUserDto + @Patch() + updateEvento(@Param(':id', ParseIntPipe) id: number, @Body() evento: UpdateEventoDto) { + return this.eventoService.updateEvento(id, evento) + } + + +} diff --git a/src/evento/evento.entity.ts b/src/evento/evento.entity.ts new file mode 100644 index 0000000..046a042 --- /dev/null +++ b/src/evento/evento.entity.ts @@ -0,0 +1,41 @@ +import { Participante } from "src/participante/participante.entity"; +import { ParticipanteEvento } from "src/participante_evento/participante_evento.entity"; +import { Column, Entity, ManyToMany, OneToMany, PrimaryGeneratedColumn } from "typeorm"; + +@Entity() +export class Evento { + @PrimaryGeneratedColumn() + id_evento: number + + @Column() + tipo_evento: string + + @Column() + nombre_evento: string + + @Column({type: 'datetime', default: () => 'CURRENT_TIMESTAMP' }) + fecha_inicio: Date + + @Column({type: 'datetime', default: () => 'CURRENT_TIMESTAMP' }) + fecha_fin: Date + + /* falta hacer la relacion + @Column() + id_administrador: number + + @ManyToOne(() => Administrador, (admin) => admin.eventos) + @JoinColumn({ name: "id_administrador" }) + administrador: Administrador; + + + + @OneToMany(() => Asistencia, (asistencia) => asistencia.evento) + asistencias: Asistencia[]; + */ + + + + @OneToMany(() => ParticipanteEvento, participanteEvento => participanteEvento.evento) + participanteEventos: ParticipanteEvento[]; + +} \ No newline at end of file diff --git a/src/evento/evento.module.ts b/src/evento/evento.module.ts new file mode 100644 index 0000000..58e904d --- /dev/null +++ b/src/evento/evento.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { EventoService } from './evento.service'; +import { EventoController } from './evento.controller'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Evento } from './evento.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([Evento])], + controllers: [EventoController], + providers: [EventoService], + exports: [EventoService] +}) +export class EventoModule {} diff --git a/src/evento/evento.service.ts b/src/evento/evento.service.ts new file mode 100644 index 0000000..eaf5d9f --- /dev/null +++ b/src/evento/evento.service.ts @@ -0,0 +1,73 @@ +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Evento } from './evento.entity'; +import { CreateEventoDto } from './dto/create-evento.dto'; +import { UpdateEventoDto } from './dto/update.evento.dto'; + +@Injectable() +export class EventoService { + constructor( + @InjectRepository(Evento) private eventoRepository: Repository, + ) {} + + async createEvento(evento: CreateEventoDto) { + const eventoFound = await this.eventoRepository.findOne({ + where: { + nombre_evento: evento.nombre_evento, + tipo_evento: evento.tipo_evento + } + }) + + if (eventoFound) + return new HttpException('Evento already exists', HttpStatus.CONFLICT) + + const createEvento = this.eventoRepository.create(evento) + + return this.eventoRepository.save(createEvento) + } + + getEventos() { + return this.eventoRepository.find({ + relations: ['participantes'] + }) + } + + async getEvento(id_evento: number) { + const eventoFound = await this.eventoRepository.findOne({ + where: { + id_evento + }, + relations: ['participantes'] + }) + + if (!eventoFound) + return new HttpException('Evento not found', HttpStatus.NOT_FOUND); + + return eventoFound + } + + async deleteEvento(id_evento: number) { + const result = await this.eventoRepository.delete({ id_evento }) + + if (result.affected === 0) { + return new HttpException('Evento not found', HttpStatus.NOT_FOUND); + } + + return result + } + + async updateEvento(id_evento: number, evento: UpdateEventoDto) { + const eventoFound = await this.eventoRepository.findOne({ + where: { + id_evento + } + }) + + if (!eventoFound) + return new HttpException('Evento not found', HttpStatus.NOT_FOUND) + + const updateEvento = Object.assign(eventoFound, evento) + return this.eventoRepository.save(updateEvento) + } +} diff --git a/src/main.ts b/src/main.ts index f76bc8d..bdbcf72 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,8 +1,58 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; +import 'dotenv/config'; +import { DocsModule } from './docs/docs.module'; +import { DataSource } from 'typeorm'; +import { TipoPregunta } from './tipo_pregunta/entities/tipo_pregunta.entity'; +import { ValidationPipe } from '@nestjs/common'; async function bootstrap() { 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(); diff --git a/src/opcion/dto/create-opcion.dto.ts b/src/opcion/dto/create-opcion.dto.ts index 69957f1..428eff5 100644 --- a/src/opcion/dto/create-opcion.dto.ts +++ b/src/opcion/dto/create-opcion.dto.ts @@ -1,8 +1,6 @@ import { IsString, MaxLength } from "class-validator"; export class CreateOpcionDto { - - @IsString() @MaxLength(50) opcion: string; diff --git a/src/opcion/entities/opcion.entity.ts b/src/opcion/entities/opcion.entity.ts index 9b7a1fc..3523e76 100644 --- a/src/opcion/entities/opcion.entity.ts +++ b/src/opcion/entities/opcion.entity.ts @@ -1,3 +1,4 @@ +import { PreguntaOpcion } from "src/pregunta_opcion/entities/pregunta_opcion.entity"; import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm"; @Entity() @@ -8,8 +9,8 @@ export class Opcion { @Column({ type: String, nullable: false, length: 200, default: '' }) opcion: string; - @OneToMany(() => PreguntaOpcion, (preguntaOpcion) => preguntaOpcion.Opcion) - Preguntas: PreguntaOpcion[]; + @OneToMany(() => PreguntaOpcion, (preguntaOpcion) => preguntaOpcion.opcion) + preguntasOpciones: PreguntaOpcion[]; } \ No newline at end of file diff --git a/src/opcion/opcion.controller.ts b/src/opcion/opcion.controller.ts index 635597a..c64e3dc 100644 --- a/src/opcion/opcion.controller.ts +++ b/src/opcion/opcion.controller.ts @@ -2,32 +2,39 @@ import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/commo import { OpcionService } from './opcion.service'; import { CreateOpcionDto } from './dto/create-opcion.dto'; import { UpdateOpcionDto } from './dto/update-opcion.dto'; +import { OpcionApiDocumentation } from './opcion.documentation'; @Controller('opcion') +@OpcionApiDocumentation.ApiController export class OpcionController { constructor(private readonly opcionService: OpcionService) {} @Post() + @OpcionApiDocumentation.ApiCreate create(@Body() createOpcionDto: CreateOpcionDto) { return this.opcionService.create(createOpcionDto); } @Get() + @OpcionApiDocumentation.ApiGetAll findAll() { return this.opcionService.findAll(); } @Get(':id') + @OpcionApiDocumentation.ApiGetOne findOne(@Param('id') id: string) { return this.opcionService.findOne(+id); } @Patch(':id') + @OpcionApiDocumentation.ApiUpdate update(@Param('id') id: string, @Body() updateOpcionDto: UpdateOpcionDto) { return this.opcionService.update(+id, updateOpcionDto); } @Delete(':id') + @OpcionApiDocumentation.ApiRemove remove(@Param('id') id: string) { return this.opcionService.remove(+id); } diff --git a/src/opcion/opcion.documentation.ts b/src/opcion/opcion.documentation.ts new file mode 100644 index 0000000..701d375 --- /dev/null +++ b/src/opcion/opcion.documentation.ts @@ -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' }) + ); +} \ No newline at end of file diff --git a/src/opcion/opcion.module.ts b/src/opcion/opcion.module.ts index b638a8a..0e1a6a8 100644 --- a/src/opcion/opcion.module.ts +++ b/src/opcion/opcion.module.ts @@ -1,9 +1,13 @@ import { Module } from '@nestjs/common'; import { OpcionService } from './opcion.service'; import { OpcionController } from './opcion.controller'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Opcion } from './entities/opcion.entity'; @Module({ + imports: [TypeOrmModule.forFeature([Opcion])], // Importa el repositorio de Opcion controllers: [OpcionController], providers: [OpcionService], + exports: [OpcionService, TypeOrmModule] }) export class OpcionModule {} diff --git a/src/opcion/opcion.service.ts b/src/opcion/opcion.service.ts index 9c44854..93e9267 100644 --- a/src/opcion/opcion.service.ts +++ b/src/opcion/opcion.service.ts @@ -12,12 +12,16 @@ export class OpcionService { private repository: Repository, ){} + create(createOpcionDto: CreateOpcionDto) { + return this.repository.save(createOpcionDto); + } + - + /* create():Promise { return this.repository.save(this.repository.create()); } - + */ findAll() { return `This action returns all opcion`; } diff --git a/src/participante/dto/create-participante.dto.ts b/src/participante/dto/create-participante.dto.ts new file mode 100644 index 0000000..871599b --- /dev/null +++ b/src/participante/dto/create-participante.dto.ts @@ -0,0 +1,22 @@ +import { IsEmail, IsNotEmpty, IsNumber } from "class-validator"; +import { ApiProperty } from "@nestjs/swagger"; + +export class CreateParticipanteDto { + @ApiProperty({ + description: 'Correo electrónico del participante', + example: 'usuario@ejemplo.com', + required: true + }) + @IsEmail({}, { message: 'El correo electrónico debe tener un formato válido' }) + @IsNotEmpty({ message: 'El correo electrónico es requerido' }) + correo: string; + + @ApiProperty({ + description: 'ID del tipo de usuario', + example: 1, + required: true + }) + @IsNumber({}, { message: 'El ID del tipo de usuario debe ser un número' }) + @IsNotEmpty({ message: 'El ID del tipo de usuario es requerido' }) + id_tipo_user: number; +} \ No newline at end of file diff --git a/src/participante/dto/update.participante.dto.ts b/src/participante/dto/update.participante.dto.ts new file mode 100644 index 0000000..9a1d769 --- /dev/null +++ b/src/participante/dto/update.participante.dto.ts @@ -0,0 +1,22 @@ +import { IsEmail, IsOptional, IsNumber } from "class-validator"; +import { ApiProperty } from "@nestjs/swagger"; + +export class UpdateParticipanteDto { + @ApiProperty({ + description: 'Nuevo correo electrónico del participante', + example: 'nuevo_correo@ejemplo.com', + required: false + }) + @IsEmail({}, { message: 'El correo electrónico debe tener un formato válido' }) + @IsOptional() + correo?: string; + + @ApiProperty({ + description: 'Nuevo ID del tipo de usuario', + example: 2, + required: false + }) + @IsNumber({}, { message: 'El ID del tipo de usuario debe ser un número' }) + @IsOptional() + id_tipo_user?: number; +} \ No newline at end of file diff --git a/src/participante/participante.controller.ts b/src/participante/participante.controller.ts new file mode 100644 index 0000000..e25b387 --- /dev/null +++ b/src/participante/participante.controller.ts @@ -0,0 +1,42 @@ +import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post } from '@nestjs/common'; +import { Participante } from './participante.entity'; +import { ParticipanteService } from './participante.service'; +import { CreateParticipanteDto } from './dto/create-participante.dto'; +import { UpdateParticipanteDto } from './dto/update.participante.dto'; +import { ParticipanteApiDocumentation } from './participante.documentation'; + +@ParticipanteApiDocumentation.ApiController +@Controller('participante') +export class ParticipanteController { + constructor(private participanteService: ParticipanteService) {} + + @ParticipanteApiDocumentation.ApiGetAll + @Get() + getParticipantes(): Promise { + return this.participanteService.getParticipantes(); + } + + @ParticipanteApiDocumentation.ApiGetOne + @Get(':id') + getParticipante(@Param('id', ParseIntPipe) id: number) { + return this.participanteService.getParticipante(id); + } + + @ParticipanteApiDocumentation.ApiCreate + @Post() + createParticipante(@Body() newParticipante: CreateParticipanteDto) { + return this.participanteService.createParticipante(newParticipante); + } + + @ParticipanteApiDocumentation.ApiRemove + @Delete(':id') + deleteParticipante(@Param('id', ParseIntPipe) id: number) { + return this.participanteService.deleteParticipante(id); + } + + @ParticipanteApiDocumentation.ApiUpdate + @Patch(':id') + updateParticipante(@Param('id', ParseIntPipe) id: number, @Body() participante: UpdateParticipanteDto) { + return this.participanteService.updateParticipante(id, participante); + } +} diff --git a/src/participante/participante.documentation.ts b/src/participante/participante.documentation.ts new file mode 100644 index 0000000..9140c5d --- /dev/null +++ b/src/participante/participante.documentation.ts @@ -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' }) + ); +} \ No newline at end of file diff --git a/src/participante/participante.entity.ts b/src/participante/participante.entity.ts new file mode 100644 index 0000000..3579682 --- /dev/null +++ b/src/participante/participante.entity.ts @@ -0,0 +1,49 @@ +import { CuestionarioRespondido } from "src/cuestionario_respondido/entities/cuestionario_respondido.entity"; +import { Evento } from "src/evento/evento.entity"; +import { ParticipanteEvento } from "src/participante_evento/participante_evento.entity"; +import { TipoUser } from "src/tipo_user/tipo_user.entity"; +import { Column, Entity, JoinColumn, JoinTable, ManyToMany, ManyToOne, OneToMany, PrimaryGeneratedColumn } from "typeorm"; + +@Entity() +export class Participante { + @PrimaryGeneratedColumn() + id_participante: number + + @Column() + correo: string + + @Column() + id_tipo_user: number + + //Relacion con tipo usuario + @ManyToOne(() => TipoUser, tipoUser => tipoUser.participante) + @JoinColumn({ name: 'participante_id' }) //nombre de la relacion + tipo_user: TipoUser + + /* + @ManyToOne(() => Administrador, (admin) => admin.eventos) + @JoinColumn({ name: "id_administrador" }) + administrador: Administrador; + + @ManyToMany(() => Evento, evento => evento.participantes) + @JoinTable({ + name: "participante_evento", + joinColumn: { name: "id_participante", referencedColumnName: "id_participante" }, + inverseJoinColumn: { name: "id_evento", referencedColumnName: "id_evento" } + }) + eventos: Evento[]; + + @OneToMany(() => Asistencia, (asistencia) => asistencia.evento) + asistencias: Asistencia[]; + */ + + + + @OneToMany(() => CuestionarioRespondido, cuestionario => cuestionario.participante) + cuestionariosRespondidos: CuestionarioRespondido[]; + + @OneToMany(() => ParticipanteEvento, participanteEvento => participanteEvento.participante) + participanteEventos: ParticipanteEvento[]; + + +} \ No newline at end of file diff --git a/src/participante/participante.module.ts b/src/participante/participante.module.ts new file mode 100644 index 0000000..41a0dd0 --- /dev/null +++ b/src/participante/participante.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { ParticipanteService } from './participante.service'; +import { ParticipanteController } from './participante.controller'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Participante } from './participante.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([Participante])], + controllers: [ParticipanteController], + providers: [ParticipanteService], + exports: [ParticipanteService, TypeOrmModule], +}) +export class ParticipanteModule {} diff --git a/src/participante/participante.service.ts b/src/participante/participante.service.ts new file mode 100644 index 0000000..c500033 --- /dev/null +++ b/src/participante/participante.service.ts @@ -0,0 +1,124 @@ +import { BadRequestException, ConflictException, Injectable, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Participante } from './participante.entity'; +import { Repository } from 'typeorm'; +import { CreateParticipanteDto } from './dto/create-participante.dto'; +import { UpdateParticipanteDto } from './dto/update.participante.dto'; + +@Injectable() +export class ParticipanteService { + constructor( + @InjectRepository(Participante) private participanteRepository: Repository + ) {} + + /** + * Crea un nuevo participante + * @param participante Datos del participante a crear + * @returns El participante creado + * @throws ConflictException si ya existe un participante con el mismo correo + */ + async createParticipante(participante: CreateParticipanteDto): Promise { + // Verificar si ya existe un participante con el mismo correo + const participanteFound = await this.participanteRepository.findOne({ + where: { + correo: participante.correo + } + }); + + if (participanteFound) { + throw new ConflictException(`Ya existe un participante con el correo ${participante.correo}`); + } + + const nuevoParticipante = this.participanteRepository.create(participante); + return this.participanteRepository.save(nuevoParticipante); + } + + /** + * Obtiene todos los participantes + * @returns Lista de participantes con sus relaciones + */ + async getParticipantes(): Promise { + return this.participanteRepository.find({ + relations: ['tipo_user', 'participanteEventos'] + }); + } + + /** + * Obtiene un participante por su ID + * @param id_participante ID del participante a buscar + * @returns El participante encontrado con sus relaciones + * @throws NotFoundException si no se encuentra el participante + */ + async getParticipante(id_participante: number): Promise { + const participanteFound = await this.participanteRepository.findOne({ + where: { + id_participante + }, + relations: ['tipo_user', 'participanteEventos'] + }); + + if (!participanteFound) { + throw new NotFoundException(`Participante con ID ${id_participante} no encontrado`); + } + + return participanteFound; + } + + /** + * Elimina un participante por su ID + * @param id_participante ID del participante a eliminar + * @returns Resultado de la eliminación + * @throws NotFoundException si no se encuentra el participante + */ + async deleteParticipante(id_participante: number) { + const result = await this.participanteRepository.delete({ id_participante }); + + if (result.affected === 0) { + throw new NotFoundException(`Participante con ID ${id_participante} no encontrado`); + } + + return { + success: true, + message: `Participante con ID ${id_participante} eliminado correctamente`, + affected: result.affected + }; + } + + /** + * Actualiza los datos de un participante + * @param id_participante ID del participante a actualizar + * @param participante Datos actualizados del participante + * @returns El participante actualizado + * @throws NotFoundException si no se encuentra el participante + * @throws BadRequestException si los datos son inválidos + */ + async updateParticipante(id_participante: number, participante: UpdateParticipanteDto): Promise { + // Verificar si el participante existe + const participanteFound = await this.participanteRepository.findOne({ + where: { + id_participante + } + }); + + if (!participanteFound) { + throw new NotFoundException(`Participante con ID ${id_participante} no encontrado`); + } + + // Si se está actualizando el correo, verificar que no exista otro participante con ese correo + if (participante.correo && participante.correo !== participanteFound.correo) { + const existingParticipante = await this.participanteRepository.findOne({ + where: { + correo: participante.correo + } + }); + + if (existingParticipante && existingParticipante.id_participante !== id_participante) { + throw new ConflictException(`Ya existe otro participante con el correo ${participante.correo}`); + } + } + + // Actualizar el participante + const updateParticipante = Object.assign(participanteFound, participante); + return this.participanteRepository.save(updateParticipante); + } +} diff --git a/src/participante_evento/dto/create-participante_evento.dto.ts b/src/participante_evento/dto/create-participante_evento.dto.ts new file mode 100644 index 0000000..44b8994 --- /dev/null +++ b/src/participante_evento/dto/create-participante_evento.dto.ts @@ -0,0 +1,5 @@ +export class CreateParticipanteEventoDto { + id_participante: number + + id_evento: number +} \ No newline at end of file diff --git a/src/participante_evento/dto/update.participante_evento.dto.ts b/src/participante_evento/dto/update.participante_evento.dto.ts new file mode 100644 index 0000000..020e021 --- /dev/null +++ b/src/participante_evento/dto/update.participante_evento.dto.ts @@ -0,0 +1,6 @@ +export class UpdateParticipanteEventoDto { + id_participante?: number + id_evento?: number + fecha_inscripcion?: Date + estatus?: boolean +} \ No newline at end of file diff --git a/src/participante_evento/participante_evento.controller.ts b/src/participante_evento/participante_evento.controller.ts new file mode 100644 index 0000000..5b78ac4 --- /dev/null +++ b/src/participante_evento/participante_evento.controller.ts @@ -0,0 +1,186 @@ +import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post } from '@nestjs/common'; +import { ParticipanteEventoService } from './participante_evento.service'; +import { ParticipanteEvento } from './participante_evento.entity'; +import { CreateParticipanteEventoDto } from './dto/create-participante_evento.dto'; +import { UpdateParticipanteEventoDto } from './dto/update.participante_evento.dto'; +import { ApiTags, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; + +@ApiTags('Participantes-Eventos') +@Controller('participante-evento') +export class ParticipanteEventoController { + + constructor(private participanteEventoService: ParticipanteEventoService) {} + + @ApiOperation({ summary: 'Obtener participantes por evento' }) + @ApiParam({ name: 'idEvento', description: 'ID del evento', type: 'number' }) + @ApiResponse({ + status: 200, + description: 'Lista de participantes que están registrados en el evento', + schema: { + type: 'array', + items: { + type: 'object', + properties: { + id_participante_evento: { type: 'number' }, + id_participante: { type: 'number' }, + id_evento: { type: 'number' }, + fecha_inscripcion: { type: 'string', format: 'date-time' }, + estatus: { type: 'boolean' }, + fecha_asistencia: { type: 'string', format: 'date-time', nullable: true }, + participante: { + type: 'object', + properties: { + id_participante: { type: 'number' }, + correo: { type: 'string' }, + id_tipo_user: { type: 'number' } + } + } + } + } + } + }) + @ApiResponse({ status: 404, description: 'Evento no encontrado' }) + @Get('evento/:idEvento') + getParticipantesPorEvento(@Param('idEvento', ParseIntPipe) idEvento: number) { + return this.participanteEventoService.getParticipantesPorEvento(idEvento); + } + + @ApiOperation({ summary: 'Obtener eventos por participante' }) + @ApiParam({ name: 'idParticipante', description: 'ID del participante', type: 'number' }) + @ApiResponse({ + status: 200, + description: 'Lista de eventos en los que está registrado el participante', + schema: { + type: 'array', + items: { + type: 'object', + properties: { + id_participante_evento: { type: 'number' }, + id_participante: { type: 'number' }, + id_evento: { type: 'number' }, + fecha_inscripcion: { type: 'string', format: 'date-time' }, + estatus: { type: 'boolean' }, + fecha_asistencia: { type: 'string', format: 'date-time', nullable: true }, + evento: { + type: 'object', + properties: { + id_evento: { type: 'number' }, + nombre_evento: { type: 'string' }, + tipo_evento: { type: 'string' }, + fecha_inicio: { type: 'string', format: 'date-time' }, + fecha_fin: { type: 'string', format: 'date-time' } + } + } + } + } + } + }) + @ApiResponse({ status: 404, description: 'Participante no encontrado' }) + @Get('participante/:idParticipante') + getEventosPorParticipante(@Param('idParticipante', ParseIntPipe) idParticipante: number) { + return this.participanteEventoService.getEventosPorParticipante(idParticipante); + } + + @ApiOperation({ summary: 'Obtener un registro específico por IDs de participante y evento' }) + @ApiParam({ name: 'idParticipante', description: 'ID del participante', type: 'number' }) + @ApiParam({ name: 'idEvento', description: 'ID del evento', type: 'number' }) + @ApiResponse({ + status: 200, + description: 'Registro participante-evento obtenido correctamente' + }) + @ApiResponse({ status: 404, description: 'Registro no encontrado' }) + @Get(':idParticipante/:idEvento') + getParticipanteEventoEspecifico( + @Param('idParticipante', ParseIntPipe) idParticipante: number, + @Param('idEvento', ParseIntPipe) idEvento: number + ) { + return this.participanteEventoService.getParticipanteEventoEspecifico(idParticipante, idEvento) + } + + @ApiOperation({ summary: 'Obtener un registro participante-evento por ID' }) + @ApiParam({ name: 'id', description: 'ID del participante', type: 'number' }) + @ApiResponse({ + status: 200, + description: 'Registro participante-evento obtenido correctamente' + }) + @ApiResponse({ status: 404, description: 'Registro no encontrado' }) + @Get(':id') + getParticipanteEvento(@Param('id', ParseIntPipe) id: number) { + return this.participanteEventoService.getParticipanteEvento(id) + } + + @ApiOperation({ summary: 'Obtener todos los registros de participante-evento' }) + @ApiResponse({ + status: 200, + description: 'Lista de todos los registros participante-evento con sus relaciones', + schema: { + type: 'array', + items: { + type: 'object', + properties: { + id_participante_evento: { type: 'number' }, + id_participante: { type: 'number' }, + id_evento: { type: 'number' }, + fecha_inscripcion: { type: 'string', format: 'date-time' }, + estatus: { type: 'boolean' }, + fecha_asistencia: { type: 'string', format: 'date-time', nullable: true } + } + } + } + }) + @Get() + getParticipantesEvento(): Promise { + return this.participanteEventoService.getParticipantesEvento() + } + + @ApiOperation({ summary: 'Registrar un participante en un evento' }) + @ApiResponse({ + status: 201, + description: 'Participante registrado en el evento correctamente' + }) + @ApiResponse({ status: 409, description: 'El participante ya está registrado en este evento' }) + @Post() + createParticipanteEvento(@Body() newParticipanteEvento: CreateParticipanteEventoDto) { + return this.participanteEventoService.createParticipanteEvento(newParticipanteEvento) + } + + @ApiOperation({ summary: 'Registrar asistencia de un participante a un evento' }) + @ApiParam({ name: 'idParticipante', description: 'ID del participante', type: 'number' }) + @ApiParam({ name: 'idEvento', description: 'ID del evento', type: 'number' }) + @ApiResponse({ + status: 200, + description: 'Asistencia registrada correctamente' + }) + @ApiResponse({ status: 404, description: 'Registro no encontrado' }) + @Post('asistencia/:idParticipante/:idEvento') + registrarAsistencia( + @Param('idParticipante', ParseIntPipe) idParticipante: number, + @Param('idEvento', ParseIntPipe) idEvento: number + ) { + return this.participanteEventoService.registrarAsistencia(idParticipante, idEvento) + } + + @ApiOperation({ summary: 'Eliminar un registro participante-evento' }) + @ApiParam({ name: 'id', description: 'ID del participante', type: 'number' }) + @ApiResponse({ + status: 200, + description: 'Registro eliminado correctamente' + }) + @ApiResponse({ status: 404, description: 'Registro no encontrado' }) + @Delete(':id') + deleteParticipanteEvento(@Param('id', ParseIntPipe) id: number) { + return this.participanteEventoService.deleteParticipanteEvento(id) + } + + @ApiOperation({ summary: 'Actualizar un registro participante-evento' }) + @ApiParam({ name: 'id', description: 'ID del participante', type: 'number' }) + @ApiResponse({ + status: 200, + description: 'Registro actualizado correctamente' + }) + @ApiResponse({ status: 404, description: 'Registro no encontrado' }) + @Patch(':id') + updateParticipanteEvento(@Param('id', ParseIntPipe) id_participante: number, id_evento: number, @Body() participanteEvento: UpdateParticipanteEventoDto) { + return this.participanteEventoService.updateParticipanteEvento(id_participante, id_evento, participanteEvento) + } +} diff --git a/src/participante_evento/participante_evento.entity.ts b/src/participante_evento/participante_evento.entity.ts new file mode 100644 index 0000000..4f537d3 --- /dev/null +++ b/src/participante_evento/participante_evento.entity.ts @@ -0,0 +1,41 @@ +import { Evento } from "src/evento/evento.entity"; +import { Participante } from "src/participante/participante.entity"; +import { Column, Entity, JoinColumn, ManyToOne, PrimaryColumn, PrimaryGeneratedColumn } from "typeorm"; + +@Entity() +export class ParticipanteEvento { + @PrimaryColumn() + id_participante: number + @PrimaryColumn() + id_evento: number + + @Column() + fecha_inscripcion: Date + + @Column() + estatus: boolean + + @Column({ nullable: true, type: 'datetime' }) + fecha_asistencia: Date + + /* + @OneToOne(() => Qr, (qr) => qr.participanteEvento) + qr: Qr; + + @OneToMany(() => ParticipanteEvento, (pe) => pe.evento) + participantes: ParticipanteEvento[]; + + @ManyToOne(() => Evento, (evento) => evento.participantes) + @JoinColumn({ name: "id_evento" }) + evento: Evento; + */ + + @ManyToOne(() => Participante, participante => participante.participanteEventos) + @JoinColumn({ name: "id_participante" }) + participante: Participante; + + @ManyToOne(() => Evento, evento => evento.participanteEventos) + @JoinColumn({ name: "id_evento" }) + evento: Evento; + +} \ No newline at end of file diff --git a/src/participante_evento/participante_evento.module.ts b/src/participante_evento/participante_evento.module.ts new file mode 100644 index 0000000..aa7ba57 --- /dev/null +++ b/src/participante_evento/participante_evento.module.ts @@ -0,0 +1,15 @@ +import { Module } from '@nestjs/common'; +import { ParticipanteEventoService } from './participante_evento.service'; +import { ParticipanteEventoController } from './participante_evento.controller'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { ParticipanteEvento } from './participante_evento.entity'; +import { Evento } from 'src/evento/evento.entity'; +import { Participante } from 'src/participante/participante.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([ParticipanteEvento, Evento, Participante])], + controllers: [ParticipanteEventoController], + providers: [ParticipanteEventoService], + exports: [ParticipanteEventoService], +}) +export class ParticipanteEventoModule {} diff --git a/src/participante_evento/participante_evento.service.ts b/src/participante_evento/participante_evento.service.ts new file mode 100644 index 0000000..77f9f5d --- /dev/null +++ b/src/participante_evento/participante_evento.service.ts @@ -0,0 +1,160 @@ +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { ParticipanteEvento } from './participante_evento.entity'; +import { Repository } from 'typeorm'; +import { CreateParticipanteEventoDto } from './dto/create-participante_evento.dto'; +import { UpdateParticipanteEventoDto } from './dto/update.participante_evento.dto'; +import { Evento } from 'src/evento/evento.entity'; +import { Participante } from 'src/participante/participante.entity'; + +@Injectable() +export class ParticipanteEventoService { + + constructor( + @InjectRepository(ParticipanteEvento) private participanteEventoRepository: Repository, + @InjectRepository(Evento) private eventoRepository: Repository, + @InjectRepository(Participante) private participanteRepository: Repository + ) {} + + async createParticipanteEvento(participanteEvento: CreateParticipanteEventoDto) { + + const participante_eventoFound = await this.participanteEventoRepository.findOne({ + where: { + id_participante: participanteEvento.id_participante, + id_evento: participanteEvento.id_evento + } + }) + + if (participante_eventoFound) { + return new HttpException('Participante already exists in this event', HttpStatus.CONFLICT) + } + + return this.participanteEventoRepository.save(participanteEvento) + } + + getParticipantesEvento() { + return this.participanteEventoRepository.find({ + relations: ['evento', 'participante'] + }) + } + + async getParticipanteEvento(id_participante: number) { + const participante_eventoFound = await this.participanteEventoRepository.findOne({ + where: { + id_participante + }, + relations: ['evento', 'participante'] + }) + + if (!participante_eventoFound) { + return new HttpException('Participante not found', HttpStatus.NOT_FOUND); + } + + return participante_eventoFound + } + + async getParticipanteEventoEspecifico(id_participante: number, id_evento: number) { + const participante_eventoFound = await this.participanteEventoRepository.findOne({ + where: { + id_participante, + id_evento + }, + relations: ['evento', 'participante'] + }) + + if (!participante_eventoFound) { + return new HttpException('Registro no encontrado', HttpStatus.NOT_FOUND); + } + + return participante_eventoFound + } + + /** + * Obtiene todos los participantes de un evento específico + * @param id_evento ID del evento + * @returns Lista de registros participante-evento con información del participante + */ + async getParticipantesPorEvento(id_evento: number) { + // Verificar si el evento existe + const eventoExists = await this.eventoRepository.findOne({ + where: { id_evento } + }); + + if (!eventoExists) { + throw new HttpException(`Evento con ID ${id_evento} no encontrado`, HttpStatus.NOT_FOUND); + } + + // Obtener todos los registros participante-evento para este evento + const participantesEvento = await this.participanteEventoRepository.find({ + where: { id_evento }, + relations: ['participante'] + }); + + return participantesEvento; + } + + /** + * Obtiene todos los eventos de un participante específico + * @param id_participante ID del participante + * @returns Lista de registros participante-evento con información del evento + */ + async getEventosPorParticipante(id_participante: number) { + // Verificar si el participante existe + const participanteExists = await this.participanteRepository.findOne({ + where: { id_participante } + }); + + if (!participanteExists) { + throw new HttpException(`Participante con ID ${id_participante} no encontrado`, HttpStatus.NOT_FOUND); + } + + // Obtener todos los registros participante-evento para este participante + const eventosParticipante = await this.participanteEventoRepository.find({ + where: { id_participante }, + relations: ['evento'] + }); + + return eventosParticipante; + } + + async deleteParticipanteEvento(id_participante: number) { + const result = await this.participanteEventoRepository.delete({ id_participante }) + + if (result.affected === 0) { + return new HttpException('Participante not found', HttpStatus.NOT_FOUND); + } + + return result + } + + async updateParticipanteEvento(id_participante: number, id_evento: number, participante_evento: UpdateParticipanteEventoDto) { + const participante_eventoFound = await this.participanteEventoRepository.findOne({ + where: { + id_participante, + id_evento + } + }) + + if (!participante_eventoFound) { + return new HttpException('Participante not found', HttpStatus.NOT_FOUND) + } + + return this.participanteEventoRepository.save(participante_evento) + } + + async registrarAsistencia(id_participante: number, id_evento: number) { + const participante_eventoFound = await this.participanteEventoRepository.findOne({ + where: { + id_participante, + id_evento + } + }); + + if (!participante_eventoFound) { + return new HttpException('Registro no encontrado', HttpStatus.NOT_FOUND); + } + + participante_eventoFound.fecha_asistencia = new Date(); + return this.participanteEventoRepository.save(participante_eventoFound); + } +} diff --git a/src/pregunta/dto/create-pregunta.dto.ts b/src/pregunta/dto/create-pregunta.dto.ts index ee5d15c..6508cb9 100644 --- a/src/pregunta/dto/create-pregunta.dto.ts +++ b/src/pregunta/dto/create-pregunta.dto.ts @@ -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[]; +} diff --git a/src/pregunta/entities/pregunta.entity.ts b/src/pregunta/entities/pregunta.entity.ts index e69de29..b936a3f 100644 --- a/src/pregunta/entities/pregunta.entity.ts +++ b/src/pregunta/entities/pregunta.entity.ts @@ -0,0 +1,39 @@ +import { PreguntaOpcion } from 'src/pregunta_opcion/entities/pregunta_opcion.entity'; +import { SeccionPregunta } from 'src/seccion_pregunta/entities/seccion_pregunta.entity'; +import { TipoPregunta } from 'src/tipo_pregunta/entities/tipo_pregunta.entity'; +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, OneToMany, JoinColumn } from 'typeorm'; + + +@Entity() +export class Pregunta { + @PrimaryGeneratedColumn() + id_pregunta: number; + + @Column() + pregunta: string; + + @Column({ default: 0 }) + contador_opcion: number; + + @Column({ default: false }) + obligatoria: boolean; + + @ManyToOne(() => TipoPregunta, tipo => tipo.preguntas) + @JoinColumn({ name: 'id_tipo_pregunta' }) + tipoPregunta: TipoPregunta; + + @Column() + id_tipo_pregunta: number; + + @Column({ type: 'int', nullable: true }) + id_opcion_dependiente?: number; + + @Column({ nullable: true }) + validacion?: string; + + @OneToMany(() => SeccionPregunta, seccionPregunta => seccionPregunta.pregunta) + seccionPreguntas: SeccionPregunta[]; + + @OneToMany(() => PreguntaOpcion, preguntaOpcion => preguntaOpcion.pregunta) + opciones: PreguntaOpcion[]; +} \ No newline at end of file diff --git a/src/pregunta/pregunta.controller.ts b/src/pregunta/pregunta.controller.ts index c5672b4..b3c6757 100644 --- a/src/pregunta/pregunta.controller.ts +++ b/src/pregunta/pregunta.controller.ts @@ -2,32 +2,39 @@ import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/commo import { PreguntaService } from './pregunta.service'; import { CreatePreguntaDto } from './dto/create-pregunta.dto'; import { UpdatePreguntaDto } from './dto/update-pregunta.dto'; +import { PreguntaApiDocumentation } from './pregunta.documentation'; @Controller('pregunta') +@PreguntaApiDocumentation.ApiController export class PreguntaController { constructor(private readonly preguntaService: PreguntaService) {} @Post() + @PreguntaApiDocumentation.ApiCreate create(@Body() createPreguntaDto: CreatePreguntaDto) { return this.preguntaService.create(createPreguntaDto); } @Get() + @PreguntaApiDocumentation.ApiGetAll findAll() { return this.preguntaService.findAll(); } @Get(':id') + @PreguntaApiDocumentation.ApiGetOne findOne(@Param('id') id: string) { return this.preguntaService.findOne(+id); } @Patch(':id') + @PreguntaApiDocumentation.ApiUpdate update(@Param('id') id: string, @Body() updatePreguntaDto: UpdatePreguntaDto) { return this.preguntaService.update(+id, updatePreguntaDto); } @Delete(':id') + @PreguntaApiDocumentation.ApiRemove remove(@Param('id') id: string) { return this.preguntaService.remove(+id); } diff --git a/src/pregunta/pregunta.documentation.ts b/src/pregunta/pregunta.documentation.ts new file mode 100644 index 0000000..0641201 --- /dev/null +++ b/src/pregunta/pregunta.documentation.ts @@ -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' }) + ); +} \ No newline at end of file diff --git a/src/pregunta/pregunta.module.ts b/src/pregunta/pregunta.module.ts index 905ec29..158e0a3 100644 --- a/src/pregunta/pregunta.module.ts +++ b/src/pregunta/pregunta.module.ts @@ -1,9 +1,25 @@ import { Module } from '@nestjs/common'; import { PreguntaService } from './pregunta.service'; 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({ + imports: [ + TypeOrmModule.forFeature([ + Pregunta, + TipoPregunta, + Opcion, + PreguntaOpcion + ]), + TipoPreguntaModule + ], controllers: [PreguntaController], providers: [PreguntaService], + exports: [TypeOrmModule, PreguntaService] }) export class PreguntaModule {} diff --git a/src/pregunta/pregunta.service.ts b/src/pregunta/pregunta.service.ts index b196cc8..c339468 100644 --- a/src/pregunta/pregunta.service.ts +++ b/src/pregunta/pregunta.service.ts @@ -1,26 +1,256 @@ -import { Injectable } from '@nestjs/common'; -import { CreatePreguntaDto } from './dto/create-pregunta.dto'; +import { Injectable, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository, DataSource } from 'typeorm'; +import { CreatePreguntaDto, OpcionDto } from './dto/create-pregunta.dto'; import { UpdatePreguntaDto } from './dto/update-pregunta.dto'; +import { 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() export class PreguntaService { - create(createPreguntaDto: CreatePreguntaDto) { - return 'This action adds a new pregunta'; + constructor( + @InjectRepository(Pregunta) + private preguntaRepository: Repository, + @InjectRepository(TipoPregunta) + private tipoPreguntaRepository: Repository, + @InjectRepository(Opcion) + private opcionRepository: Repository, + @InjectRepository(PreguntaOpcion) + private preguntaOpcionRepository: Repository, + 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() { - return `This action returns all pregunta`; + private async createOpciones( + 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) { - return `This action returns a #${id} pregunta`; + async findAll() { + return this.preguntaRepository.find(); } - update(id: number, updatePreguntaDto: UpdatePreguntaDto) { - return `This action updates a #${id} pregunta`; + async findOne(id: number) { + const pregunta = await this.preguntaRepository.findOne({ + where: { id_pregunta: id } + }); + + if (!pregunta) { + throw new NotFoundException(`Pregunta con ID ${id} no encontrada`); + } + + return pregunta; } - remove(id: number) { - return `This action removes a #${id} pregunta`; + async findWithOptions(id: number) { + 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(); + } } } diff --git a/src/pregunta_opcion/entities/pregunta_opcion.entity.ts b/src/pregunta_opcion/entities/pregunta_opcion.entity.ts index e69de29..3c0a024 100644 --- a/src/pregunta_opcion/entities/pregunta_opcion.entity.ts +++ b/src/pregunta_opcion/entities/pregunta_opcion.entity.ts @@ -0,0 +1,26 @@ +import { Opcion } from 'src/opcion/entities/opcion.entity'; +import { Pregunta } from 'src/pregunta/entities/pregunta.entity'; +import { RespuestaParticipanteCerrada } from 'src/respuesta_participante_cerrada/entities/respuesta_participante_cerrada.entity'; +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, OneToMany } from 'typeorm'; + + +@Entity() +export class PreguntaOpcion { + @PrimaryGeneratedColumn() + id_pregunta_opcion: number; + + @Column() + posicion: number; + + @ManyToOne(() => Pregunta) + pregunta: Pregunta; + + @Column() + id_pregunta: number; + + @ManyToOne(() => Opcion, opcion => opcion.preguntasOpciones) + opcion: Opcion; + + @OneToMany(()=> RespuestaParticipanteCerrada, respuestaParticipanteCerrada=>respuestaParticipanteCerrada.preguntaOpcion) + respuestaParticipanteCerrada:RespuestaParticipanteCerrada[]; +} \ No newline at end of file diff --git a/src/pregunta_opcion/pregunta_opcion.module.ts b/src/pregunta_opcion/pregunta_opcion.module.ts index 6eff7d3..3386b19 100644 --- a/src/pregunta_opcion/pregunta_opcion.module.ts +++ b/src/pregunta_opcion/pregunta_opcion.module.ts @@ -1,9 +1,19 @@ import { Module } from '@nestjs/common'; import { PreguntaOpcionService } from './pregunta_opcion.service'; import { PreguntaOpcionController } from './pregunta_opcion.controller'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { PreguntaOpcion } from './entities/pregunta_opcion.entity'; +import { OpcionModule } from '../opcion/opcion.module'; +import { PreguntaModule } from '../pregunta/pregunta.module'; @Module({ + imports: [ + TypeOrmModule.forFeature([PreguntaOpcion]), + OpcionModule, + PreguntaModule + ], controllers: [PreguntaOpcionController], providers: [PreguntaOpcionService], + exports: [TypeOrmModule] }) export class PreguntaOpcionModule {} diff --git a/src/qr/dto/create-qr.dto.ts b/src/qr/dto/create-qr.dto.ts new file mode 100644 index 0000000..b2fac5a --- /dev/null +++ b/src/qr/dto/create-qr.dto.ts @@ -0,0 +1,6 @@ +export class CreateQrDto { + id_participante_evento: number + fecha_creacion: Date + decha_vencimiento: Date + activo: boolean +} \ No newline at end of file diff --git a/src/qr/dto/update.qr.dto.ts b/src/qr/dto/update.qr.dto.ts new file mode 100644 index 0000000..6f59cf1 --- /dev/null +++ b/src/qr/dto/update.qr.dto.ts @@ -0,0 +1,5 @@ +export class UpdateQrDto { + fecha_creacion?: Date + fecha_vencimiento: Date + activo?: boolean +} \ No newline at end of file diff --git a/src/qr/qr.controller.ts b/src/qr/qr.controller.ts new file mode 100644 index 0000000..67b4ff9 --- /dev/null +++ b/src/qr/qr.controller.ts @@ -0,0 +1,68 @@ +import { + Body, + Controller, + Delete, + Get, + Param, + ParseIntPipe, + Patch, + Post, + Query, +} from '@nestjs/common'; +import { QrService } from './qr.service'; +import { Qr } from './qr.entity'; +import { CreateQrDto } from './dto/create-qr.dto'; +import { UpdateQrDto } from './dto/update.qr.dto'; +import { ApiOperation, ApiParam, ApiQuery } from '@nestjs/swagger'; + +@Controller('qr') +export class QrController { + constructor(private qrService: QrService) {} + + @Get('generate') + @ApiOperation({ summary: 'Genera un código QR a partir de un texto' }) + @ApiQuery({ + name: 'text', + required: true, + description: 'Texto a codificar en el QR', + }) + async generateQRCode(@Query('text') text: string): Promise { + return this.qrService.generateQRCode(text); + } + + @Get('asistencia/:idParticipante/:idEvento') + @ApiOperation({ summary: 'Genera un código QR para asistencia con IDs de participante y evento' }) + @ApiParam({ name: 'idParticipante', description: 'ID del participante' }) + @ApiParam({ name: 'idEvento', description: 'ID del evento' }) + async generateAsistenciaQR( + @Param('idParticipante', ParseIntPipe) idParticipante: number, + @Param('idEvento', ParseIntPipe) idEvento: number + ): Promise { + return this.qrService.generateAsistenciaQR(idParticipante, idEvento); + } + + @Get() + getQrs(): Promise { + return this.qrService.getQrs(); + } + + @Get(':id') + getQr(@Param('id', ParseIntPipe) id: number) { + return this.qrService.getQr(id); + } + + @Post() //en el body ValidationPipe + createQr(@Body() newQr: CreateQrDto) { + return this.qrService.createQr(newQr); + } + + @Delete(':id') + deleteQr(@Param('id', ParseIntPipe) id: number) { + return this.qrService.deleteQr(id); + } + + @Patch(':id') + updateQr(@Param('id', ParseIntPipe) id: number, @Body() qr: UpdateQrDto) { + return this.qrService.updateQr(id, qr); + } +} diff --git a/src/qr/qr.entity.ts b/src/qr/qr.entity.ts new file mode 100644 index 0000000..9d6b3ac --- /dev/null +++ b/src/qr/qr.entity.ts @@ -0,0 +1,25 @@ +import { Column, Entity, PrimaryGeneratedColumn } from "typeorm"; + +@Entity() +export class Qr { + @PrimaryGeneratedColumn() + id_qr: number + + /* + @OneToOne(() => ParticipanteEvento, (pe) => pe.qr) + @JoinColumn({ name: "id_participante_evento" }) + participanteEvento: ParticipanteEvento; + */ + + @Column({type: 'datetime', default: () => 'CURRENT_TIMESTAMP' }) + fecha_creacion: Date + + @Column({type: 'datetime', default: () => 'CURRENT_TIMESTAMP' }) + fecha_vencimiento: Date + + @Column() + activo: boolean + + //Relacion con id_participante_evento + +} \ No newline at end of file diff --git a/src/qr/qr.module.ts b/src/qr/qr.module.ts new file mode 100644 index 0000000..1a6040e --- /dev/null +++ b/src/qr/qr.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { QrService } from './qr.service'; +import { QrController } from './qr.controller'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Qr } from './qr.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([Qr])], + controllers: [QrController], + providers: [QrService] +}) +export class QrModule {} diff --git a/src/qr/qr.service.ts b/src/qr/qr.service.ts new file mode 100644 index 0000000..5fc831c --- /dev/null +++ b/src/qr/qr.service.ts @@ -0,0 +1,105 @@ +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { Qr } from './qr.entity'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { CreateQrDto } from './dto/create-qr.dto'; +import { UpdateQrDto } from './dto/update.qr.dto'; +// @ts-ignore +import * as QRCode from 'qrcode'; + +@Injectable() +export class QrService { + constructor(@InjectRepository(Qr) private qrRepository: Repository) {} + + async generateQRCode(text: string): Promise { + return await QRCode.toDataURL(text); + } + + async generateAsistenciaQR(id_participante: number, id_evento: number): Promise { + // Crear un objeto JSON que contenga los IDs + const qrData = { + id_participante, + id_evento + }; + + // Convertir el objeto a una cadena JSON + const jsonStr = JSON.stringify(qrData); + + // Generar el código QR con la cadena JSON + return await QRCode.toDataURL(jsonStr); + } + + async generateBuffer(text: string): Promise { + return await QRCode.toBuffer(text); // Devuelve un buffer para adjuntar en email o stream + } + + // 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: '

Escanea el siguiente código:

', + attachments: [{ + filename: 'qrcode.png', + content: qrBuffer, + cid: 'qrcode', + }], +}); */ + + async createQr(qr: CreateQrDto) { + const qrFound = await this.qrRepository.findOne({ + where: { + id_qr: qr.id_participante_evento, + }, + }); + + if (qrFound) { + return new HttpException('Qr already exists', HttpStatus.CONFLICT); + } + + return this.qrRepository.save(qr); + } + + getQrs() { + return this.qrRepository.find({}); + } + + async getQr(id_qr) { + const qrFound = await this.qrRepository.findOne({ + where: { + id_qr, + }, + }); + + if (!qrFound) { + return new HttpException('User not found', HttpStatus.NOT_FOUND); + } + + return qrFound; + } + + async deleteQr(id_qr: number) { + const result = await this.qrRepository.delete({ id_qr }); + + if (result.affected === 0) { + return new HttpException('User not found', HttpStatus.NOT_FOUND); + } + + return result; + } + + async updateQr(id_qr: number, qr: UpdateQrDto) { + const qrFound = await this.qrRepository.findOne({ + where: { + id_qr, + }, + }); + + if (!qrFound) { + return new HttpException('User not found', HttpStatus.NOT_FOUND); + } + + const updateQr = Object.assign(qrFound, qr); + return this.qrRepository.save(updateQr); + } +} diff --git a/src/respuesta_participante_abierta/entities/respuesta_participante_abierta.entity.ts b/src/respuesta_participante_abierta/entities/respuesta_participante_abierta.entity.ts index 1990bee..4a6262e 100644 --- a/src/respuesta_participante_abierta/entities/respuesta_participante_abierta.entity.ts +++ b/src/respuesta_participante_abierta/entities/respuesta_participante_abierta.entity.ts @@ -14,17 +14,14 @@ export class RespuestaParticipanteAbierta { // Relación con CuestionarioRespondido @ManyToOne( () => CuestionarioRespondido, - cuestionarioRespondido => cuestionarioRespondido.idCuestionarioRespondido + cuestionarioRespondido => cuestionarioRespondido.respuestasAbiertas ) @JoinColumn({ name: 'id_cuestionario_respondido' }) - id_cuestionario_respondido: CuestionarioRespondido; + cuestionarioRespondido: CuestionarioRespondido; - @ManyToOne( - () => Pregunta, - pregunta => pregunta.id_pregunta - ) + @ManyToOne(() => Pregunta) @JoinColumn({ name: 'id_pregunta' }) - id_pregunta: Pregunta; + pregunta: Pregunta; } \ No newline at end of file diff --git a/src/respuesta_participante_abierta/respuesta_participante_abierta.module.ts b/src/respuesta_participante_abierta/respuesta_participante_abierta.module.ts index 125f0d5..8fada88 100644 --- a/src/respuesta_participante_abierta/respuesta_participante_abierta.module.ts +++ b/src/respuesta_participante_abierta/respuesta_participante_abierta.module.ts @@ -1,9 +1,19 @@ import { Module } from '@nestjs/common'; import { RespuestaParticipanteAbiertaService } from './respuesta_participante_abierta.service'; import { RespuestaParticipanteAbiertaController } from './respuesta_participante_abierta.controller'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { RespuestaParticipanteAbierta } from './entities/respuesta_participante_abierta.entity'; +import { CuestionarioRespondidoModule } from '../cuestionario_respondido/cuestionario_respondido.module'; +import { PreguntaModule } from '../pregunta/pregunta.module'; @Module({ + imports: [ + TypeOrmModule.forFeature([RespuestaParticipanteAbierta]), + CuestionarioRespondidoModule, + PreguntaModule + ], controllers: [RespuestaParticipanteAbiertaController], providers: [RespuestaParticipanteAbiertaService], + exports: [TypeOrmModule] }) export class RespuestaParticipanteAbiertaModule {} diff --git a/src/respuesta_participante_cerrada/entities/respuesta_participante_cerrada.entity.ts b/src/respuesta_participante_cerrada/entities/respuesta_participante_cerrada.entity.ts index 1c60109..0d65cb0 100644 --- a/src/respuesta_participante_cerrada/entities/respuesta_participante_cerrada.entity.ts +++ b/src/respuesta_participante_cerrada/entities/respuesta_participante_cerrada.entity.ts @@ -1 +1,20 @@ -export class RespuestaParticipanteCerrada {} +import { CuestionarioRespondido } from 'src/cuestionario_respondido/entities/cuestionario_respondido.entity'; +import { PreguntaOpcion } from 'src/pregunta_opcion/entities/pregunta_opcion.entity'; +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm'; + + +@Entity('respuesta_participante_cerrada') +export class RespuestaParticipanteCerrada { + @PrimaryGeneratedColumn() + idRespuestaParticipanteCerrada: number; + + @ManyToOne(() => CuestionarioRespondido, cuestionarioRespondido => cuestionarioRespondido.respuestasCerradas) + @JoinColumn({ name: 'id_cuestionario_respondido' }) + cuestionarioRespondido: CuestionarioRespondido; + + @ManyToOne(() => PreguntaOpcion, preguntaOpcion => preguntaOpcion.respuestaParticipanteCerrada) + @JoinColumn({ name: 'id_pregunta_opcion' }) + preguntaOpcion: PreguntaOpcion; + + +} \ No newline at end of file diff --git a/src/respuesta_participante_cerrada/respuesta_participante_cerrada.module.ts b/src/respuesta_participante_cerrada/respuesta_participante_cerrada.module.ts index 5fdf968..7f826bd 100644 --- a/src/respuesta_participante_cerrada/respuesta_participante_cerrada.module.ts +++ b/src/respuesta_participante_cerrada/respuesta_participante_cerrada.module.ts @@ -1,9 +1,19 @@ import { Module } from '@nestjs/common'; import { RespuestaParticipanteCerradaService } from './respuesta_participante_cerrada.service'; import { RespuestaParticipanteCerradaController } from './respuesta_participante_cerrada.controller'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { RespuestaParticipanteCerrada } from './entities/respuesta_participante_cerrada.entity'; +import { PreguntaOpcionModule } from '../pregunta_opcion/pregunta_opcion.module'; +import { CuestionarioRespondidoModule } from '../cuestionario_respondido/cuestionario_respondido.module'; @Module({ + imports: [ + TypeOrmModule.forFeature([RespuestaParticipanteCerrada]), + PreguntaOpcionModule, + CuestionarioRespondidoModule + ], controllers: [RespuestaParticipanteCerradaController], providers: [RespuestaParticipanteCerradaService], + exports: [TypeOrmModule] }) export class RespuestaParticipanteCerradaModule {} diff --git a/src/seccion/dto/create-seccion.dto.ts b/src/seccion/dto/create-seccion.dto.ts index 4753a9c..ccb44c1 100644 --- a/src/seccion/dto/create-seccion.dto.ts +++ b/src/seccion/dto/create-seccion.dto.ts @@ -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[]; +} diff --git a/src/seccion/entities/seccion.entity.ts b/src/seccion/entities/seccion.entity.ts index ee2701d..a5c3b6d 100644 --- a/src/seccion/entities/seccion.entity.ts +++ b/src/seccion/entities/seccion.entity.ts @@ -11,7 +11,7 @@ export class Seccion { contador_pregunta: number; @Column({ type: 'text', nullable: true }) - descripcion: string; + descripcion?: string; @Column({ type: 'text', nullable: true }) titulo: string; @@ -19,6 +19,6 @@ export class Seccion { @OneToMany(() => CuestionarioSeccion,(cuestionario_seccion) => cuestionario_seccion.id_seccion ) seccion:CuestionarioSeccion[]; - @OneToMany(()=> SeccionPregunta, (seccion_pregunta)=> seccion_pregunta.id_seccion) - seccion_pregunta:SeccionPregunta[]; + @OneToMany(()=> SeccionPregunta, (seccion_pregunta)=> seccion_pregunta.seccion) + seccionPreguntas:SeccionPregunta[]; } diff --git a/src/seccion/seccion.controller.ts b/src/seccion/seccion.controller.ts index f3e3dcf..d3967c8 100644 --- a/src/seccion/seccion.controller.ts +++ b/src/seccion/seccion.controller.ts @@ -2,32 +2,39 @@ import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/commo import { SeccionService } from './seccion.service'; import { CreateSeccionDto } from './dto/create-seccion.dto'; import { UpdateSeccionDto } from './dto/update-seccion.dto'; +import { SeccionApiDocumentation } from './seccion.documentation'; @Controller('seccion') +@SeccionApiDocumentation.ApiController export class SeccionController { constructor(private readonly seccionService: SeccionService) {} @Post() + @SeccionApiDocumentation.ApiCreate create(@Body() createSeccionDto: CreateSeccionDto) { return this.seccionService.create(createSeccionDto); } @Get() + @SeccionApiDocumentation.ApiGetAll findAll() { return this.seccionService.findAll(); } @Get(':id') + @SeccionApiDocumentation.ApiGetOne findOne(@Param('id') id: string) { return this.seccionService.findOne(+id); } @Patch(':id') + @SeccionApiDocumentation.ApiUpdate update(@Param('id') id: string, @Body() updateSeccionDto: UpdateSeccionDto) { return this.seccionService.update(+id, updateSeccionDto); } @Delete(':id') + @SeccionApiDocumentation.ApiRemove remove(@Param('id') id: string) { return this.seccionService.remove(+id); } diff --git a/src/seccion/seccion.documentation.ts b/src/seccion/seccion.documentation.ts new file mode 100644 index 0000000..72d4976 --- /dev/null +++ b/src/seccion/seccion.documentation.ts @@ -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' }) + ); +} \ No newline at end of file diff --git a/src/seccion/seccion.module.ts b/src/seccion/seccion.module.ts index a3b53de..8baf400 100644 --- a/src/seccion/seccion.module.ts +++ b/src/seccion/seccion.module.ts @@ -1,9 +1,27 @@ import { Module } from '@nestjs/common'; import { SeccionService } from './seccion.service'; import { SeccionController } from './seccion.controller'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Seccion } from './entities/seccion.entity'; +import { Pregunta } from '../pregunta/entities/pregunta.entity'; +import { 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({ + imports: [ + TypeOrmModule.forFeature([ + Seccion, + Pregunta, + SeccionPregunta, + TipoPregunta, + Opcion, + PreguntaOpcion + ]) + ], controllers: [SeccionController], providers: [SeccionService], + exports: [TypeOrmModule, SeccionService] }) export class SeccionModule {} diff --git a/src/seccion/seccion.service.ts b/src/seccion/seccion.service.ts index 789091b..7915dd1 100644 --- a/src/seccion/seccion.service.ts +++ b/src/seccion/seccion.service.ts @@ -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 { 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() export class SeccionService { - create(createSeccionDto: CreateSeccionDto) { - return 'This action adds a new seccion'; + constructor( + @InjectRepository(Seccion) + private seccionRepository: Repository, + @InjectRepository(Pregunta) + private preguntaRepository: Repository, + @InjectRepository(SeccionPregunta) + private seccionPreguntaRepository: Repository, + @InjectRepository(TipoPregunta) + private tipoPreguntaRepository: Repository, + @InjectRepository(Opcion) + private opcionRepository: Repository, + @InjectRepository(PreguntaOpcion) + private preguntaOpcionRepository: Repository, + 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() { - return `This action returns all seccion`; + async findAll() { + return this.seccionRepository.find(); } - findOne(id: number) { - return `This action returns a #${id} seccion`; + async findOne(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 seccion; } - update(id: number, updateSeccionDto: UpdateSeccionDto) { - return `This action updates a #${id} seccion`; + async findWithPreguntas(id: number) { + const seccion = await this.seccionRepository.findOne({ + where: { id_seccion: id } + }); + + if (!seccion) { + throw new NotFoundException(`Sección con ID ${id} no encontrada`); + } + + // 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) { - return `This action removes a #${id} seccion`; + async update(id: number, updateSeccionDto: UpdateSeccionDto) { + 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); } } diff --git a/src/seccion_pregunta/entities/seccion_pregunta.entity.ts b/src/seccion_pregunta/entities/seccion_pregunta.entity.ts index cafbe50..c0dcad6 100644 --- a/src/seccion_pregunta/entities/seccion_pregunta.entity.ts +++ b/src/seccion_pregunta/entities/seccion_pregunta.entity.ts @@ -1,3 +1,4 @@ +import { Pregunta } from 'src/pregunta/entities/pregunta.entity'; import { Seccion } from 'src/seccion/entities/seccion.entity'; import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from 'typeorm'; @@ -10,11 +11,15 @@ export class SeccionPregunta { @Column({ type: 'tinyint' }) posicion: number; - @ManyToOne(() => Pregunta, (pregunta) => pregunta.id_pregunta) + @ManyToOne(() => Pregunta, (pregunta) => pregunta.seccionPreguntas) + pregunta: Pregunta; + @Column({ type: 'int' }) id_pregunta: number; - @ManyToOne(() => Seccion, (seccion) => seccion.id_seccion) + @ManyToOne(() => Seccion, (seccion) => seccion.seccionPreguntas) + seccion: Seccion; + @Column({ type: 'int' }) id_seccion: number; } diff --git a/src/seccion_pregunta/seccion_pregunta.module.ts b/src/seccion_pregunta/seccion_pregunta.module.ts index d911c60..5564e86 100644 --- a/src/seccion_pregunta/seccion_pregunta.module.ts +++ b/src/seccion_pregunta/seccion_pregunta.module.ts @@ -1,9 +1,19 @@ import { Module } from '@nestjs/common'; import { SeccionPreguntaService } from './seccion_pregunta.service'; import { SeccionPreguntaController } from './seccion_pregunta.controller'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { SeccionPregunta } from './entities/seccion_pregunta.entity'; +import { PreguntaModule } from '../pregunta/pregunta.module'; +import { SeccionModule } from '../seccion/seccion.module'; @Module({ + imports: [ + TypeOrmModule.forFeature([SeccionPregunta]), + PreguntaModule, + SeccionModule + ], controllers: [SeccionPreguntaController], providers: [SeccionPreguntaService], + exports: [TypeOrmModule] }) export class SeccionPreguntaModule {} diff --git a/src/tipo_pregunta/entities/tipo_pregunta.entity.ts b/src/tipo_pregunta/entities/tipo_pregunta.entity.ts index e69de29..e6bea46 100644 --- a/src/tipo_pregunta/entities/tipo_pregunta.entity.ts +++ b/src/tipo_pregunta/entities/tipo_pregunta.entity.ts @@ -0,0 +1,14 @@ +import { Pregunta } from 'src/pregunta/entities/pregunta.entity'; +import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm'; + +@Entity() +export class TipoPregunta { + @PrimaryGeneratedColumn() + id_tipo: number; + + @Column() + tipo_pregunta: string; + + @OneToMany(() => Pregunta, pregunta => pregunta.tipoPregunta) + preguntas: Pregunta[]; +} \ No newline at end of file diff --git a/src/tipo_pregunta/tipo_pregunta.controller.ts b/src/tipo_pregunta/tipo_pregunta.controller.ts index e69de29..cc69f18 100644 --- a/src/tipo_pregunta/tipo_pregunta.controller.ts +++ b/src/tipo_pregunta/tipo_pregunta.controller.ts @@ -0,0 +1,6 @@ +import { Controller } from '@nestjs/common'; + +@Controller('tipo_pregunta') +export class TipoPreguntaController { + // Métodos del controlador +} diff --git a/src/tipo_pregunta/tipo_pregunta.module.ts b/src/tipo_pregunta/tipo_pregunta.module.ts index 0494c98..3ef0222 100644 --- a/src/tipo_pregunta/tipo_pregunta.module.ts +++ b/src/tipo_pregunta/tipo_pregunta.module.ts @@ -1,9 +1,13 @@ import { Module } from '@nestjs/common'; import { TipoPreguntaService } from './tipo_pregunta.service'; import { TipoPreguntaController } from './tipo_pregunta.controller'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { TipoPregunta } from './entities/tipo_pregunta.entity'; @Module({ + imports: [TypeOrmModule.forFeature([TipoPregunta])], controllers: [TipoPreguntaController], providers: [TipoPreguntaService], + exports: [TipoPreguntaService, TypeOrmModule] }) export class TipoPreguntaModule {} diff --git a/src/tipo_pregunta/tipo_pregunta.service.ts b/src/tipo_pregunta/tipo_pregunta.service.ts index e69de29..37f97c0 100644 --- a/src/tipo_pregunta/tipo_pregunta.service.ts +++ b/src/tipo_pregunta/tipo_pregunta.service.ts @@ -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, + ) {} + + 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); + } +} diff --git a/src/tipo_user/dto/create-tipo-user.dto.ts b/src/tipo_user/dto/create-tipo-user.dto.ts new file mode 100644 index 0000000..4ab9314 --- /dev/null +++ b/src/tipo_user/dto/create-tipo-user.dto.ts @@ -0,0 +1,7 @@ +export class CreateTipoUserDto { + tipo: string + nombre: string + apellido_p: string + apellido_m: string + correo?: string +} \ No newline at end of file diff --git a/src/tipo_user/dto/update.tipo_user.dto.ts b/src/tipo_user/dto/update.tipo_user.dto.ts new file mode 100644 index 0000000..26dbfac --- /dev/null +++ b/src/tipo_user/dto/update.tipo_user.dto.ts @@ -0,0 +1,6 @@ +export class UpdateTipoUserDto { + tipo?: string + nombre?: string + apellido_p?: string + apellido_m: string +} \ No newline at end of file diff --git a/src/tipo_user/tipo_user.controller.ts b/src/tipo_user/tipo_user.controller.ts new file mode 100644 index 0000000..abed59a --- /dev/null +++ b/src/tipo_user/tipo_user.controller.ts @@ -0,0 +1,63 @@ +import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post } from '@nestjs/common'; +import { TipoUserService } from './tipo_user.service'; +import { TipoUser } from './tipo_user.entity'; +import { CreateTipoUserDto } from './dto/create-tipo-user.dto'; +import { UpdateTipoUserDto } from './dto/update.tipo_user.dto'; +import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiBody } from '@nestjs/swagger'; + +@ApiTags('Tipo de Usuario') // Agrupa los endpoints en Swagger +@Controller('tipo-user') +export class TipoUserController { + + constructor(private tipoUserService: TipoUserService) {} + + @ApiOperation({ summary: 'Obtener todos los tipos de usuario' }) + @ApiResponse({ status: 200, description: 'Lista de tipos de usuario obtenida correctamente.' }) + @Get() + getTipoUsers(): Promise { + return this.tipoUserService.getTipoUsers(); + } + + @Get(':id') + @ApiOperation({ summary: 'Obtener un tipo de usuario por ID' }) + @ApiParam({ name: 'id', description: 'ID del tipo de usuario', example: 1 }) + @ApiResponse({ status: 200, description: 'Tipo de usuario obtenido correctamente.' }) + @ApiResponse({ status: 404, description: 'Tipo de usuario no encontrado.' }) + getTipoUser(@Param('id', ParseIntPipe) id: number) { + return this.tipoUserService.getTipoUser(id); + } + + @Post() + @ApiOperation({ + summary: 'Crear un tipo de usuario', + description: 'Este endpoint permite registrar un nuevo tipo de usuario en el sistema.', + }) + @ApiBody({ + description: 'Datos del tipo de usuario', + schema: { + type: 'object', + properties: { + tipo: { type: 'string', example: 'Administrador' }, + nombre: { type: 'string', example: 'Juan' }, + apellido_p: { type: 'string', example: 'Pérez' }, + apellido_m: { type: 'string', example: 'Gómez' }, + }, + }, + }) + @ApiResponse({ status: 201, description: 'Tipo de usuario creado correctamente.' }) + @ApiResponse({ status: 400, description: 'Datos inválidos en la solicitud.' }) + + createTipoUser(@Body() newTipoUser: CreateTipoUserDto) { + return this.tipoUserService.createTipoUser(newTipoUser) + } + + @Delete(':id') + deleteTipoUser(@Param('id', ParseIntPipe) id:number) { + return this.tipoUserService.deleteTipoUser(id) + } + + @Patch() + updateTipoUser(@Param(':id', ParseIntPipe) id: number, @Body() tipoUser: UpdateTipoUserDto) { + return this.tipoUserService.updateTipoUser(id, tipoUser) + } +} diff --git a/src/tipo_user/tipo_user.entity.ts b/src/tipo_user/tipo_user.entity.ts new file mode 100644 index 0000000..a20976e --- /dev/null +++ b/src/tipo_user/tipo_user.entity.ts @@ -0,0 +1,28 @@ +import { Administrador } from "src/administrador/administrador.entity"; +import { Participante } from "src/participante/participante.entity"; +import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm"; + +@Entity() +export class TipoUser { + @PrimaryGeneratedColumn() + id_tipo_user: number; + + @Column() + tipo: string; + + @Column() + nombre: string; + + @Column({nullable: true}) + apellido_p: string; + + @Column({nullable: true}) + apellido_m: string; + + @OneToMany(() => Participante, participante => participante.tipo_user) + participante: Participante[] + + @OneToMany(() => Administrador, administrador => administrador.tipoUser) + administrador: Administrador[] + +} \ No newline at end of file diff --git a/src/tipo_user/tipo_user.module.ts b/src/tipo_user/tipo_user.module.ts new file mode 100644 index 0000000..2caf76b --- /dev/null +++ b/src/tipo_user/tipo_user.module.ts @@ -0,0 +1,16 @@ +import { Module } from '@nestjs/common'; +import { TipoUserService } from './tipo_user.service'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { TipoUser } from './tipo_user.entity'; +import { TipoUserController } from './tipo_user.controller'; +import { Administrador } from 'src/administrador/administrador.entity'; +import { Participante } from 'src/participante/participante.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([TipoUser, Administrador, Participante])], + controllers: [TipoUserController], + providers: [TipoUserService], + exports: [TipoUserService] + +}) +export class TipoUserModule {} diff --git a/src/tipo_user/tipo_user.service.ts b/src/tipo_user/tipo_user.service.ts new file mode 100644 index 0000000..f86d4a4 --- /dev/null +++ b/src/tipo_user/tipo_user.service.ts @@ -0,0 +1,126 @@ +import { BadRequestException, HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { TipoUser } from './tipo_user.entity'; +import { Repository } from 'typeorm'; +import { CreateTipoUserDto } from './dto/create-tipo-user.dto'; +import { UpdateTipoUserDto } from './dto/update.tipo_user.dto'; +import { Participante } from 'src/participante/participante.entity'; +import { Administrador } from 'src/administrador/administrador.entity'; + +@Injectable() +export class TipoUserService { + + constructor( + @InjectRepository(TipoUser) private tipoUserRepository: Repository, + @InjectRepository(Participante) private participanteRepository: Repository, + @InjectRepository(Administrador) private administradorRepository: Repository, + ) {} + + /* + //Funcion para registrar usuario + async registrarUsuario(dto: CreateTipoUserDto) { + // Crear el registro en tipo_user + const tipoUser = this.tipoUserRepository.create({ + tipo: dto.tipo, + nombre: dto.nombre, + apellido_p: dto.apellido_p, + apellido_m: dto.apellido_m, + }); + + await this.tipoUserRepository.save(tipoUser); + + // Si es participante, insertar en participante + if (dto.tipo === 'participante') { + if (!dto.correo) { + throw new BadRequestException('El correo es obligatorio para participantes'); + } + + const participante = this.participanteRepository.create({ + correo: dto.correo, + tipo_user: tipoUser, // Relación con tipo_user + }); + + await this.participanteRepository.save(participante); + } + + // Si es administrador, insertar en administrador + if (dto.tipo === 'administrador') { + const administrador = this.administradorRepository.create({ + tipoUser: tipoUser, // Relación con tipo_user + }); + + await this.administradorRepository.save(administrador); + } + + return { message: 'Usuario registrado correctamente' }; + } + } + */ + + async createTipoUser(tipoUser: CreateTipoUserDto) { + // falta hacer la validacion + + const tipo_userFound = await this.tipoUserRepository.findOne({ + where: { + nombre: tipoUser.nombre, + tipo: tipoUser.tipo, + //id_tipo_user: + } + }) + + if(tipo_userFound) { + return new HttpException('Tipo User already exict', HttpStatus.NOT_FOUND); + } + + const tipo_user = this.tipoUserRepository.create(tipoUser) + + return this.tipoUserRepository.save(tipo_user) + } + + getTipoUsers() { + return this.tipoUserRepository.find({ + relations: ['participante', 'administrador'] + }) + } + + async getTipoUser(id: number) { + const tipo_userFound = await this.tipoUserRepository.findOne({ + where: { + id_tipo_user: id + }, + relations: ['participante', 'administrador'] + }); + + if(!tipo_userFound) { + return new HttpException('User not found', HttpStatus.NOT_FOUND); + } + + return tipo_userFound; + } + + async deleteTipoUser(id_tipo_user: number) { + const result = await this.tipoUserRepository.delete({ id_tipo_user }) + + if(result.affected === 0) { + return new HttpException('User not found', HttpStatus.NOT_FOUND); + } + + return result + } + + async updateTipoUser(id_tipo_user: number, tipo_user: UpdateTipoUserDto) { + const tipo_userFound = await this.tipoUserRepository.findOne({ + where: { + id_tipo_user + } + }) + + if(!tipo_userFound) { + return new HttpException('User not found', HttpStatus.NOT_FOUND) + } + + const updateTipoUser = Object.assign(tipo_userFound, tipo_user) + return this.tipoUserRepository.save(updateTipoUser) + } + +} diff --git a/src/validaciones/validaciones.module.ts b/src/validaciones/validaciones.module.ts new file mode 100644 index 0000000..2ad3103 --- /dev/null +++ b/src/validaciones/validaciones.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { ValidadorRespuestasService } from './validador-respuestas.service'; + +@Module({ + providers: [ValidadorRespuestasService], + exports: [ValidadorRespuestasService] +}) +export class ValidacionesModule {} \ No newline at end of file diff --git a/src/validaciones/validador-respuestas.service.ts b/src/validaciones/validador-respuestas.service.ts new file mode 100644 index 0000000..6dd7598 --- /dev/null +++ b/src/validaciones/validador-respuestas.service.ts @@ -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); + } +} \ No newline at end of file diff --git a/src/validaciones/validador.ts b/src/validaciones/validador.ts new file mode 100644 index 0000000..1ca4be1 --- /dev/null +++ b/src/validaciones/validador.ts @@ -0,0 +1,287 @@ +/** + * 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 { + 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 (!/^[a-zA-ZáéíóúÁÉÍÓÚñÑüÜ\\s]+$/.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); +} \ No newline at end of file diff --git a/utils/crear_formulario_feria.ts b/utils/crear_formulario_feria.ts new file mode 100644 index 0000000..0b2d77d --- /dev/null +++ b/utils/crear_formulario_feria.ts @@ -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, + }, + ], + }, + ], + }; \ No newline at end of file diff --git a/utils/get_formulario_feria.ts b/utils/get_formulario_feria.ts new file mode 100644 index 0000000..dfffb3c --- /dev/null +++ b/utils/get_formulario_feria.ts @@ -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', + }, + }, + ], + }, + }, + ], + }, + ], + }, + }; \ No newline at end of file diff --git a/utils/load-form.js b/utils/load-form.js new file mode 100644 index 0000000..9489b53 --- /dev/null +++ b/utils/load-form.js @@ -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); + }); \ No newline at end of file diff --git a/utils/mandar_formulario.json b/utils/mandar_formulario.json new file mode 100644 index 0000000..e69de29 diff --git a/utils/respuesta_1.json b/utils/respuesta_1.json new file mode 100644 index 0000000..e69de29 diff --git a/utils/respuesta_2.json b/utils/respuesta_2.json new file mode 100644 index 0000000..e69de29 diff --git a/utils/respuesta_formulario_feria.ts b/utils/respuesta_formulario_feria.ts new file mode 100644 index 0000000..0a2b5fe --- /dev/null +++ b/utils/respuesta_formulario_feria.ts @@ -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', + }; \ No newline at end of file diff --git a/utils/test-crear-formulario.ts b/utils/test-crear-formulario.ts new file mode 100644 index 0000000..62b9ada --- /dev/null +++ b/utils/test-crear-formulario.ts @@ -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')); \ No newline at end of file