7 Commits

Author SHA1 Message Date
jalvarado 2af1b23f4d qr genereta 2025-04-02 13:54:50 -06:00
eithan f9549529b9 Merge pull request 'eithan' (#7) from eithan into develop
Reviewed-on: #7
2025-04-02 12:44:32 -06:00
Your Name a16e82005e formato correcto de cuestionario por verificar 2025-04-02 12:43:37 -06:00
Your Name a1f52daa8a swagger 2025-04-02 11:50:08 -06:00
Your Name bf7c0e93a8 variables de entorno, correcciones de codigo actual, docker file 2025-04-02 11:14:45 -06:00
eithan 60c75f437f Merge pull request 'santiago' (#6) from santiago into develop
Reviewed-on: #6
2025-04-02 10:31:24 -06:00
santiago 7b72ee4ec6 Cambios en las relaciones de evento, participante_evento y participante. 2025-04-02 09:15:14 -06:00
73 changed files with 2805 additions and 1567 deletions
+18
View File
@@ -0,0 +1,18 @@
# Etapa de desarrollo para ejecutar en watch mode
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
# Asegurarse de compilar el proyecto
RUN npm run build
# Puerto expuesto
EXPOSE 4200
# Comando para iniciar en modo desarrollo
CMD ["npm", "run", "start:dev"]
+33
View File
@@ -0,0 +1,33 @@
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
restart: unless-stopped
api:
build:
context: .
dockerfile: Dockerfile
container_name: formularios_api
env_file:
- .env
volumes:
- .:/app
- /app/node_modules
ports:
- "4200:4200"
depends_on:
- mariadb
restart: unless-stopped
volumes:
mariadb_data:
+237 -16
View File
@@ -9,16 +9,19 @@
"version": "0.0.1", "version": "0.0.1",
"license": "UNLICENSED", "license": "UNLICENSED",
"dependencies": { "dependencies": {
"@nestjs/common": "^11.0.1", "@nestjs/common": "^11.0.12",
"@nestjs/config": "^4.0.2", "@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.0.1", "@nestjs/core": "^11.0.1",
"@nestjs/mapped-types": "*", "@nestjs/mapped-types": "*",
"@nestjs/platform-express": "^11.0.1", "@nestjs/platform-express": "^11.0.1",
"@nestjs/swagger": "^11.1.0", "@nestjs/swagger": "^11.1.0",
"@nestjs/typeorm": "^11.0.0", "@nestjs/typeorm": "^11.0.0",
"axios": "^1.8.4",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.14.1", "class-validator": "^0.14.1",
"dotenv": "^16.4.7",
"mysql2": "^3.14.0", "mysql2": "^3.14.0",
"qrcode": "^1.5.4",
"reflect-metadata": "^0.2.2", "reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1", "rxjs": "^7.8.1",
"typeorm": "^0.3.21" "typeorm": "^0.3.21"
@@ -34,6 +37,7 @@
"@types/express": "^5.0.0", "@types/express": "^5.0.0",
"@types/jest": "^29.5.14", "@types/jest": "^29.5.14",
"@types/node": "^22.10.7", "@types/node": "^22.10.7",
"@types/qrcode": "^1.5.5",
"@types/supertest": "^6.0.2", "@types/supertest": "^6.0.2",
"eslint": "^9.18.0", "eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1", "eslint-config-prettier": "^10.0.1",
@@ -47,7 +51,7 @@
"ts-loader": "^9.5.2", "ts-loader": "^9.5.2",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0", "tsconfig-paths": "^4.2.0",
"typescript": "^5.7.3", "typescript": "^5.8.2",
"typescript-eslint": "^8.20.0" "typescript-eslint": "^8.20.0"
} }
}, },
@@ -3280,6 +3284,16 @@
"undici-types": "~6.20.0" "undici-types": "~6.20.0"
} }
}, },
"node_modules/@types/qrcode": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.5.tgz",
"integrity": "sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/qs": { "node_modules/@types/qs": {
"version": "6.9.18", "version": "6.9.18",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz",
@@ -4191,7 +4205,6 @@
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/aws-ssl-profiles": { "node_modules/aws-ssl-profiles": {
@@ -4203,6 +4216,17 @@
"node": ">= 6.0.0" "node": ">= 6.0.0"
} }
}, },
"node_modules/axios": {
"version": "1.8.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz",
"integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/b4a": { "node_modules/b4a": {
"version": "1.6.7", "version": "1.6.7",
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz",
@@ -4693,7 +4717,6 @@
"version": "5.3.1", "version": "5.3.1",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=6" "node": ">=6"
@@ -4974,7 +4997,6 @@
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"delayed-stream": "~1.0.0" "delayed-stream": "~1.0.0"
@@ -5217,6 +5239,15 @@
} }
} }
}, },
"node_modules/decamelize": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/decompress-response": { "node_modules/decompress-response": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
@@ -5305,7 +5336,6 @@
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=0.4.0" "node": ">=0.4.0"
@@ -5380,6 +5410,12 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0" "node": "^14.15.0 || ^16.10.0 || >=18.0.0"
} }
}, },
"node_modules/dijkstrajs": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
"license": "MIT"
},
"node_modules/dotenv": { "node_modules/dotenv": {
"version": "16.4.7", "version": "16.4.7",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
@@ -5549,7 +5585,6 @@
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"es-errors": "^1.3.0", "es-errors": "^1.3.0",
@@ -6327,6 +6362,26 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/follow-redirects": {
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/foreground-child": { "node_modules/foreground-child": {
"version": "3.3.1", "version": "3.3.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
@@ -6440,7 +6495,6 @@
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"asynckit": "^0.4.0", "asynckit": "^0.4.0",
@@ -6466,7 +6520,6 @@
"version": "1.52.0", "version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.6" "node": ">= 0.6"
@@ -6476,7 +6529,6 @@
"version": "2.1.35", "version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"mime-db": "1.52.0" "mime-db": "1.52.0"
@@ -6830,7 +6882,6 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"has-symbols": "^1.0.3" "has-symbols": "^1.0.3"
@@ -8893,7 +8944,6 @@
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=6" "node": ">=6"
@@ -8950,7 +9000,6 @@
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=8" "node": ">=8"
@@ -9168,6 +9217,15 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/pngjs": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
"license": "MIT",
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/prelude-ls": { "node_modules/prelude-ls": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -9268,6 +9326,12 @@
"node": ">= 0.10" "node": ">= 0.10"
} }
}, },
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/punycode": { "node_modules/punycode": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -9295,6 +9359,148 @@
], ],
"license": "MIT" "license": "MIT"
}, },
"node_modules/qrcode": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
"integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
"license": "MIT",
"dependencies": {
"dijkstrajs": "^1.0.1",
"pngjs": "^5.0.0",
"yargs": "^15.3.1"
},
"bin": {
"qrcode": "bin/qrcode"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/qrcode/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/qrcode/node_modules/cliui": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
"license": "ISC",
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
"wrap-ansi": "^6.2.0"
}
},
"node_modules/qrcode/node_modules/find-up": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
"license": "MIT",
"dependencies": {
"locate-path": "^5.0.0",
"path-exists": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/qrcode/node_modules/locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
"license": "MIT",
"dependencies": {
"p-locate": "^4.1.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/qrcode/node_modules/p-limit": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
"license": "MIT",
"dependencies": {
"p-try": "^2.0.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/qrcode/node_modules/p-locate": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
"license": "MIT",
"dependencies": {
"p-limit": "^2.2.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/qrcode/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/qrcode/node_modules/y18n": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
"license": "ISC"
},
"node_modules/qrcode/node_modules/yargs": {
"version": "15.4.1",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
"license": "MIT",
"dependencies": {
"cliui": "^6.0.0",
"decamelize": "^1.2.0",
"find-up": "^4.1.0",
"get-caller-file": "^2.0.1",
"require-directory": "^2.1.1",
"require-main-filename": "^2.0.0",
"set-blocking": "^2.0.0",
"string-width": "^4.2.0",
"which-module": "^2.0.0",
"y18n": "^4.0.0",
"yargs-parser": "^18.1.2"
},
"engines": {
"node": ">=8"
}
},
"node_modules/qrcode/node_modules/yargs-parser": {
"version": "18.1.3",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
"license": "ISC",
"dependencies": {
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/qs": { "node_modules/qs": {
"version": "6.13.0", "version": "6.13.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
@@ -9467,6 +9673,12 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/require-main-filename": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
"license": "ISC"
},
"node_modules/resolve": { "node_modules/resolve": {
"version": "1.22.10", "version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
@@ -9827,6 +10039,12 @@
"node": ">= 18" "node": ">= 18"
} }
}, },
"node_modules/set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
"license": "ISC"
},
"node_modules/setprototypeof": { "node_modules/setprototypeof": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
@@ -11642,6 +11860,12 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/which-module": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
"license": "ISC"
},
"node_modules/word-wrap": { "node_modules/word-wrap": {
"version": "1.2.5", "version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
@@ -11656,7 +11880,6 @@
"version": "6.2.0", "version": "6.2.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ansi-styles": "^4.0.0", "ansi-styles": "^4.0.0",
@@ -11710,7 +11933,6 @@
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=8" "node": ">=8"
@@ -11720,7 +11942,6 @@
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ansi-regex": "^5.0.1" "ansi-regex": "^5.0.1"
+6 -2
View File
@@ -20,16 +20,19 @@
"test:e2e": "jest --config ./test/jest-e2e.json" "test:e2e": "jest --config ./test/jest-e2e.json"
}, },
"dependencies": { "dependencies": {
"@nestjs/common": "^11.0.1", "@nestjs/common": "^11.0.12",
"@nestjs/config": "^4.0.2", "@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.0.1", "@nestjs/core": "^11.0.1",
"@nestjs/mapped-types": "*", "@nestjs/mapped-types": "*",
"@nestjs/platform-express": "^11.0.1", "@nestjs/platform-express": "^11.0.1",
"@nestjs/swagger": "^11.1.0", "@nestjs/swagger": "^11.1.0",
"@nestjs/typeorm": "^11.0.0", "@nestjs/typeorm": "^11.0.0",
"axios": "^1.8.4",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.14.1", "class-validator": "^0.14.1",
"dotenv": "^16.4.7",
"mysql2": "^3.14.0", "mysql2": "^3.14.0",
"qrcode": "^1.5.4",
"reflect-metadata": "^0.2.2", "reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1", "rxjs": "^7.8.1",
"typeorm": "^0.3.21" "typeorm": "^0.3.21"
@@ -45,6 +48,7 @@
"@types/express": "^5.0.0", "@types/express": "^5.0.0",
"@types/jest": "^29.5.14", "@types/jest": "^29.5.14",
"@types/node": "^22.10.7", "@types/node": "^22.10.7",
"@types/qrcode": "^1.5.5",
"@types/supertest": "^6.0.2", "@types/supertest": "^6.0.2",
"eslint": "^9.18.0", "eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1", "eslint-config-prettier": "^10.0.1",
@@ -58,7 +62,7 @@
"ts-loader": "^9.5.2", "ts-loader": "^9.5.2",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0", "tsconfig-paths": "^4.2.0",
"typescript": "^5.7.3", "typescript": "^5.8.2",
"typescript-eslint": "^8.20.0" "typescript-eslint": "^8.20.0"
}, },
"jest": { "jest": {
+40 -11
View File
@@ -1,4 +1,5 @@
import { forwardRef, Module } from '@nestjs/common'; //import { Module } from '@nestjs/common;
import { Module } from '@nestjs/common';
import { AppController } from './app.controller'; import { AppController } from './app.controller';
import { AppService } from './app.service'; import { AppService } from './app.service';
import { CuestionarioModule } from './cuestionario/cuestionario.module'; import { CuestionarioModule } from './cuestionario/cuestionario.module';
@@ -15,15 +16,22 @@ import { CuestionarioRespondidoModule } from './cuestionario_respondido/cuestion
import { RespuestaParticipanteAbiertaModule } from './respuesta_participante_abierta/respuesta_participante_abierta.module'; import { RespuestaParticipanteAbiertaModule } from './respuesta_participante_abierta/respuesta_participante_abierta.module';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule } from '@nestjs/config'; import { ConfigModule } from '@nestjs/config';
import { EventoModule } from './evento/evento.module';
import { TipoUserModule } from './tipo_user/tipo_user.module';
import { ParticipanteModule } from './participante/participante.module'; import { 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({ @Module({
imports: [ imports: [
ConfigModule.forRoot(), ConfigModule.forRoot(),
TypeOrmModule.forRoot({ TypeOrmModule.forRoot({
type: 'mysql', type: 'mysql',
host: process.env.db_host, host: process.env.db_host, //process.env.db_host
username: process.env.db_username, username: process.env.db_username,
database: process.env.db_database, database: process.env.db_database,
password: process.env.db_password, password: process.env.db_password,
@@ -33,22 +41,43 @@ import { ParticipanteModule } from './participante/participante.module';
// logging: true, // Habilita los logs para depuración // logging: true, // Habilita los logs para depuración
autoLoadEntities: true, // Carga automáticamente las entidades autoLoadEntities: true, // Carga automáticamente las entidades
//esta es mi base de datos local
/*
type: 'mysql',
host: 'localhost',
port: 3306, //3306
username: 'root',
password: 'admin', //admin
database: 'cidwa', //nestdb
*/
// entities: [__dirname + '/**/*.entity{.ts,.js}'],
// synchronize: true,
//extra
retryAttempts: 10, // Intentos para reconectar
retryDelay: 3000, // Tiempo entre reintentos
}), }),
forwardRef(() => CuestionarioModule), CuestionarioModule,
forwardRef(() => SeccionModule),
forwardRef(() => PreguntaModule),
forwardRef(() => OpcionModule),
ParticipanteModule,
TipoCuestionarioModule, TipoCuestionarioModule,
CuestionarioSeccionModule, CuestionarioSeccionModule,
SeccionModule,
SeccionPreguntaModule, SeccionPreguntaModule,
PreguntaModule,
TipoPreguntaModule, TipoPreguntaModule,
PreguntaOpcionModule, OpcionModule, PreguntaOpcionModule,
RespuestaParticipanteCerradaModule, RespuestaParticipanteCerradaModule,
CuestionarioRespondidoModule, CuestionarioRespondidoModule,
RespuestaParticipanteAbiertaModule], RespuestaParticipanteAbiertaModule,
EventoModule,
TipoUserModule,
ParticipanteModule,
QrModule,
AdministradorModule,
AsistenciaModule,
ParticipanteEventoModule,
],
controllers: [AppController], controllers: [AppController],
providers: [AppService], providers: [AppService],
+26 -194
View File
@@ -1,216 +1,48 @@
import { import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common';
Controller,
Post,
Body,
Get,
Param,
Put,
Delete,
HttpStatus,
HttpException,
Query,
} from '@nestjs/common';
import { CuestionarioService } from './cuestionario.service'; import { CuestionarioService } from './cuestionario.service';
import { CreateCuestionarioDto } from './dto/create-cuestionario.dto'; import { CreateCuestionarioDto } from './dto/create-cuestionario.dto';
import { UpdateCuestionarioDto } from './dto/update-cuestionario.dto'; import { UpdateCuestionarioDto } from './dto/update-cuestionario.dto';
import { import { CuestionarioApiDocumentation } from './cuestionario.documentation';
ApiTags, import { ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger';
ApiOperation,
ApiResponse,
ApiParam,
ApiBody,
ApiQuery,
} from '@nestjs/swagger';
import { SeccionExistenteCuestionarioDto } from './dto/seccion-existente-cuestionario.dto';
@ApiTags('Cuestionarios') @Controller('cuestionario')
@Controller('cuestionarios') @CuestionarioApiDocumentation.ApiController
export class CuestionarioController { export class CuestionarioController {
constructor(private readonly cuestionarioService: CuestionarioService) {} constructor(private readonly cuestionarioService: CuestionarioService) {}
@Post() @Post()
@ApiOperation({ summary: 'Crear un nuevo cuestionario' }) @CuestionarioApiDocumentation.ApiCreate
@ApiBody({ type: CreateCuestionarioDto }) create(@Body() createCuestionarioDto: CreateCuestionarioDto) {
@ApiResponse({ return this.cuestionarioService.create(createCuestionarioDto);
status: HttpStatus.CREATED,
description: 'Cuestionario creado exitosamente con sus secciones y preguntas',
})
@ApiResponse({
status: HttpStatus.BAD_REQUEST,
description: 'Datos de entrada inválidos',
})
async create(@Body() createCuestionarioDto: CreateCuestionarioDto) {
try {
return await this.cuestionarioService.create(createCuestionarioDto);
} catch (error) {
throw new HttpException(
{
status: HttpStatus.BAD_REQUEST,
error: error.message,
},
HttpStatus.BAD_REQUEST,
);
}
} }
@Get() @Get()
@ApiOperation({ summary: 'Obtener todos los cuestionarios' }) @CuestionarioApiDocumentation.ApiGetAll
@ApiQuery({ findAll() {
name: 'tipo',
required: false,
description: 'Filtrar por tipo de cuestionario',
})
@ApiResponse({
status: HttpStatus.OK,
description: 'Lista de cuestionarios obtenida exitosamente',
})
async findAll(@Query('tipo') tipoCuestionarioId?: number) {
if (tipoCuestionarioId) {
return this.cuestionarioService.findAllByTipo(tipoCuestionarioId);
}
return this.cuestionarioService.findAll(); return this.cuestionarioService.findAll();
} }
@Get(':id') @Get(':id')
@ApiOperation({ summary: 'Obtener un cuestionario específico con todas sus relaciones' }) @CuestionarioApiDocumentation.ApiGetOne
@ApiParam({ name: 'id', description: 'ID del cuestionario' }) findOne(@Param('id') id: string) {
@ApiResponse({ return this.cuestionarioService.findOne(+id);
status: HttpStatus.OK,
description: 'Cuestionario encontrado con todas sus secciones y preguntas',
})
@ApiResponse({
status: HttpStatus.NOT_FOUND,
description: 'Cuestionario no encontrado',
})
async findOne(@Param('id') id: string) {
const cuestionario = await this.cuestionarioService.findOneWithRelations(+id);
if (!cuestionario) {
throw new HttpException(
'Cuestionario no encontrado',
HttpStatus.NOT_FOUND,
);
}
return cuestionario;
} }
@Put(':id') @Get(':id/formulario')
@ApiOperation({ summary: 'Actualizar los metadatos de un cuestionario' }) @CuestionarioApiDocumentation.ApiGetFormulario
@ApiParam({ name: 'id', description: 'ID del cuestionario a actualizar' }) findFormulario(@Param('id') id: string) {
@ApiBody({ type: UpdateCuestionarioDto }) return this.cuestionarioService.findCompleto(+id);
@ApiResponse({ }
status: HttpStatus.OK,
description: 'Cuestionario actualizado exitosamente', @Patch(':id')
}) @CuestionarioApiDocumentation.ApiUpdate
@ApiResponse({ update(@Param('id') id: string, @Body() updateCuestionarioDto: UpdateCuestionarioDto) {
status: HttpStatus.NOT_FOUND, return this.cuestionarioService.update(+id, updateCuestionarioDto);
description: 'Cuestionario no encontrado',
})
async update(
@Param('id') id: string,
@Body() updateCuestionarioDto: UpdateCuestionarioDto,
) {
try {
return await this.cuestionarioService.update(+id, updateCuestionarioDto);
} catch (error) {
throw new HttpException(
{
status: HttpStatus.BAD_REQUEST,
error: error.message,
},
HttpStatus.BAD_REQUEST,
);
}
} }
@Delete(':id') @Delete(':id')
@ApiOperation({ summary: 'Eliminar un cuestionario y todas sus relaciones' }) @CuestionarioApiDocumentation.ApiRemove
@ApiParam({ name: 'id', description: 'ID del cuestionario a eliminar' }) remove(@Param('id') id: string) {
@ApiResponse({ return this.cuestionarioService.remove(+id);
status: HttpStatus.OK,
description: 'Cuestionario eliminado exitosamente',
})
@ApiResponse({
status: HttpStatus.NOT_FOUND,
description: 'Cuestionario no encontrado',
})
async remove(@Param('id') id: string) {
try {
return await this.cuestionarioService.remove(+id);
} catch (error) {
throw new HttpException(
{
status: HttpStatus.BAD_REQUEST,
error: error.message,
},
HttpStatus.BAD_REQUEST,
);
}
} }
@Post(':id/secciones')
@ApiOperation({ summary: 'Agregar una sección existente al cuestionario' })
@ApiParam({ name: 'id', description: 'ID del cuestionario' })
@ApiBody({ type: SeccionExistenteCuestionarioDto })
@ApiResponse({
status: HttpStatus.CREATED,
description: 'Sección agregada al cuestionario exitosamente',
})
@ApiResponse({
status: HttpStatus.NOT_FOUND,
description: 'Cuestionario o sección no encontrada',
})
async agregarSeccion(
@Param('id') cuestionarioId: string,
@Body() seccionDto: SeccionExistenteCuestionarioDto,
) {
try {
return await this.cuestionarioService.agregarSeccionExistente(
+cuestionarioId,
seccionDto.id_seccion,
seccionDto.posicion,
);
} catch (error) {
throw new HttpException(
{
status: error.status || HttpStatus.BAD_REQUEST,
error: error.message,
},
error.status || HttpStatus.BAD_REQUEST,
);
}
}
@Delete(':cuestionarioId/secciones/:seccionId')
@ApiOperation({ summary: 'Remover una sección del cuestionario' })
@ApiParam({ name: 'cuestionarioId', description: 'ID del cuestionario' })
@ApiParam({ name: 'seccionId', description: 'ID de la sección a remover' })
@ApiResponse({
status: HttpStatus.OK,
description: 'Sección removida del cuestionario exitosamente',
})
@ApiResponse({
status: HttpStatus.NOT_FOUND,
description: 'Relación no encontrada',
})
async removerSeccion(
@Param('cuestionarioId') cuestionarioId: string,
@Param('seccionId') seccionId: string,
) {
try {
return await this.cuestionarioService.eliminarSeccionDeCuestionario(
+cuestionarioId,
+seccionId,
);
} catch (error) {
throw new HttpException(
{
status: error.status || HttpStatus.BAD_REQUEST,
error: error.message,
},
error.status || HttpStatus.BAD_REQUEST,
);
}
}
} }
@@ -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 } 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'
}),
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',
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'
}),
ApiBody({
description: 'Datos del cuestionario a crear',
type: FormularioDto,
examples: {
feriaSexualidad: {
summary: 'Ejemplo de cuestionario para feria de sexualidad',
description: 'Cuestionario con secciones, preguntas y opciones para feria de sexualidad',
value: feriaSexualidad
}
}
}),
ApiResponse({
status: 201,
description: 'Cuestionario creado correctamente',
schema: {
properties: {
success: { type: 'boolean', example: true },
cuestionario: {
type: 'object',
properties: {
id_cuestionario: { type: 'number', example: 1 },
nombre_form: { type: 'string', example: 'Registro para la Feria de la Sexualidad - FES Acatlán' },
descripcion: { type: 'string' },
contador_secciones: { type: 'number', example: 1 },
editable: { type: 'boolean', example: true },
fecha_fin: { type: 'string', format: 'date-time' },
fecha_inicio: { type: 'string', format: 'date-time' },
id_tipo_cuestionario: { type: 'number', example: 1 }
}
}
}
}
}),
ApiResponse({ status: 400, description: 'Datos del cuestionario inválidos' }),
ApiResponse({ status: 500, description: 'Error interno del servidor' })
);
// Documentación para obtener todos los cuestionarios
static ApiGetAll = applyDecorators(
ApiOperation({
summary: 'Obtener todos los cuestionarios',
description: 'Retorna una lista de todos los cuestionarios registrados'
}),
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' })
);
}
+22 -9
View File
@@ -1,20 +1,33 @@
import { Module, forwardRef } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CuestionarioController } from './cuestionario.controller';
import { CuestionarioService } from './cuestionario.service'; import { CuestionarioService } from './cuestionario.service';
import { CuestionarioController } from './cuestionario.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Cuestionario } from './entities/cuestionario.entity'; import { Cuestionario } from './entities/cuestionario.entity';
import { Seccion } from '../seccion/entities/seccion.entity';
import { CuestionarioSeccion } from '../cuestionario_seccion/entities/cuestionario_seccion.entity'; import { CuestionarioSeccion } from '../cuestionario_seccion/entities/cuestionario_seccion.entity';
import { SeccionModule } from '../seccion/seccion.module'; import { Pregunta } from '../pregunta/entities/pregunta.entity';
import { TipoCuestionarioModule } from '../tipo_cuestionario/tipo_cuestionario.module'; 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';
@Module({ @Module({
imports: [ imports: [
TypeOrmModule.forFeature([Cuestionario, CuestionarioSeccion]), TypeOrmModule.forFeature([
forwardRef(() => SeccionModule), Cuestionario,
TipoCuestionarioModule, Seccion,
CuestionarioSeccion,
Pregunta,
SeccionPregunta,
Opcion,
PreguntaOpcion,
TipoPregunta,
TipoCuestionario
])
], ],
controllers: [CuestionarioController], controllers: [CuestionarioController],
providers: [CuestionarioService], providers: [CuestionarioService],
exports: [CuestionarioService], exports: [TypeOrmModule, CuestionarioService]
}) })
export class CuestionarioModule {} export class CuestionarioModule {}
+284 -194
View File
@@ -1,221 +1,311 @@
import { Injectable, NotFoundException } from '@nestjs/common'; import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm'; import { Repository, DataSource, In } from 'typeorm';
import { Cuestionario } from './entities/cuestionario.entity'; import { CreateCuestionarioDto } from './dto/create-cuestionario.dto';
import { SeccionService } from '../seccion/seccion.service';
import { CreateCuestionarioDto, SeccionEnCuestionarioDto } from './dto/create-cuestionario.dto';
import { UpdateCuestionarioDto } from './dto/update-cuestionario.dto'; import { UpdateCuestionarioDto } from './dto/update-cuestionario.dto';
import { Cuestionario } from './entities/cuestionario.entity';
import { Seccion } from '../seccion/entities/seccion.entity'; import { Seccion } from '../seccion/entities/seccion.entity';
import { CuestionarioSeccion } from 'src/cuestionario_seccion/entities/cuestionario_seccion.entity'; import { CuestionarioSeccion } from '../cuestionario_seccion/entities/cuestionario_seccion.entity';
import { TipoCuestionario } from 'src/tipo_cuestionario/entities/tipo_cuestionario.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';
@Injectable() @Injectable()
export class CuestionarioService { export class CuestionarioService {
async findAllByTipo(tipoCuestionarioId: number): Promise<Cuestionario[]> {
return this.cuestionarioRepository.find({
where: { tipo_cuestionario: { id_tipo_cuestionario: tipoCuestionarioId } },
relations: ['tipo_cuestionario', 'cuestionarioSecciones'],
});
}
obtenerResumen(arg0: number) {
throw new Error('Method not implemented.');
}
constructor( constructor(
@InjectRepository(Cuestionario) @InjectRepository(Cuestionario)
private readonly cuestionarioRepository: Repository<Cuestionario>, private cuestionarioRepository: Repository<Cuestionario>,
@InjectRepository(TipoCuestionario) @InjectRepository(Seccion)
private readonly tipoCuestionarioRepository: Repository<TipoCuestionario>, private seccionRepository: Repository<Seccion>,
@InjectRepository(CuestionarioSeccion) @InjectRepository(CuestionarioSeccion)
private readonly cuestionarioSeccionRepository: Repository<CuestionarioSeccion>, private cuestionarioSeccionRepository: Repository<CuestionarioSeccion>,
private readonly seccionService: SeccionService, @InjectRepository(Pregunta)
private preguntaRepository: Repository<Pregunta>,
@InjectRepository(SeccionPregunta)
private seccionPreguntaRepository: Repository<SeccionPregunta>,
@InjectRepository(Opcion)
private opcionRepository: Repository<Opcion>,
@InjectRepository(PreguntaOpcion)
private preguntaOpcionRepository: Repository<PreguntaOpcion>,
@InjectRepository(TipoPregunta)
private tipoPreguntaRepository: Repository<TipoPregunta>,
private dataSource: DataSource,
) {} ) {}
async create(createCuestionarioDto: CreateCuestionarioDto) { async create(createCuestionarioDto: CreateCuestionarioDto) {
// Verificar tipo de cuestionario const queryRunner = this.dataSource.createQueryRunner();
const tipoCuestionario = await this.tipoCuestionarioRepository.findOne({ await queryRunner.connect();
where: { id_tipo_cuestionario: createCuestionarioDto.id_tipo_cuestionario }, await queryRunner.startTransaction();
});
if (!tipoCuestionario) { try {
throw new NotFoundException('Tipo de cuestionario no encontrado'); // 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;
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];
// 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 = 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();
} }
// Crear cuestionario base
const cuestionario = this.cuestionarioRepository.create({
nombre_form: createCuestionarioDto.nombre_form,
descripcion: createCuestionarioDto.descripcion,
editable: createCuestionarioDto.editable ?? true,
fecha_inicio: createCuestionarioDto.fecha_inicio,
fecha_fin: createCuestionarioDto.fecha_fin,
tipo_cuestionario: tipoCuestionario,
contador_secciones: createCuestionarioDto.secciones?.length || 0,
});
const cuestionarioGuardado = await this.cuestionarioRepository.save(cuestionario);
// Procesar secciones si existen
if (createCuestionarioDto.secciones && createCuestionarioDto.secciones.length > 0) {
await this.agregarSeccionesACuestionario(
cuestionarioGuardado.id_cuestionario,
createCuestionarioDto.secciones,
);
}
return this.findOneWithRelations(cuestionarioGuardado.id_cuestionario);
} }
private async agregarSeccionesACuestionario( findAll() {
cuestionarioId: number, return this.cuestionarioRepository.find();
secciones: SeccionEnCuestionarioDto[], }
) {
await Promise.all(
secciones.map(async (seccionDto) => {
// Crear la sección usando el servicio existente
const seccionCreada = await this.seccionService.create(seccionDto.seccion);
// Crear la relación cuestionario-sección async findOne(id: number) {
const cuestionarioSeccion = this.cuestionarioSeccionRepository.create({ const cuestionario = await this.cuestionarioRepository.findOne({
posicion: seccionDto.posicion, where: { id_cuestionario: id }
cuestionario: { id_cuestionario: cuestionarioId }, });
seccion: { id_seccion: seccionCreada?.id_seccion },
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 }
});
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'
};
// 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' }
}); });
await this.cuestionarioSeccionRepository.save(cuestionarioSeccion); // 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 un mapeo hardcodeado para tipos de pregunta
const tiposPregunta = {
1: { id_tipo: 1, tipo_pregunta: 'Cerrada' },
2: { id_tipo: 2, tipo_pregunta: 'Abierta' },
3: { id_tipo: 3, tipo_pregunta: 'Multiple' }
};
const tipoPregunta = tiposPregunta[pregunta.id_tipo_pregunta] ||
{ id_tipo: pregunta.id_tipo_pregunta, tipo_pregunta: 'Desconocido' };
// Obtener opciones para preguntas de tipo multiple/radio
const preguntaOpciones = await this.preguntaOpcionRepository.find({
where: { id_pregunta: pregunta.id_pregunta },
relations: ['opcion'],
order: { posicion: 'ASC' }
});
// 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,
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));
}
async findOneWithRelations(id: number) { // Construir objeto final respetando el formato de get_formulario_feria.ts
return this.cuestionarioRepository.findOne({ return {
where: { id_cuestionario: id }, tipo_cuestionario: {
relations: [ id_tipo_cuestionario: tipoCuestionario.id_tipo_cuestionario,
'tipo_cuestionario', tipo_cuestionario: tipoCuestionario.tipo_cuestionario
'cuestionarioSecciones',
'cuestionarioSecciones.seccion',
'cuestionarioSecciones.seccion.seccionPreguntas',
'cuestionarioSecciones.seccion.seccionPreguntas.pregunta',
'cuestionarioSecciones.seccion.seccionPreguntas.pregunta.tipo_pregunta',
'cuestionarioSecciones.seccion.seccionPreguntas.pregunta.opciones',
'cuestionarioSecciones.seccion.seccionPreguntas.pregunta.opciones.opcion',
],
});
}
async findAll() {
return this.cuestionarioRepository.find({
relations: ['tipo_cuestionario'],
});
}
async update(id: number, updateCuestionarioDto: UpdateCuestionarioDto) {
const cuestionario = await this.cuestionarioRepository.findOne({
where: { id_cuestionario: id },
});
if (!cuestionario) {
throw new NotFoundException(`Cuestionario con ID ${id} no encontrado`);
}
// Actualizar campos básicos
if (updateCuestionarioDto.nombre_form !== undefined) {
cuestionario.nombre_form = updateCuestionarioDto.nombre_form;
}
if (updateCuestionarioDto.descripcion !== undefined) {
cuestionario.descripcion = updateCuestionarioDto.descripcion;
}
if (updateCuestionarioDto.editable !== undefined) {
cuestionario.editable = updateCuestionarioDto.editable;
}
if (updateCuestionarioDto.fecha_inicio !== undefined) {
cuestionario.fecha_inicio = updateCuestionarioDto.fecha_inicio;
}
if (updateCuestionarioDto.fecha_fin !== undefined) {
cuestionario.fecha_fin = updateCuestionarioDto.fecha_fin;
}
return this.cuestionarioRepository.save(cuestionario);
}
async remove(id: number) {
// Eliminar relaciones cuestionario-sección primero
await this.cuestionarioSeccionRepository.delete({
cuestionario: { id_cuestionario: id },
});
// Luego eliminar el cuestionario
const result = await this.cuestionarioRepository.delete(id);
if (result.affected === 0) {
throw new NotFoundException(`Cuestionario con ID ${id} no encontrado`);
}
return { affected: result.affected };
}
async agregarSeccionExistente(
cuestionarioId: number,
seccionId: number,
posicion: number,
) {
// Verificar que existan ambos elementos
const cuestionario = await this.cuestionarioRepository.findOne({
where: { id_cuestionario: cuestionarioId },
});
if (!cuestionario) {
throw new NotFoundException(`Cuestionario con ID ${cuestionarioId} no encontrado`);
}
const seccion = await this.seccionService.findOneWithRelations(seccionId);
if (!seccion) {
throw new NotFoundException(`Sección con ID ${seccionId} no encontrada`);
}
// Crear la relación
const cuestionarioSeccion = this.cuestionarioSeccionRepository.create({
posicion,
cuestionario: { id_cuestionario: cuestionarioId },
seccion: { id_seccion: seccionId },
});
await this.cuestionarioSeccionRepository.save(cuestionarioSeccion);
// Actualizar contador de secciones
await this.cuestionarioRepository.increment(
{ id_cuestionario: cuestionarioId },
'contador_secciones',
1,
);
return this.findOneWithRelations(cuestionarioId);
}
async eliminarSeccionDeCuestionario(cuestionarioId: number, seccionId: number) {
const relacion = await this.cuestionarioSeccionRepository.findOne({
where: {
cuestionario: { id_cuestionario: cuestionarioId },
seccion: { id_seccion: seccionId },
}, },
}); 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,
secciones: seccionesFormateadas
}
};
}
if (!relacion) { update(id: number, updateCuestionarioDto: UpdateCuestionarioDto) {
throw new NotFoundException('La sección no está asociada a este cuestionario'); return this.cuestionarioRepository.update(id, updateCuestionarioDto);
} }
await this.cuestionarioSeccionRepository.remove(relacion); remove(id: number) {
return this.cuestionarioRepository.delete(id);
// Actualizar contador de secciones
await this.cuestionarioRepository.decrement(
{ id_cuestionario: cuestionarioId },
'contador_secciones',
1,
);
return { success: true };
} }
} }
@@ -1,6 +1,6 @@
import { IsNotEmpty, IsString, IsBoolean, IsDateString, IsNumber, IsOptional, IsArray, ValidateNested } from 'class-validator'; 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 { Type } from 'class-transformer';
import { CreateSeccionDto } from 'src/seccion/dto/create-seccion.dto';
export class CreateCuestionarioDto { export class CreateCuestionarioDto {
@IsNotEmpty() @IsNotEmpty()
@@ -12,16 +12,12 @@ export class CreateCuestionarioDto {
descripcion?: string; descripcion?: string;
@IsOptional() @IsOptional()
@IsBoolean()
editable?: boolean;
@IsNotEmpty()
@IsDateString() @IsDateString()
fecha_inicio: Date; fecha_inicio?: Date;
@IsNotEmpty() @IsOptional()
@IsDateString() @IsDateString()
fecha_fin: Date; fecha_fin?: Date;
@IsNotEmpty() @IsNotEmpty()
@IsNumber() @IsNumber()
@@ -30,17 +26,6 @@ export class CreateCuestionarioDto {
@IsOptional() @IsOptional()
@IsArray() @IsArray()
@ValidateNested({ each: true }) @ValidateNested({ each: true })
@Type(() => SeccionEnCuestionarioDto)
secciones?: SeccionEnCuestionarioDto[];
}
export class SeccionEnCuestionarioDto {
@IsNotEmpty()
@IsNumber()
posicion: number;
@IsNotEmpty()
@ValidateNested()
@Type(() => CreateSeccionDto) @Type(() => CreateSeccionDto)
seccion: CreateSeccionDto; secciones?: CreateSeccionDto[];
} }
+94
View File
@@ -0,0 +1,94 @@
import { ApiProperty } from '@nestjs/swagger';
export class OpcionDto {
@ApiProperty({
description: 'Valor de la opción',
example: 'Si'
})
valor: string;
}
export class PreguntaDto {
@ApiProperty({
description: 'Título de la pregunta',
example: '¿Eres parte de la comunidad de la FES Acatlán?'
})
titulo: string;
@ApiProperty({
description: 'Indica si la pregunta es obligatoria',
example: true
})
obligatoria: boolean;
@ApiProperty({
description: 'Tipo de pregunta (Texto, Numero, Multiple, Radio, Abierto, Fecha)',
example: 'Multiple'
})
tipo: string;
@ApiProperty({
description: 'Lista de opciones para preguntas de tipo Multiple o Radio',
type: [OpcionDto],
required: false
})
opciones?: OpcionDto[];
}
export class SeccionDto {
@ApiProperty({
description: 'Título de la sección',
example: 'Información Personal'
})
titulo: string;
@ApiProperty({
description: 'Descripción de la sección',
example: 'Proporciona tus datos personales para poder contactarte.'
})
descripcion?: string;
@ApiProperty({
description: 'Lista de preguntas que conforman la sección',
type: [PreguntaDto]
})
preguntas: PreguntaDto[];
}
export class FormularioDto {
@ApiProperty({
description: 'Nombre del formulario',
example: 'Registro para la Feria de la Sexualidad - FES Acatlán'
})
nombre_form: string;
@ApiProperty({
description: 'Descripción del formulario',
example: 'La Feria de la Sexualidad es un espacio seguro e informativo...'
})
descripcion?: string;
@ApiProperty({
description: 'Fecha de inicio del formulario',
example: '2025-03-07T00:00:01'
})
fecha_inicio?: string;
@ApiProperty({
description: 'Fecha de fin del formulario',
example: '2025-03-18T23:59:59'
})
fecha_fin?: string;
@ApiProperty({
description: 'ID del tipo de cuestionario',
example: 1
})
id_tipo_cuestionario: number;
@ApiProperty({
description: 'Secciones que conforman el formulario',
type: [SeccionDto]
})
secciones: SeccionDto[];
}
@@ -1,22 +0,0 @@
import { IsNotEmpty, IsNumber, Min } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class SeccionExistenteCuestionarioDto {
@ApiProperty({
example: 1,
description: 'Posición de la sección dentro del cuestionario',
})
@IsNotEmpty()
@IsNumber()
@Min(1)
posicion: number;
@ApiProperty({
example: 5,
description: 'ID de la sección existente a agregar',
})
@IsNotEmpty()
@IsNumber()
@Min(1)
id_seccion: number;
}
@@ -1,23 +1,4 @@
import { IsOptional, IsString, IsBoolean, IsDateString } from 'class-validator'; import { PartialType } from '@nestjs/mapped-types';
import { CreateCuestionarioDto } from './create-cuestionario.dto';
export class UpdateCuestionarioDto { export class UpdateCuestionarioDto extends PartialType(CreateCuestionarioDto) {}
@IsOptional()
@IsString()
nombre_form?: string;
@IsOptional()
@IsString()
descripcion?: string;
@IsOptional()
@IsBoolean()
editable?: boolean;
@IsOptional()
@IsDateString()
fecha_inicio?: Date;
@IsOptional()
@IsDateString()
fecha_fin?: Date;
}
@@ -1,7 +1,6 @@
import { CuestionarioRespondido } from 'src/cuestionario_respondido/entities/cuestionario_respondido.entity';
import { CuestionarioSeccion } from 'src/cuestionario_seccion/entities/cuestionario_seccion.entity'; import { CuestionarioSeccion } from 'src/cuestionario_seccion/entities/cuestionario_seccion.entity';
import { TipoCuestionario } from 'src/tipo_cuestionario/entities/tipo_cuestionario.entity'; import { TipoCuestionario } from 'src/tipo_cuestionario/entities/tipo_cuestionario.entity';
import { Entity, PrimaryGeneratedColumn, Column, OneToMany, ManyToMany, ManyToOne, JoinColumn } from 'typeorm'; import { Entity, PrimaryGeneratedColumn, Column, OneToMany, ManyToMany, ManyToOne } from 'typeorm';
@@ -17,33 +16,27 @@ export class Cuestionario {
contador_secciones: number; contador_secciones: number;
@Column({ type: 'text', nullable: true }) @Column({ type: 'text', nullable: true })
descripcion: string; descripcion?: string;
@Column({ type: 'boolean', default: true }) @Column({ type: 'boolean', default: true })
editable: boolean; editable: boolean;
@Column({ type: 'datetime', nullable: true }) @Column({ type: 'datetime', nullable: true })
fecha_fin: Date; fecha_fin?: Date;
@Column({ type: 'datetime', nullable: true }) @Column({ type: 'datetime', nullable: true })
fecha_inicio: Date; fecha_inicio?: Date;
@OneToMany(() => CuestionarioRespondido, cuestionarioRespondido => cuestionarioRespondido.cuestionario)
cuestionariosRespondidos: CuestionarioRespondido[];
@OneToMany(() => CuestionarioSeccion,(cuestionario_seccion) => cuestionario_seccion.seccion) @OneToMany(() => CuestionarioSeccion,(cuestionario_seccion) => cuestionario_seccion.id_cuestionario)
@JoinColumn()
cuestionarioSeccion:CuestionarioSeccion[] cuestionarioSeccion:CuestionarioSeccion[]
@OneToMany(()=> Cuestionario, (cuestionario) => cuestionario.cuestionario_original) @Column({ type: 'int', nullable: true })
cuestionarioOriginal: Cuestionario[]; id_cuestionario_original: number;
@ManyToOne(() => TipoCuestionario, (tipo) => tipo.cuestionarios) @Column({ type: 'int' })
@JoinColumn({ name: 'id_tipo_cuestionario' }) // Clave foránea en la base de datos id_tipo_cuestionario: number;
tipo_cuestionario: TipoCuestionario;
@ManyToOne(()=> Cuestionario, (cuestionario) => cuestionario.cuestionarioOriginal) @ManyToOne(()=> Cuestionario)
@JoinColumn({ name: 'id_cuestionario_original' }) cuestionarioOriginal: Cuestionario;
cuestionario_original: Cuestionario;
} }
@@ -1,14 +1,19 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { CuestionarioRespondidoService } from './cuestionario_respondido.service'; import { CuestionarioRespondidoService } from './cuestionario_respondido.service';
import { CuestionarioRespondidoController } from './cuestionario_respondido.controller'; import { CuestionarioRespondidoController } from './cuestionario_respondido.controller';
import { CuestionarioRespondido } from './entities/cuestionario_respondido.entity';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { CuestionarioRespondido } from './entities/cuestionario_respondido.entity';
import { CuestionarioModule } from '../cuestionario/cuestionario.module';
import { ParticipanteModule } from '../participante/participante.module';
@Module({ @Module({
imports: [
imports: [TypeOrmModule.forFeature([CuestionarioRespondido])], TypeOrmModule.forFeature([CuestionarioRespondido]),
controllers: [CuestionarioRespondidoController], CuestionarioModule,
providers: [CuestionarioRespondidoService], ParticipanteModule
exports:[CuestionarioRespondidoService, TypeOrmModule] ],
controllers: [CuestionarioRespondidoController],
providers: [CuestionarioRespondidoService],
exports: [TypeOrmModule]
}) })
export class CuestionarioRespondidoModule {} export class CuestionarioRespondidoModule {}
@@ -14,12 +14,12 @@ export class CuestionarioRespondido {
fechaRespuesta: Date; fechaRespuesta: Date;
// Relación con Participante // Relación con Participante
@ManyToOne(() => Participante, participante => participante.cuestionarioRespondidos) @ManyToOne(() => Participante, participante => participante.cuestionariosRespondidos)
@JoinColumn({ name: 'id_participante' }) @JoinColumn({ name: 'id_participante' })
participante: Participante; participante: Participante;
// Relación con Cuestionario // Relación con Cuestionario
@ManyToOne(() => Cuestionario, cuestionario => cuestionario.cuestionariosRespondidos) @ManyToOne(() => Cuestionario)
@JoinColumn({ name: 'id_cuestionario' }) @JoinColumn({ name: 'id_cuestionario' })
cuestionario: Cuestionario; cuestionario: Cuestionario;
@@ -3,11 +3,17 @@ import { CuestionarioSeccionService } from './cuestionario_seccion.service';
import { CuestionarioSeccionController } from './cuestionario_seccion.controller'; import { CuestionarioSeccionController } from './cuestionario_seccion.controller';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { CuestionarioSeccion } from './entities/cuestionario_seccion.entity'; import { CuestionarioSeccion } from './entities/cuestionario_seccion.entity';
import { CuestionarioModule } from '../cuestionario/cuestionario.module';
import { SeccionModule } from '../seccion/seccion.module';
@Module({ @Module({
imports: [TypeOrmModule.forFeature([CuestionarioSeccion])], imports: [
controllers: [CuestionarioSeccionController], TypeOrmModule.forFeature([CuestionarioSeccion]),
providers: [CuestionarioSeccionService], CuestionarioModule,
exports:[CuestionarioSeccionService, TypeOrmModule] SeccionModule
],
controllers: [CuestionarioSeccionController],
providers: [CuestionarioSeccionService],
exports: [TypeOrmModule]
}) })
export class CuestionarioSeccionModule {} export class CuestionarioSeccionModule {}
@@ -1,4 +1,4 @@
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm'; import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from 'typeorm';
import { Cuestionario } from '../../cuestionario/entities/cuestionario.entity'; // Asegúrate de importar la entidad Cuestionario import { Cuestionario } from '../../cuestionario/entities/cuestionario.entity'; // Asegúrate de importar la entidad Cuestionario
import { Seccion } from 'src/seccion/entities/seccion.entity'; import { Seccion } from 'src/seccion/entities/seccion.entity';
@@ -12,10 +12,14 @@ export class CuestionarioSeccion {
@ManyToOne(() => Cuestionario, (cuestionario) => cuestionario.cuestionarioSeccion) @ManyToOne(() => Cuestionario, (cuestionario) => cuestionario.cuestionarioSeccion)
@JoinColumn({name:'id_cuestionario'})
cuestionario: Cuestionario; cuestionario: Cuestionario;
@ManyToOne(() => Seccion, (seccion) => seccion.cuestionarioSeccion) @Column({ type: 'int' })
@JoinColumn({name:'id_seccion'}) id_cuestionario: number;
seccion:Seccion;
@ManyToOne(() => Seccion, (seccion) => seccion.seccion)
seccion: Seccion;
@Column({type:'int'})
id_seccion:number;
} }
+2 -2
View File
@@ -6,7 +6,7 @@ import { INestApplication } from '@nestjs/common';
@Module({}) @Module({})
export class DocsModule { export class DocsModule {
static setupSwagger(app: INestApplication) { static setupSwagger(app: INestApplication) {
const document = SwaggerModule.createDocument(app, swaggerConfig); const document = SwaggerModule.createDocument(app, swaggerConfig);
SwaggerModule.setup('api-docs', app, document); SwaggerModule.setup('api-docs', app, document);
} }
} }
+23 -3
View File
@@ -1,11 +1,31 @@
import { DocumentBuilder } from '@nestjs/swagger'; import { DocumentBuilder } from '@nestjs/swagger';
export const swaggerConfig = new DocumentBuilder() export const swaggerConfig = new DocumentBuilder()
.setTitle('Sistema de Registro de Usuarios y Eventos') .setTitle('Sistema de Formularios y Eventos CIDWA')
.setDescription('API para gestionar usuarios, eventos, asistencia y códigos QR') .setDescription(`
API para la gestión de formularios, cuestionarios, eventos y asistencias.
## Funcionalidades principales
- **Cuestionarios**: Creación y gestión de formularios con secciones y preguntas
- **Eventos**: Registro y control de eventos
- **Participantes**: Gestión de usuarios y participantes
- **Asistencia**: Control de asistencia a eventos
- **QR**: Generación y validación de códigos QR
## Ejemplos de uso
La API permite crear cuestionarios complejos con secciones y diferentes tipos de preguntas.
Consulte la documentación de cada endpoint para ver ejemplos específicos.
`)
.setVersion('1.0') .setVersion('1.0')
.addTag('Cuestionarios')
.addTag('Secciones')
.addTag('Preguntas')
.addTag('Opciones')
.addTag('Usuarios') .addTag('Usuarios')
.addTag('Eventos') .addTag('Eventos')
.addTag('Asistencia') .addTag('Asistencia')
.addTag('QR') .addTag('QR')
.addBearerAuth()
.build(); .build();
+8 -5
View File
@@ -1,5 +1,6 @@
import { Participante } from "src/participante/participante.entity";
import { ParticipanteEvento } from "src/participante_evento/participante_evento.entity"; import { ParticipanteEvento } from "src/participante_evento/participante_evento.entity";
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm"; import { Column, Entity, ManyToMany, OneToMany, PrimaryGeneratedColumn } from "typeorm";
@Entity() @Entity()
export class Evento { export class Evento {
@@ -26,13 +27,15 @@ export class Evento {
@JoinColumn({ name: "id_administrador" }) @JoinColumn({ name: "id_administrador" })
administrador: Administrador; administrador: Administrador;
@OneToMany(() => ParticipanteEvento, (pe) => pe.evento)
participantes: ParticipanteEvento[];
@OneToMany(() => Asistencia, (asistencia) => asistencia.evento) @OneToMany(() => Asistencia, (asistencia) => asistencia.evento)
asistencias: Asistencia[]; asistencias: Asistencia[];
*/ */
@OneToMany(() => ParticipanteEvento, (participanteEvento) => participanteEvento.evento)
participantes: ParticipanteEvento[];
@OneToMany(() => ParticipanteEvento, participanteEvento => participanteEvento.evento)
participanteEventos: ParticipanteEvento[];
} }
+2 -1
View File
@@ -7,6 +7,7 @@ import { Evento } from './evento.entity';
@Module({ @Module({
imports: [TypeOrmModule.forFeature([Evento])], imports: [TypeOrmModule.forFeature([Evento])],
controllers: [EventoController], controllers: [EventoController],
providers: [EventoService] providers: [EventoService],
exports: [EventoService]
}) })
export class EventoModule {} export class EventoModule {}
+51 -1
View File
@@ -1,8 +1,58 @@
import { NestFactory } from '@nestjs/core'; import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module'; import { AppModule } from './app.module';
import 'dotenv/config';
import { DocsModule } from './docs/docs.module';
import { DataSource } from 'typeorm';
import { TipoPregunta } from './tipo_pregunta/entities/tipo_pregunta.entity';
import { ValidationPipe } from '@nestjs/common';
async function bootstrap() { async function bootstrap() {
const app = await NestFactory.create(AppModule); const app = await NestFactory.create(AppModule);
await app.listen(process.env.PORT ?? 3000);
// Configuración de validación global
app.useGlobalPipes(
new ValidationPipe({
whitelist: true, // Elimina propiedades no decoradas
forbidNonWhitelisted: true, // Arroja error si hay propiedades no decoradas
transform: true, // Transforma los datos recibidos al tipo definido en el DTO
})
);
// CORS configuration
app.enableCors({
origin: '*',
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
allowedHeaders: ['Content-Type', 'Authorization'],
});
// Swagger setup
DocsModule.setupSwagger(app);
// Seed TipoPregunta data
const dataSource = app.get(DataSource);
const tiposPreguntaRepository = dataSource.getRepository(TipoPregunta);
// Check if data exists
const count = await tiposPreguntaRepository.count();
if (count === 0) {
// Create default tipos de pregunta
const tiposPregunta = [
{ tipo_pregunta: 'Texto' },
{ tipo_pregunta: 'Numero' },
{ tipo_pregunta: 'Radio' },
{ tipo_pregunta: 'Multiple' },
{ tipo_pregunta: 'Abierto' },
{ tipo_pregunta: 'Fecha' },
];
await tiposPreguntaRepository.save(tiposPregunta);
console.log('Tipos de pregunta creados');
}
// Start the server
await app.listen(process.env.PORT ?? 4200);
console.log('API en ejecución en http://localhost:4200');
console.log('Swagger disponible en http://localhost:4200/api-docs');
} }
bootstrap(); bootstrap();
+5 -7
View File
@@ -1,11 +1,9 @@
import { IsString, IsNumber, IsNotEmpty, IsOptional } from 'class-validator'; import { IsString, MaxLength } from "class-validator";
export class CreateOpcionDto { export class CreateOpcionDto {
@IsString() @IsString()
@IsNotEmpty() @MaxLength(50)
opcion: string; opcion: string;
@IsNumber()
@IsOptional()
posicion?: number;
} }
+3 -10
View File
@@ -1,11 +1,4 @@
import { IsString, IsNumber, IsOptional } from 'class-validator'; import { PartialType } from '@nestjs/mapped-types';
import { CreateOpcionDto } from './create-opcion.dto';
export class UpdateOpcionDto { export class UpdateOpcionDto extends PartialType(CreateOpcionDto) {}
@IsString()
@IsOptional()
opcion?: string;
@IsNumber()
@IsOptional()
posicion?: number;
}
+1 -1
View File
@@ -10,7 +10,7 @@ export class Opcion {
opcion: string; opcion: string;
@OneToMany(() => PreguntaOpcion, (preguntaOpcion) => preguntaOpcion.opcion) @OneToMany(() => PreguntaOpcion, (preguntaOpcion) => preguntaOpcion.opcion)
Preguntas: PreguntaOpcion[]; preguntasOpciones: PreguntaOpcion[];
} }
+17 -18
View File
@@ -2,41 +2,40 @@ import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/commo
import { OpcionService } from './opcion.service'; import { OpcionService } from './opcion.service';
import { CreateOpcionDto } from './dto/create-opcion.dto'; import { CreateOpcionDto } from './dto/create-opcion.dto';
import { UpdateOpcionDto } from './dto/update-opcion.dto'; import { UpdateOpcionDto } from './dto/update-opcion.dto';
import { Pregunta } from 'src/pregunta/entities/pregunta.entity'; import { OpcionApiDocumentation } from './opcion.documentation';
@Controller('opcion') @Controller('opcion')
@OpcionApiDocumentation.ApiController
export class OpcionController { export class OpcionController {
constructor(private readonly opcionService: OpcionService) {} constructor(private readonly opcionService: OpcionService) {}
@Post() @Post()
async create( @OpcionApiDocumentation.ApiCreate
@Param('PreguntaId')PreguntaId:number, create(@Body() createOpcionDto: CreateOpcionDto) {
@Body() createOpcionDto: CreateOpcionDto) { return this.opcionService.create(createOpcionDto);
return this.opcionService.create(PreguntaId,createOpcionDto);
} }
@Get() @Get()
async findAll( @OpcionApiDocumentation.ApiGetAll
@Param('PreguntaId')PreguntaId:number findAll() {
) { return this.opcionService.findAll();
return this.opcionService.findAllByPregunta(PreguntaId);
} }
@Get(':id') @Get(':id')
async findOne( @OpcionApiDocumentation.ApiGetOne
@Param('preguntaId') preguntaId: number, findOne(@Param('id') id: string) {
@Param('opcionId') OpcionId: number) { return this.opcionService.findOne(+id);
return this.opcionService.findOne(preguntaId, OpcionId);
} }
@Patch(':id') @Patch(':id')
async pdate(@Param('preguntaId') preguntaId:number ,@Param('opcionId') id: number, @Body() updateOpcionDto: UpdateOpcionDto) { @OpcionApiDocumentation.ApiUpdate
return this.opcionService.update(preguntaId, id,updateOpcionDto); update(@Param('id') id: string, @Body() updateOpcionDto: UpdateOpcionDto) {
return this.opcionService.update(+id, updateOpcionDto);
} }
@Delete(':id') @Delete(':id')
async remove(@Param('preguntaId') preguntaId:number ,@Param('opcionId') id: number) { @OpcionApiDocumentation.ApiRemove
return this.opcionService.remove(preguntaId, id); remove(@Param('id') id: string) {
return this.opcionService.remove(+id);
} }
} }
+153
View File
@@ -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' })
);
}
+4 -10
View File
@@ -1,19 +1,13 @@
import { Module, forwardRef } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { OpcionService } from './opcion.service'; import { OpcionService } from './opcion.service';
import { OpcionController } from './opcion.controller'; import { OpcionController } from './opcion.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Opcion } from './entities/opcion.entity'; import { Opcion } from './entities/opcion.entity';
import { PreguntaOpcion } from '../pregunta_opcion/entities/pregunta_opcion.entity';
import { PreguntaModule } from '../pregunta/pregunta.module';
import { Pregunta } from '../pregunta/entities/pregunta.entity';
@Module({ @Module({
imports: [ imports: [TypeOrmModule.forFeature([Opcion])], // Importa el repositorio de Opcion
TypeOrmModule.forFeature([Opcion, Pregunta, PreguntaOpcion]), // Importar todas las entidades necesarias
forwardRef(() => PreguntaModule), // Usar forwardRef para dependencia circular
],
controllers: [OpcionController], controllers: [OpcionController],
providers: [OpcionService], providers: [OpcionService],
exports: [OpcionService, TypeOrmModule], // Exportar TypeOrmModule si otros módulos necesitan los repositorios exports: [OpcionService, TypeOrmModule]
}) })
export class OpcionModule {} export class OpcionModule {}
+22 -140
View File
@@ -1,158 +1,40 @@
import { Injectable, NotFoundException } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { CreateOpcionDto } from './dto/create-opcion.dto';
import { UpdateOpcionDto } from './dto/update-opcion.dto';
import { Opcion } from './entities/opcion.entity';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
import { Opcion } from './entities/opcion.entity';
import { Pregunta } from '../pregunta/entities/pregunta.entity';
import { CreateOpcionDto } from './dto/create-opcion.dto';
import { PreguntaOpcion } from 'src/pregunta_opcion/entities/pregunta_opcion.entity';
import { UpdateOpcionDto } from './dto/update-opcion.dto';
@Injectable() @Injectable()
export class OpcionService { export class OpcionService {
constructor( constructor(
@InjectRepository(Opcion) @InjectRepository(Opcion)
private readonly opcionRepository: Repository<Opcion>, private repository: Repository<Opcion>,
@InjectRepository(Pregunta) ){}
private readonly preguntaRepository: Repository<Pregunta>,
@InjectRepository(PreguntaOpcion)
private readonly preguntaOpcionRepository: Repository<PreguntaOpcion>,
) {}
async create(preguntaId: number, createOpcionDto: CreateOpcionDto) { create(createOpcionDto: CreateOpcionDto) {
// Verificar que la pregunta exista y sea cerrada return this.repository.save(createOpcionDto);
const pregunta = await this.preguntaRepository.findOne({
where: { id_pregunta: preguntaId },
relations: ['tipo_pregunta'],
});
if (!pregunta) {
throw new NotFoundException('Pregunta no encontrada');
}
if (pregunta.tipo_pregunta.tipo_pregunta !== 'Cerrada') {
throw new Error('Solo se pueden agregar opciones a preguntas cerradas');
}
// Crear la opción
const opcion = this.opcionRepository.create({
opcion: createOpcionDto.opcion,
});
const savedOpcion = await this.opcionRepository.save(opcion);
// Crear la relación entre pregunta y opción
const preguntaOpcion = this.preguntaOpcionRepository.create({
pregunta: { id_pregunta: preguntaId },
opcion: savedOpcion,
posicion: createOpcionDto.posicion || 0, // Asignar posición
});
await this.preguntaOpcionRepository.save(preguntaOpcion);
// Actualizar el contador de opciones en la pregunta
await this.preguntaRepository.increment(
{ id_pregunta: preguntaId },
'contador_opcion',
1
);
return savedOpcion;
} }
async findAllByPregunta(preguntaId: number) {
return this.preguntaOpcionRepository.find({ /*
where: { pregunta: { id_pregunta: preguntaId } }, create():Promise<Opcion> {
relations: ['opcion'], return this.repository.save(this.repository.create());
order: { posicion: 'ASC' }, }
}); */
findAll() {
return `This action returns all opcion`;
} }
async findOne(preguntaId: number, id: number) { findOne(id: number) {
const preguntaOpcion = await this.preguntaOpcionRepository.findOne({ return `This action returns a #${id} opcion`;
where: {
pregunta: { id_pregunta: preguntaId },
opcion: { id_opcion: id },
},
relations: ['opcion'],
});
if (!preguntaOpcion) {
throw new NotFoundException('Opción no encontrada para esta pregunta');
}
return preguntaOpcion.opcion;
} }
async update( update(id: number, updateOpcionDto: UpdateOpcionDto) {
preguntaId: number, return `This action updates a #${id} opcion`;
id: number,
updateOpcionDto: UpdateOpcionDto
) {
// Verificar que la relación exista
const preguntaOpcion = await this.preguntaOpcionRepository.findOne({
where: {
pregunta: { id_pregunta: preguntaId },
opcion: { id_opcion: id },
},
relations: ['opcion'],
});
if (!preguntaOpcion) {
throw new NotFoundException('Opción no encontrada para esta pregunta');
}
// Actualizar posición si se proporciona
if (updateOpcionDto.posicion !== undefined) {
await this.preguntaOpcionRepository.update(
{ id_pregunta_opcion: preguntaOpcion.id_pregunta_opcion },
{ posicion: updateOpcionDto.posicion }
);
}
// Actualizar texto de la opción
if (updateOpcionDto.opcion) {
await this.opcionRepository.update(
{ id_opcion: id },
{ opcion: updateOpcionDto.opcion }
);
}
return this.findOne(preguntaId, id);
} }
async remove(preguntaId: number, id: number) { remove(id: number) {
// Verificar que la relación exista return `This action removes a #${id} opcion`;
const preguntaOpcion = await this.preguntaOpcionRepository.findOne({
where: {
pregunta: { id_pregunta: preguntaId },
opcion: { id_opcion: id },
},
});
if (!preguntaOpcion) {
throw new NotFoundException('Opción no encontrada para esta pregunta');
}
// Eliminar la relación primero
await this.preguntaOpcionRepository.remove(preguntaOpcion);
// Eliminar la opción (si no está siendo usada en otras preguntas)
const count = await this.preguntaOpcionRepository.count({
where: { opcion: { id_opcion: id } },
});
if (count === 0) {
await this.opcionRepository.delete(id);
}
// Actualizar el contador de opciones en la pregunta
await this.preguntaRepository.decrement(
{ id_pregunta: preguntaId },
'contador_opcion',
1
);
return { message: 'Opción eliminada correctamente' };
} }
} }
+18 -8
View File
@@ -1,7 +1,8 @@
import { CuestionarioRespondido } from "src/cuestionario_respondido/entities/cuestionario_respondido.entity"; import { CuestionarioRespondido } from "src/cuestionario_respondido/entities/cuestionario_respondido.entity";
import { Evento } from "src/evento/evento.entity";
import { ParticipanteEvento } from "src/participante_evento/participante_evento.entity"; import { ParticipanteEvento } from "src/participante_evento/participante_evento.entity";
import { TipoUser } from "src/tipo_user/tipo_user.entity"; import { TipoUser } from "src/tipo_user/tipo_user.entity";
import { Column, Entity, JoinColumn, ManyToOne, OneToMany, PrimaryGeneratedColumn } from "typeorm"; import { Column, Entity, JoinColumn, JoinTable, ManyToMany, ManyToOne, OneToMany, PrimaryGeneratedColumn } from "typeorm";
@Entity() @Entity()
export class Participante { export class Participante {
@@ -14,26 +15,35 @@ export class Participante {
@Column() @Column()
id_tipo_user: number id_tipo_user: number
@OneToMany(() => CuestionarioRespondido, cuestionarioRespondido => cuestionarioRespondido.participante )
cuestionarioRespondidos:CuestionarioRespondido[];
//Relacion con tipo usuario //Relacion con tipo usuario
@ManyToOne(() => TipoUser, tipoUser => tipoUser.participante) @ManyToOne(() => TipoUser, tipoUser => tipoUser.participante)
@JoinColumn({ name: 'participante_id' }) //nombre de la relacion @JoinColumn({ name: 'participante_id' }) //nombre de la relacion
tipo_user: TipoUser[] tipo_user: TipoUser
/* /*
@ManyToOne(() => Administrador, (admin) => admin.eventos) @ManyToOne(() => Administrador, (admin) => admin.eventos)
@JoinColumn({ name: "id_administrador" }) @JoinColumn({ name: "id_administrador" })
administrador: Administrador; administrador: Administrador;
@OneToMany(() => ParticipanteEvento, (pe) => pe.evento) @ManyToMany(() => Evento, evento => evento.participantes)
participantes: ParticipanteEvento[]; @JoinTable({
name: "participante_evento",
joinColumn: { name: "id_participante", referencedColumnName: "id_participante" },
inverseJoinColumn: { name: "id_evento", referencedColumnName: "id_evento" }
})
eventos: Evento[];
@OneToMany(() => Asistencia, (asistencia) => asistencia.evento) @OneToMany(() => Asistencia, (asistencia) => asistencia.evento)
asistencias: Asistencia[]; asistencias: Asistencia[];
*/ */
@OneToMany(() => CuestionarioRespondido, cuestionario => cuestionario.participante)
cuestionariosRespondidos: CuestionarioRespondido[];
@OneToMany(() => ParticipanteEvento, participanteEvento => participanteEvento.participante)
participanteEventos: ParticipanteEvento[];
} }
+3 -3
View File
@@ -3,7 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm';
import { Participante } from './participante.entity'; import { Participante } from './participante.entity';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
import { CreateParticipanteDto } from './dto/create-participante.dto'; import { CreateParticipanteDto } from './dto/create-participante.dto';
//import { UpdateAdminDto } from 'src/admin/dto/update.admin.dto';
import { UpdateParticipanteDto } from './dto/update.participante.dto'; import { UpdateParticipanteDto } from './dto/update.participante.dto';
@Injectable() @Injectable()
@@ -28,7 +28,7 @@ export class ParticipanteService {
getParticipantes() { getParticipantes() {
return this.participanteRepository.find({ return this.participanteRepository.find({
relations: ['tipo_user'] relations: ['tipo_user', 'participanteEventos']
}) })
} }
@@ -37,7 +37,7 @@ export class ParticipanteService {
where: { where: {
id_participante id_participante
}, },
relations: ['tipo_user'] relations: ['tipo_user', 'participanteEventos']
}) })
if (!participanteFound) if (!participanteFound)
@@ -1,4 +1,5 @@
import { Evento } from "src/evento/evento.entity"; import { Evento } from "src/evento/evento.entity";
import { Participante } from "src/participante/participante.entity";
import { Column, Entity, JoinColumn, ManyToOne, PrimaryColumn, PrimaryGeneratedColumn } from "typeorm"; import { Column, Entity, JoinColumn, ManyToOne, PrimaryColumn, PrimaryGeneratedColumn } from "typeorm";
@Entity() @Entity()
@@ -18,16 +19,20 @@ export class ParticipanteEvento {
@OneToOne(() => Qr, (qr) => qr.participanteEvento) @OneToOne(() => Qr, (qr) => qr.participanteEvento)
qr: Qr; qr: Qr;
@ManyToOne(() => Participante, (participante) => participante.eventos) @OneToMany(() => ParticipanteEvento, (pe) => pe.evento)
@JoinColumn({ name: "id_participante" }) participantes: ParticipanteEvento[];
participante: Participante;
@ManyToOne(() => Evento, (evento) => evento.participantes) @ManyToOne(() => Evento, (evento) => evento.participantes)
@JoinColumn({ name: "id_evento" }) @JoinColumn({ name: "id_evento" })
evento: Evento; evento: Evento;
*/ */
@ManyToOne(() => Evento, (evento) => evento.participantes) @ManyToOne(() => Participante, participante => participante.participanteEventos)
@JoinColumn({ name: "id_participante" })
participante: Participante;
@ManyToOne(() => Evento, evento => evento.participanteEventos)
@JoinColumn({ name: "id_evento" }) @JoinColumn({ name: "id_evento" })
evento: Evento; evento: Evento;
} }
@@ -4,11 +4,12 @@ import { ParticipanteEventoController } from './participante_evento.controller';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { ParticipanteEvento } from './participante_evento.entity'; import { ParticipanteEvento } from './participante_evento.entity';
import { Evento } from 'src/evento/evento.entity'; import { Evento } from 'src/evento/evento.entity';
import { Participante } from 'src/participante/participante.entity';
@Module({ @Module({
imports: [TypeOrmModule.forFeature([ParticipanteEvento]), Evento], imports: [TypeOrmModule.forFeature([ParticipanteEvento, Evento, Participante])],
controllers: [ParticipanteEventoController], controllers: [ParticipanteEventoController],
providers: [ParticipanteEventoService], providers: [ParticipanteEventoService],
exports:[TypeOrmModule,ParticipanteEventoService] exports: [ParticipanteEventoService],
}) })
export class ParticipanteEventoModule {} export class ParticipanteEventoModule {}
@@ -4,12 +4,16 @@ import { ParticipanteEvento } from './participante_evento.entity';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
import { CreateParticipanteEventoDto } from './dto/create-participante_evento.dto'; import { CreateParticipanteEventoDto } from './dto/create-participante_evento.dto';
import { UpdateParticipanteEventoDto } from './dto/update.participante_evento.dto'; import { UpdateParticipanteEventoDto } from './dto/update.participante_evento.dto';
import { Evento } from 'src/evento/evento.entity';
import { Participante } from 'src/participante/participante.entity';
@Injectable() @Injectable()
export class ParticipanteEventoService { export class ParticipanteEventoService {
constructor( constructor(
@InjectRepository(ParticipanteEvento) private participanteEventoRepository: Repository<ParticipanteEvento> @InjectRepository(ParticipanteEvento) private participanteEventoRepository: Repository<ParticipanteEvento>,
@InjectRepository(Evento) private eventoRepository: Repository<Evento>,
@InjectRepository(Participante) private participanteRepository: Repository<Participante>
) {} ) {}
async createParticipanteEvento(participanteEvento: CreateParticipanteEventoDto) { async createParticipanteEvento(participanteEvento: CreateParticipanteEventoDto) {
@@ -30,7 +34,7 @@ export class ParticipanteEventoService {
getParticipantesEvento() { getParticipantesEvento() {
return this.participanteEventoRepository.find({ return this.participanteEventoRepository.find({
relations: ['evento'] relations: ['evento', 'participante']
}) })
} }
@@ -39,7 +43,7 @@ export class ParticipanteEventoService {
where: { where: {
id_participante id_participante
}, },
relations: ['evento'] relations: ['evento', 'participante']
}) })
if (!participante_eventoFound) { if (!participante_eventoFound) {
+19 -6
View File
@@ -1,23 +1,36 @@
import { IsNotEmpty, IsString, IsBoolean, IsNumber, IsOptional, IsArray, ValidateNested } from 'class-validator';
import { Type } from 'class-transformer'; import { Type } from 'class-transformer';
import { CreateOpcionDto } from '../../opcion/dto/create-opcion.dto'; import { IsArray, IsBoolean, IsNotEmpty, IsNumber, IsOptional, IsString, ValidateNested } from 'class-validator';
export class OpcionDto {
@IsNotEmpty()
@IsString()
valor: string;
}
export class CreatePreguntaDto { export class CreatePreguntaDto {
@IsNotEmpty() @IsNotEmpty()
@IsString() @IsString()
pregunta: string; titulo: string;
@IsOptional() @IsOptional()
@IsBoolean() @IsBoolean()
obligatoria?: boolean; obligatoria?: boolean;
@IsNotEmpty() @IsNotEmpty()
@IsString()
tipo: string;
@IsOptional()
@IsNumber() @IsNumber()
id_tipo_pregunta: number; contador_opcion?: number;
@IsOptional()
@IsNumber()
id_opcion_dependiente?: number;
@IsOptional() @IsOptional()
@IsArray() @IsArray()
@ValidateNested({ each: true }) @ValidateNested({ each: true })
@Type(() => CreateOpcionDto) @Type(() => OpcionDto)
opciones?: CreateOpcionDto[]; opciones?: OpcionDto[];
} }
+3 -14
View File
@@ -1,15 +1,4 @@
import { IsOptional, IsString, IsBoolean, IsNumber } from 'class-validator'; import { PartialType } from '@nestjs/mapped-types';
import { CreatePreguntaDto } from './create-pregunta.dto';
export class UpdatePreguntaDto { export class UpdatePreguntaDto extends PartialType(CreatePreguntaDto) {}
@IsOptional()
@IsString()
pregunta?: string;
@IsOptional()
@IsBoolean()
obligatoria?: boolean;
@IsOptional()
@IsNumber()
id_tipo_pregunta?: number;
}
+15 -17
View File
@@ -1,38 +1,36 @@
import { PreguntaOpcion } from 'src/pregunta_opcion/entities/pregunta_opcion.entity'; import { PreguntaOpcion } from 'src/pregunta_opcion/entities/pregunta_opcion.entity';
import { RespuestaParticipanteAbierta } from 'src/respuesta_participante_abierta/entities/respuesta_participante_abierta.entity';
import { SeccionPregunta } from 'src/seccion_pregunta/entities/seccion_pregunta.entity'; import { SeccionPregunta } from 'src/seccion_pregunta/entities/seccion_pregunta.entity';
import { TipoPregunta } from 'src/tipo_pregunta/entities/tipo_pregunta.entity'; import { TipoPregunta } from 'src/tipo_pregunta/entities/tipo_pregunta.entity';
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, OneToMany, JoinColumn } from 'typeorm'; import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, OneToMany, JoinColumn } from 'typeorm';
@Entity('pregunta') @Entity()
export class Pregunta { export class Pregunta {
@PrimaryGeneratedColumn({ name: 'id_pregunta' }) @PrimaryGeneratedColumn()
id_pregunta: number; id_pregunta: number;
@Column({ type: 'text' }) @Column()
pregunta: string; pregunta: string;
@Column({ name: 'contador_opcion', default: 0 }) @Column({ default: 0 })
contador_opcion: number; contador_opcion: number;
@Column({ default: false }) @Column({ default: false })
obligatoria: boolean; obligatoria: boolean;
// Relación con TipoPregunta @ManyToOne(() => TipoPregunta, tipo => tipo.preguntas)
@ManyToOne(() => TipoPregunta, tipoPregunta => tipoPregunta.preguntas)
@JoinColumn({ name: 'id_tipo_pregunta' }) @JoinColumn({ name: 'id_tipo_pregunta' })
tipo_pregunta: TipoPregunta; tipoPregunta: TipoPregunta;
@Column()
id_tipo_pregunta: number;
@Column({ type: 'int', nullable: true })
id_opcion_dependiente?: number;
@OneToMany(() => SeccionPregunta, seccionPregunta => seccionPregunta.pregunta)
seccionPreguntas: SeccionPregunta[];
// Relación con PreguntaOpcion (tabla intermedia para opciones)
@OneToMany(() => PreguntaOpcion, preguntaOpcion => preguntaOpcion.pregunta) @OneToMany(() => PreguntaOpcion, preguntaOpcion => preguntaOpcion.pregunta)
opciones: PreguntaOpcion[]; opciones: PreguntaOpcion[];
// Relación con SeccionPregunta (tabla intermedia para secciones)
@OneToMany(() => SeccionPregunta, seccionPregunta => seccionPregunta.pregunta)
seccionesPregunta: SeccionPregunta[];
// Relación con RespuestasAbiertas
@OneToMany(() => RespuestaParticipanteAbierta, respuestaAbierta => respuestaAbierta.pregunta)
respuestasAbiertas: RespuestaParticipanteAbierta[];
} }
+19 -88
View File
@@ -1,110 +1,41 @@
import { import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common';
Controller,
Post,
Body,
Get,
Param,
Put,
Delete,
HttpStatus,
HttpException,
Query
} from '@nestjs/common';
import { PreguntaService } from './pregunta.service'; import { PreguntaService } from './pregunta.service';
import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiQuery } from '@nestjs/swagger';
import { CreatePreguntaDto } from './dto/create-pregunta.dto'; import { CreatePreguntaDto } from './dto/create-pregunta.dto';
import { UpdatePreguntaDto } from './dto/update-pregunta.dto'; import { UpdatePreguntaDto } from './dto/update-pregunta.dto';
import { PreguntaApiDocumentation } from './pregunta.documentation';
@ApiTags('Preguntas') @Controller('pregunta')
@Controller('preguntas') @PreguntaApiDocumentation.ApiController
export class PreguntaController { export class PreguntaController {
constructor(private readonly preguntaService: PreguntaService) {} constructor(private readonly preguntaService: PreguntaService) {}
@Post() @Post()
@ApiOperation({ summary: 'Crear una nueva pregunta' }) @PreguntaApiDocumentation.ApiCreate
@ApiResponse({ status: 201, description: 'Pregunta creada exitosamente' }) create(@Body() createPreguntaDto: CreatePreguntaDto) {
@ApiResponse({ status: 400, description: 'Datos de entrada inválidos' }) return this.preguntaService.create(createPreguntaDto);
@ApiResponse({ status: 404, description: 'Tipo de pregunta no encontrado' })
async create(@Body() createPreguntaDto: CreatePreguntaDto) {
try {
return await this.preguntaService.create(createPreguntaDto);
} catch (error) {
throw new HttpException({
status: HttpStatus.BAD_REQUEST,
error: error.message,
}, HttpStatus.BAD_REQUEST);
}
} }
@Get() @Get()
@ApiOperation({ summary: 'Obtener todas las preguntas' }) @PreguntaApiDocumentation.ApiGetAll
@ApiQuery({ name: 'tipo', required: false, description: 'Filtrar por tipo de pregunta' }) findAll() {
@ApiResponse({ status: 200, description: 'Lista de preguntas' })
async findAll(@Query('tipo') tipoPreguntaId?: number) {
if (tipoPreguntaId) {
return this.preguntaService.findAllByTipo(tipoPreguntaId);
}
return this.preguntaService.findAll(); return this.preguntaService.findAll();
} }
@Get(':id') @Get(':id')
@ApiOperation({ summary: 'Obtener una pregunta específica' }) @PreguntaApiDocumentation.ApiGetOne
@ApiParam({ name: 'id', description: 'ID de la pregunta' }) findOne(@Param('id') id: string) {
@ApiResponse({ status: 200, description: 'Pregunta encontrada' }) return this.preguntaService.findOne(+id);
@ApiResponse({ status: 404, description: 'Pregunta no encontrada' })
async findOne(@Param('id') id: string) {
const pregunta = await this.preguntaService.findOneWithRelations(+id);
if (!pregunta) {
throw new HttpException('Pregunta no encontrada', HttpStatus.NOT_FOUND);
}
return pregunta;
} }
@Put(':id') @Patch(':id')
@ApiOperation({ summary: 'Actualizar una pregunta' }) @PreguntaApiDocumentation.ApiUpdate
@ApiParam({ name: 'id', description: 'ID de la pregunta a actualizar' }) update(@Param('id') id: string, @Body() updatePreguntaDto: UpdatePreguntaDto) {
@ApiResponse({ status: 200, description: 'Pregunta actualizada' }) return this.preguntaService.update(+id, updatePreguntaDto);
@ApiResponse({ status: 404, description: 'Pregunta no encontrada' })
async update(
@Param('id') id: string,
@Body() updatePreguntaDto: UpdatePreguntaDto,
) {
try {
return await this.preguntaService.update(+id, updatePreguntaDto);
} catch (error) {
throw new HttpException({
status: HttpStatus.BAD_REQUEST,
error: error.message,
}, HttpStatus.BAD_REQUEST);
}
} }
@Delete(':id') @Delete(':id')
@ApiOperation({ summary: 'Eliminar una pregunta' }) @PreguntaApiDocumentation.ApiRemove
@ApiParam({ name: 'id', description: 'ID de la pregunta a eliminar' }) remove(@Param('id') id: string) {
@ApiResponse({ status: 200, description: 'Pregunta eliminada' }) return this.preguntaService.remove(+id);
@ApiResponse({ status: 404, description: 'Pregunta no encontrada' })
async remove(@Param('id') id: string) {
try {
return await this.preguntaService.remove(+id);
} catch (error) {
throw new HttpException({
status: HttpStatus.BAD_REQUEST,
error: error.message,
}, HttpStatus.BAD_REQUEST);
}
}
@Get(':id/opciones')
@ApiOperation({ summary: 'Obtener opciones de una pregunta' })
@ApiParam({ name: 'id', description: 'ID de la pregunta' })
@ApiResponse({ status: 200, description: 'Opciones de la pregunta' })
@ApiResponse({ status: 404, description: 'Pregunta no encontrada' })
async getOpciones(@Param('id') id: string) {
const pregunta = await this.preguntaService.findOneWithRelations(+id);
if (!pregunta) {
throw new HttpException('Pregunta no encontrada', HttpStatus.NOT_FOUND);
}
return pregunta.opciones;
} }
} }
+189
View File
@@ -0,0 +1,189 @@
import { ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
import { applyDecorators } from '@nestjs/common';
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', example: 'Multiple' },
contador_opcion: { type: 'number', example: 0 },
id_opcion_dependiente: { type: 'number', example: null },
opciones: {
type: 'array',
items: {
type: 'object',
properties: {
valor: { type: 'string', example: 'Si' }
}
}
}
}
}
}),
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 }
}
}
}),
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 }
}
}
}
}),
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 },
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' })
);
}
+14 -9
View File
@@ -1,20 +1,25 @@
import { Module, forwardRef } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Pregunta } from './entities/pregunta.entity';
import { PreguntaService } from './pregunta.service'; import { PreguntaService } from './pregunta.service';
import { PreguntaController } from './pregunta.controller'; import { PreguntaController } from './pregunta.controller';
import { OpcionModule } from '../opcion/opcion.module'; import { TypeOrmModule } from '@nestjs/typeorm';
import { PreguntaOpcion } from '../pregunta_opcion/entities/pregunta_opcion.entity'; import { Pregunta } from './entities/pregunta.entity';
import { TipoPreguntaModule } from '../tipo_pregunta/tipo_pregunta.module'; import { TipoPreguntaModule } from '../tipo_pregunta/tipo_pregunta.module';
import { TipoPregunta } from '../tipo_pregunta/entities/tipo_pregunta.entity';
import { Opcion } from '../opcion/entities/opcion.entity';
import { PreguntaOpcion } from '../pregunta_opcion/entities/pregunta_opcion.entity';
@Module({ @Module({
imports: [ imports: [
TypeOrmModule.forFeature([Pregunta, PreguntaOpcion]), TypeOrmModule.forFeature([
forwardRef(() => OpcionModule), Pregunta,
TipoPreguntaModule, TipoPregunta,
Opcion,
PreguntaOpcion
]),
TipoPreguntaModule
], ],
controllers: [PreguntaController], controllers: [PreguntaController],
providers: [PreguntaService], providers: [PreguntaService],
exports: [PreguntaService], exports: [TypeOrmModule, PreguntaService]
}) })
export class PreguntaModule {} export class PreguntaModule {}
+227 -130
View File
@@ -1,157 +1,254 @@
import { Injectable, NotFoundException } from '@nestjs/common'; import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from '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 { Pregunta } from './entities/pregunta.entity';
import { OpcionService } from '../opcion/opcion.service'; import { TipoPregunta } from '../tipo_pregunta/entities/tipo_pregunta.entity';
import { CreatePreguntaDto } from './dto/create-pregunta.dto'; import { Opcion } from '../opcion/entities/opcion.entity';
import { CreateOpcionDto } from '../opcion/dto/create-opcion.dto'; import { PreguntaOpcion } from '../pregunta_opcion/entities/pregunta_opcion.entity';
import { TipoPregunta } from 'src/tipo_pregunta/entities/tipo_pregunta.entity';
@Injectable() @Injectable()
export class PreguntaService { export class PreguntaService {
constructor( constructor(
@InjectRepository(Pregunta) @InjectRepository(Pregunta)
private readonly preguntaRepository: Repository<Pregunta>, private preguntaRepository: Repository<Pregunta>,
@InjectRepository(TipoPregunta) @InjectRepository(TipoPregunta)
private readonly tipoPreguntaRepository: Repository<TipoPregunta>, private tipoPreguntaRepository: Repository<TipoPregunta>,
private readonly opcionService: OpcionService, @InjectRepository(Opcion)
private opcionRepository: Repository<Opcion>,
@InjectRepository(PreguntaOpcion)
private preguntaOpcionRepository: Repository<PreguntaOpcion>,
private dataSource: DataSource
) {} ) {}
async create(createPreguntaDto: CreatePreguntaDto) { async create(createPreguntaDto: CreatePreguntaDto) {
// Verificar que el tipo de pregunta exista const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
//Se espera que solo exista el ripo de pregunta abierta y cerrada try {
const tipoPregunta = await this.tipoPreguntaRepository.findOne({ // Obtener el tipo de pregunta por su nombre
where: { id_tipo: createPreguntaDto.id_tipo_pregunta },
});
if (!tipoPregunta) {
throw new NotFoundException(
`Tipo de pregunta con ID ${createPreguntaDto.id_tipo_pregunta} no encontrado`,
);
}
// Crear la pregunta base
const pregunta = this.preguntaRepository.create({
pregunta: createPreguntaDto.pregunta,
obligatoria: createPreguntaDto.obligatoria || false,
tipo_pregunta: tipoPregunta,
});
const preguntaGuardada = await this.preguntaRepository.save(pregunta);
// Si es pregunta cerrada y tiene opciones, crearlas
if (
tipoPregunta.tipo_pregunta === 'Cerrada' &&
createPreguntaDto.opciones &&
createPreguntaDto.opciones.length > 0
) {
await this.agregarOpcionesAPregunta(
preguntaGuardada.id_pregunta,
createPreguntaDto.opciones,
);
// Actualizar el contador de opciones
await this.preguntaRepository.update(
{ id_pregunta: preguntaGuardada.id_pregunta },
{ contador_opcion: createPreguntaDto.opciones.length },
);
}
return this.findOneWithRelations(preguntaGuardada.id_pregunta);
}
private async agregarOpcionesAPregunta(
preguntaId: number,
opciones: CreateOpcionDto[],
) {
for (const opcionDto of opciones) {
await this.opcionService.create(preguntaId, opcionDto);
}
}
async findOneWithRelations(id: number) {
return this.preguntaRepository.findOne({
where: { id_pregunta: id },
relations: [
'tipo_pregunta',
'opciones',
'opciones.opcion',
'seccionesPregunta',
'seccionesPregunta.seccion',
],
});
}
async findAllByTipo(tipoPreguntaId: number) {
return this.preguntaRepository.find({
where: { tipo_pregunta: { id_tipo: tipoPreguntaId } },
relations: ['opciones', 'opciones.opcion'],
});
}
async remove(id: number) {
const pregunta = await this.preguntaRepository.findOne({
where: { id_pregunta: id },
relations: ['opciones'],
});
if (!pregunta) {
throw new NotFoundException(`Pregunta con ID ${id} no encontrada`);
}
// Eliminar opciones primero si existen
if (pregunta.opciones && pregunta.opciones.length > 0) {
for (const opcion of pregunta.opciones) {
await this.opcionService.remove(id, opcion.id_pregunta_opcion);
}
}
// Eliminar la pregunta
await this.preguntaRepository.delete(id);
return { message: 'Pregunta eliminada correctamente' };
}
async update(id: number, updatePreguntaDto: any) {
const pregunta = await this.preguntaRepository.findOne({
where: { id_pregunta: id },
});
if (!pregunta) {
throw new NotFoundException(`Pregunta con ID ${id} no encontrada`);
}
// Actualizar campos básicos
if (updatePreguntaDto.pregunta !== undefined) {
pregunta.pregunta = updatePreguntaDto.pregunta;
}
if (updatePreguntaDto.obligatoria !== undefined) {
pregunta.obligatoria = updatePreguntaDto.obligatoria;
}
// Si se cambia el tipo de pregunta
if (updatePreguntaDto.id_tipo_pregunta !== undefined) {
const tipoPregunta = await this.tipoPreguntaRepository.findOne({ const tipoPregunta = await this.tipoPreguntaRepository.findOne({
where: { id_tipo: updatePreguntaDto.id_tipo_pregunta }, where: { tipo_pregunta: createPreguntaDto.tipo }
}); });
if (!tipoPregunta) { if (!tipoPregunta) {
throw new NotFoundException( throw new Error(`Tipo de pregunta '${createPreguntaDto.tipo}' no encontrado`);
`Tipo de pregunta con ID ${updatePreguntaDto.id_tipo_pregunta} 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;
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
); );
} }
pregunta.tipo_pregunta = tipoPregunta; await queryRunner.commitTransaction();
}
return this.preguntaRepository.save(pregunta); return {
success: true,
pregunta: savedPregunta
};
} catch (error) {
await queryRunner.rollbackTransaction();
throw error;
} finally {
await queryRunner.release();
}
} }
findAll() { 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);
}
}
async findAll() {
return this.preguntaRepository.find(); return this.preguntaRepository.find();
} }
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;
}
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;
// Si se proporciona un nuevo tipo de pregunta, actualizarlo
if (updatePreguntaDto.tipo) {
const tipoPregunta = await this.tipoPreguntaRepository.findOne({
where: { tipo_pregunta: updatePreguntaDto.tipo }
});
if (!tipoPregunta) {
throw new Error(`Tipo de pregunta '${updatePreguntaDto.tipo}' no encontrado`);
}
pregunta.id_tipo_pregunta = tipoPregunta.id_tipo;
}
// Actualizar la pregunta
const updatedPregunta = await queryRunner.manager.save(pregunta);
// Si se proporcionan nuevas opciones, actualizarlas
if (updatePreguntaDto.opciones && updatePreguntaDto.opciones.length > 0) {
// Eliminar las opciones existentes
const existingOptions = await this.preguntaOpcionRepository.find({
where: { id_pregunta: id }
});
for (const option of existingOptions) {
await queryRunner.manager.remove(option);
}
// Crear las nuevas opciones
await this.createOpciones(
queryRunner,
id,
updatePreguntaDto.opciones
);
// Actualizar el contador de opciones
updatedPregunta.contador_opcion = updatePreguntaDto.opciones.length;
await queryRunner.manager.save(updatedPregunta);
}
await queryRunner.commitTransaction();
return {
success: true,
pregunta: updatedPregunta
};
} catch (error) {
await queryRunner.rollbackTransaction();
throw error;
} finally {
await queryRunner.release();
}
}
async remove(id: number) {
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
const pregunta = await this.preguntaRepository.findOne({
where: { id_pregunta: id }
});
if (!pregunta) {
throw new NotFoundException(`Pregunta con ID ${id} no encontrada`);
}
// Eliminar las relaciones con opciones
const preguntaOpciones = await this.preguntaOpcionRepository.find({
where: { id_pregunta: id }
});
for (const po of preguntaOpciones) {
await queryRunner.manager.remove(po);
}
// Eliminar la pregunta
await queryRunner.manager.remove(pregunta);
await queryRunner.commitTransaction();
return {
success: true,
message: `Pregunta con ID ${id} eliminada correctamente`
};
} catch (error) {
await queryRunner.rollbackTransaction();
throw error;
} finally {
await queryRunner.release();
}
}
} }
@@ -1,31 +1,26 @@
import { Opcion } from 'src/opcion/entities/opcion.entity'; import { Opcion } from 'src/opcion/entities/opcion.entity';
import { Pregunta } from 'src/pregunta/entities/pregunta.entity'; import { Pregunta } from 'src/pregunta/entities/pregunta.entity';
import { RespuestaParticipanteCerrada } from 'src/respuesta_participante_cerrada/entities/respuesta_participante_cerrada.entity'; import { RespuestaParticipanteCerrada } from 'src/respuesta_participante_cerrada/entities/respuesta_participante_cerrada.entity';
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, OneToMany, JoinColumn } from 'typeorm'; import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, OneToMany } from 'typeorm';
@Entity('pregunta_opcion') @Entity()
export class PreguntaOpcion { export class PreguntaOpcion {
@PrimaryGeneratedColumn({ name: 'id_pregunta_opcion' }) @PrimaryGeneratedColumn()
id_pregunta_opcion: number; id_pregunta_opcion: number;
@Column() @Column()
posicion: number; posicion: number;
// Relación con Pregunta @ManyToOne(() => Pregunta)
@ManyToOne(() => Pregunta, pregunta => pregunta.opciones)
@JoinColumn({ name: 'id_pregunta' })
pregunta: Pregunta; pregunta: Pregunta;
// Relación con Opcion @Column()
@ManyToOne(() => Opcion, opcion => opcion.Preguntas) id_pregunta: number;
@JoinColumn({ name: 'id_opcion' })
@ManyToOne(() => Opcion, opcion => opcion.preguntasOpciones)
opcion: Opcion; opcion: Opcion;
// Nueva relación con RespuestaParticipanteCerrada @OneToMany(()=> RespuestaParticipanteCerrada, respuestaParticipanteCerrada=>respuestaParticipanteCerrada.preguntaOpcion)
@OneToMany( respuestaParticipanteCerrada:RespuestaParticipanteCerrada[];
() => RespuestaParticipanteCerrada,
respuestaCerrada => respuestaCerrada.preguntaOpcion
)
respuestasCerradas: RespuestaParticipanteCerrada[];
} }
+11 -5
View File
@@ -1,13 +1,19 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { PreguntaOpcionService } from './pregunta_opcion.service'; import { PreguntaOpcionService } from './pregunta_opcion.service';
import { PreguntaOpcionController } from './pregunta_opcion.controller'; import { PreguntaOpcionController } from './pregunta_opcion.controller';
import { PreguntaOpcion } from './entities/pregunta_opcion.entity';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { PreguntaOpcion } from './entities/pregunta_opcion.entity';
import { OpcionModule } from '../opcion/opcion.module';
import { PreguntaModule } from '../pregunta/pregunta.module';
@Module({ @Module({
imports: [TypeOrmModule.forFeature([PreguntaOpcion])], imports: [
controllers: [PreguntaOpcionController], TypeOrmModule.forFeature([PreguntaOpcion]),
providers: [PreguntaOpcionService], OpcionModule,
exports:[PreguntaOpcionService, TypeOrmModule] PreguntaModule
],
controllers: [PreguntaOpcionController],
providers: [PreguntaOpcionService],
exports: [TypeOrmModule]
}) })
export class PreguntaOpcionModule {} export class PreguntaOpcionModule {}
+43 -22
View File
@@ -1,36 +1,57 @@
import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post } from '@nestjs/common'; import {
Body,
Controller,
Delete,
Get,
Param,
ParseIntPipe,
Patch,
Post,
Query,
} from '@nestjs/common';
import { QrService } from './qr.service'; import { QrService } from './qr.service';
import { Qr } from './qr.entity'; import { Qr } from './qr.entity';
import { CreateQrDto } from './dto/create-qr.dto'; import { CreateQrDto } from './dto/create-qr.dto';
import { UpdateQrDto } from './dto/update.qr.dto'; import { UpdateQrDto } from './dto/update.qr.dto';
import { ApiOperation, ApiQuery } from '@nestjs/swagger';
@Controller('qr') @Controller('qr')
export class QrController { export class QrController {
constructor(private qrService: QrService) {}
constructor(private qrService: QrService) {} @Get('generate')
@ApiOperation({ summary: 'Genera un código QR a partir de un texto' })
@ApiQuery({
name: 'text',
required: true,
description: 'Texto a codificar en el QR',
})
async generateQRCode(@Query('text') text: string): Promise<string> {
return this.qrService.generateQRCode(text);
}
@Get() @Get()
getQrs(): Promise<Qr[]> { getQrs(): Promise<Qr[]> {
return this.qrService.getQrs(); return this.qrService.getQrs();
} }
@Get(':id') @Get(':id')
getQr(@Param('id', ParseIntPipe) id: number) { getQr(@Param('id', ParseIntPipe) id: number) {
return this.qrService.getQr(id); return this.qrService.getQr(id);
} }
@Post() //en el body ValidationPipe @Post() //en el body ValidationPipe
createQr(@Body() newQr: CreateQrDto) { createQr(@Body() newQr: CreateQrDto) {
return this.qrService.createQr(newQr) return this.qrService.createQr(newQr);
} }
@Delete(':id') @Delete(':id')
deleteQr(@Param('id', ParseIntPipe) id:number) { deleteQr(@Param('id', ParseIntPipe) id: number) {
return this.qrService.deleteQr(id); return this.qrService.deleteQr(id);
} }
@Patch(':id') @Patch(':id')
updateQr(@Param('id', ParseIntPipe) id: number, @Body() qr: UpdateQrDto) { updateQr(@Param('id', ParseIntPipe) id: number, @Body() qr: UpdateQrDto) {
return this.qrService.updateQr(id, qr) return this.qrService.updateQr(id, qr);
} }
} }
+64 -47
View File
@@ -4,70 +4,87 @@ import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
import { CreateQrDto } from './dto/create-qr.dto'; import { CreateQrDto } from './dto/create-qr.dto';
import { UpdateQrDto } from './dto/update.qr.dto'; import { UpdateQrDto } from './dto/update.qr.dto';
import * as QRCode from 'qrcode';
@Injectable() @Injectable()
export class QrService { export class QrService {
constructor(@InjectRepository(Qr) private qrRepository: Repository<Qr>) {}
constructor( async generateQRCode(text: string): Promise<string> {
@InjectRepository(Qr) private qrRepository: Repository<Qr> return await QRCode.toDataURL(text);
) {} }
async createQr(qr: CreateQrDto) { async generateBuffer(text: string): Promise<Buffer> {
const qrFound = await this.qrRepository.findOne({ return await QRCode.toBuffer(text); // Devuelve un buffer para adjuntar en email o stream
where: { }
id_qr: qr.id_participante_evento
}
})
if (qrFound) { // Para enviar el QR por email
return new HttpException('Qr already exists', HttpStatus.CONFLICT) /* const qrBuffer = await this.qrService.generateBuffer('Texto a codificar');
} await transporter.sendMail({
to: 'destinatario@example.com',
subject: 'Tu código QR',
html: '<p>Escanea el siguiente código:</p><img src="cid:qrcode"/>',
attachments: [{
filename: 'qrcode.png',
content: qrBuffer,
cid: 'qrcode',
}],
}); */
return this.qrRepository.save(qr) async createQr(qr: CreateQrDto) {
const qrFound = await this.qrRepository.findOne({
where: {
id_qr: qr.id_participante_evento,
},
});
if (qrFound) {
return new HttpException('Qr already exists', HttpStatus.CONFLICT);
} }
getQrs() { return this.qrRepository.save(qr);
return this.qrRepository.find({}) }
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);
} }
async getQr(id_qr) { return qrFound;
const qrFound = await this.qrRepository.findOne({ }
where: {
id_qr
}
})
if (!qrFound) { async deleteQr(id_qr: number) {
return new HttpException('User not found', HttpStatus.NOT_FOUND); const result = await this.qrRepository.delete({ id_qr });
}
return qrFound if (result.affected === 0) {
return new HttpException('User not found', HttpStatus.NOT_FOUND);
} }
async deleteQr(id_qr: number) { return result;
const result = await this.qrRepository.delete({ id_qr }) }
if (result.affected === 0) { async updateQr(id_qr: number, qr: UpdateQrDto) {
return new HttpException('User not found', HttpStatus.NOT_FOUND); const qrFound = await this.qrRepository.findOne({
} where: {
id_qr,
},
});
return result if (!qrFound) {
return new HttpException('User not found', HttpStatus.NOT_FOUND);
} }
async updateQr(id_qr: number, qr: UpdateQrDto) { const updateQr = Object.assign(qrFound, qr);
const qrFound = await this.qrRepository.findOne({ return this.qrRepository.save(updateQr);
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)
}
} }
@@ -2,45 +2,26 @@ import { CuestionarioRespondido } from 'src/cuestionario_respondido/entities/cue
import { Pregunta } from 'src/pregunta/entities/pregunta.entity'; import { Pregunta } from 'src/pregunta/entities/pregunta.entity';
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm'; import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
@Entity('respuesta_participante_abierta') @Entity('respuesta_participante_abierta')
export class RespuestaParticipanteAbierta { export class RespuestaParticipanteAbierta {
@PrimaryGeneratedColumn({ @PrimaryGeneratedColumn()
name: 'id_respuesta_participante_abierta', idRespuestaParticipanteAbierta: number;
type: 'integer'
})
id: number;
@Column({ @Column({ type: 'text' })
type: 'text',
nullable: false
})
respuesta: string; respuesta: string;
// Relación con CuestionarioRespondido // Relación con CuestionarioRespondido
@ManyToOne( @ManyToOne(
() => CuestionarioRespondido, () => CuestionarioRespondido,
(cuestionarioRespondido) => cuestionarioRespondido.respuestasAbiertas cuestionarioRespondido => cuestionarioRespondido.respuestasAbiertas
) )
@JoinColumn({ @JoinColumn({ name: 'id_cuestionario_respondido' })
name: 'id_cuestionario_respondido'
})
cuestionarioRespondido: CuestionarioRespondido; cuestionarioRespondido: CuestionarioRespondido;
// Relación con Pregunta @ManyToOne(() => Pregunta)
@ManyToOne( @JoinColumn({ name: 'id_pregunta' })
() => Pregunta,
(pregunta) => pregunta.respuestasAbiertas
)
@JoinColumn({
name: 'id_pregunta'
})
pregunta: Pregunta; pregunta: Pregunta;
/* Campo adicional para tracking
@Column({
name: 'fecha_respuesta',
type: 'timestamp',
default: () => 'CURRENT_TIMESTAMP'
})
fechaRespuesta: Date;*/
} }
@@ -1,15 +1,19 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { RespuestaParticipanteAbiertaService } from './respuesta_participante_abierta.service'; import { RespuestaParticipanteAbiertaService } from './respuesta_participante_abierta.service';
import { RespuestaParticipanteAbiertaController } from './respuesta_participante_abierta.controller'; import { RespuestaParticipanteAbiertaController } from './respuesta_participante_abierta.controller';
import { RespuestaParticipanteAbierta } from './entities/respuesta_participante_abierta.entity';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { RespuestaParticipanteAbierta } from './entities/respuesta_participante_abierta.entity';
import { CuestionarioRespondidoModule } from '../cuestionario_respondido/cuestionario_respondido.module';
import { PreguntaModule } from '../pregunta/pregunta.module';
@Module({ @Module({
imports: [ imports: [
TypeOrmModule.forFeature([RespuestaParticipanteAbierta]), TypeOrmModule.forFeature([RespuestaParticipanteAbierta]),
], CuestionarioRespondidoModule,
controllers: [RespuestaParticipanteAbiertaController], PreguntaModule
providers: [RespuestaParticipanteAbiertaService], ],
exports: [RespuestaParticipanteAbiertaService, TypeOrmModule], controllers: [RespuestaParticipanteAbiertaController],
providers: [RespuestaParticipanteAbiertaService],
exports: [TypeOrmModule]
}) })
export class RespuestaParticipanteAbiertaModule {} export class RespuestaParticipanteAbiertaModule {}
@@ -12,7 +12,7 @@ export class RespuestaParticipanteCerrada {
@JoinColumn({ name: 'id_cuestionario_respondido' }) @JoinColumn({ name: 'id_cuestionario_respondido' })
cuestionarioRespondido: CuestionarioRespondido; cuestionarioRespondido: CuestionarioRespondido;
@ManyToOne(() => PreguntaOpcion, preguntaOpcion => preguntaOpcion.respuestasCerradas) @ManyToOne(() => PreguntaOpcion, preguntaOpcion => preguntaOpcion.respuestaParticipanteCerrada)
@JoinColumn({ name: 'id_pregunta_opcion' }) @JoinColumn({ name: 'id_pregunta_opcion' })
preguntaOpcion: PreguntaOpcion; preguntaOpcion: PreguntaOpcion;
@@ -1,13 +1,19 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { RespuestaParticipanteCerradaService } from './respuesta_participante_cerrada.service'; import { RespuestaParticipanteCerradaService } from './respuesta_participante_cerrada.service';
import { RespuestaParticipanteCerradaController } from './respuesta_participante_cerrada.controller'; import { RespuestaParticipanteCerradaController } from './respuesta_participante_cerrada.controller';
import { RespuestaParticipanteCerrada } from './entities/respuesta_participante_cerrada.entity';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { RespuestaParticipanteCerrada } from './entities/respuesta_participante_cerrada.entity';
import { PreguntaOpcionModule } from '../pregunta_opcion/pregunta_opcion.module';
import { CuestionarioRespondidoModule } from '../cuestionario_respondido/cuestionario_respondido.module';
@Module({ @Module({
imports: [TypeOrmModule.forFeature([RespuestaParticipanteCerrada])], imports: [
controllers: [RespuestaParticipanteCerradaController], TypeOrmModule.forFeature([RespuestaParticipanteCerrada]),
providers: [RespuestaParticipanteCerradaService], PreguntaOpcionModule,
exports:[RespuestaParticipanteCerradaService, TypeOrmModule] CuestionarioRespondidoModule
],
controllers: [RespuestaParticipanteCerradaController],
providers: [RespuestaParticipanteCerradaService],
exports: [TypeOrmModule]
}) })
export class RespuestaParticipanteCerradaModule {} export class RespuestaParticipanteCerradaModule {}
+7 -14
View File
@@ -1,6 +1,6 @@
import { IsNotEmpty, IsString, IsOptional, IsArray, ValidateNested, IsInt } from 'class-validator';
import { Type } from 'class-transformer';
import { CreatePreguntaDto } from '../../pregunta/dto/create-pregunta.dto'; 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 { export class CreateSeccionDto {
@IsNotEmpty() @IsNotEmpty()
@@ -11,20 +11,13 @@ export class CreateSeccionDto {
@IsString() @IsString()
descripcion?: string; descripcion?: string;
@IsOptional()
@IsNumber()
contador_pregunta?: number;
@IsOptional() @IsOptional()
@IsArray() @IsArray()
@ValidateNested({ each: true }) @ValidateNested({ each: true })
@Type(() => PreguntaEnSeccionDto)
preguntas?: PreguntaEnSeccionDto[];
}
export class PreguntaEnSeccionDto {
@IsNotEmpty()
@IsInt()
posicion: number;
@IsNotEmpty()
@ValidateNested()
@Type(() => CreatePreguntaDto) @Type(() => CreatePreguntaDto)
pregunta: CreatePreguntaDto; preguntas?: CreatePreguntaDto[];
} }
+4 -4
View File
@@ -11,14 +11,14 @@ export class Seccion {
contador_pregunta: number; contador_pregunta: number;
@Column({ type: 'text', nullable: true }) @Column({ type: 'text', nullable: true })
descripcion: string; descripcion?: string;
@Column({ type: 'text', nullable: true }) @Column({ type: 'text', nullable: true })
titulo: string; titulo: string;
@OneToMany(() => CuestionarioSeccion,(cuestionario_seccion) => cuestionario_seccion.seccion ) @OneToMany(() => CuestionarioSeccion,(cuestionario_seccion) => cuestionario_seccion.id_seccion )
cuestionarioSeccion:CuestionarioSeccion[]; seccion:CuestionarioSeccion[];
@OneToMany(()=> SeccionPregunta, (seccion_pregunta)=> seccion_pregunta.seccion) @OneToMany(()=> SeccionPregunta, (seccion_pregunta)=> seccion_pregunta.seccion)
seccion_pregunta:SeccionPregunta[]; seccionPreguntas:SeccionPregunta[];
} }
+20 -145
View File
@@ -1,166 +1,41 @@
import { import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common';
Controller,
Post,
Body,
Get,
Param,
Put,
Delete,
HttpStatus,
HttpException,
Query,
} from '@nestjs/common';
import { SeccionService } from './seccion.service'; import { SeccionService } from './seccion.service';
import { CreateSeccionDto, PreguntaEnSeccionDto } from './dto/create-seccion.dto'; import { CreateSeccionDto } from './dto/create-seccion.dto';
import { UpdateSeccionDto } from './dto/update-seccion.dto'; import { UpdateSeccionDto } from './dto/update-seccion.dto';
import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiBody } from '@nestjs/swagger'; import { SeccionApiDocumentation } from './seccion.documentation';
@ApiTags('Secciones') @Controller('seccion')
@Controller('secciones') @SeccionApiDocumentation.ApiController
export class SeccionController { export class SeccionController {
constructor(private readonly seccionService: SeccionService) {} constructor(private readonly seccionService: SeccionService) {}
@Post() @Post()
@ApiOperation({ summary: 'Crear una nueva sección con preguntas' }) @SeccionApiDocumentation.ApiCreate
@ApiBody({ type: CreateSeccionDto }) create(@Body() createSeccionDto: CreateSeccionDto) {
@ApiResponse({ return this.seccionService.create(createSeccionDto);
status: HttpStatus.CREATED,
description: 'Sección creada exitosamente con sus preguntas'
})
@ApiResponse({
status: HttpStatus.BAD_REQUEST,
description: 'Datos de entrada inválidos'
})
async create(@Body() createSeccionDto: CreateSeccionDto) {
try {
return await this.seccionService.create(createSeccionDto);
} catch (error) {
throw new HttpException({
status: HttpStatus.BAD_REQUEST,
error: error.message,
}, HttpStatus.BAD_REQUEST);
}
} }
@Get() @Get()
@ApiOperation({ summary: 'Obtener todas las secciones' }) @SeccionApiDocumentation.ApiGetAll
@ApiResponse({ findAll() {
status: HttpStatus.OK,
description: 'Lista de secciones obtenida exitosamente'
})
async findAll() {
return this.seccionService.findAll(); return this.seccionService.findAll();
} }
@Get(':id') @Get(':id')
@ApiOperation({ summary: 'Obtener una sección específica con sus preguntas' }) @SeccionApiDocumentation.ApiGetOne
@ApiParam({ name: 'id', description: 'ID de la sección' }) findOne(@Param('id') id: string) {
@ApiResponse({ return this.seccionService.findOne(+id);
status: HttpStatus.OK,
description: 'Sección encontrada con todas sus relaciones'
})
@ApiResponse({
status: HttpStatus.NOT_FOUND,
description: 'Sección no encontrada'
})
async findOne(@Param('id') id: string) {
const seccion = await this.seccionService.findOneWithRelations(+id);
if (!seccion) {
throw new HttpException('Sección no encontrada', HttpStatus.NOT_FOUND);
}
return seccion;
} }
@Put(':id') @Patch(':id')
@ApiOperation({ summary: 'Actualizar los metadatos de una sección' }) @SeccionApiDocumentation.ApiUpdate
@ApiParam({ name: 'id', description: 'ID de la sección a actualizar' }) update(@Param('id') id: string, @Body() updateSeccionDto: UpdateSeccionDto) {
@ApiBody({ type: UpdateSeccionDto }) return this.seccionService.update(+id, updateSeccionDto);
@ApiResponse({
status: HttpStatus.OK,
description: 'Sección actualizada exitosamente'
})
async update(
@Param('id') id: string,
@Body() updateSeccionDto: UpdateSeccionDto,
) {
try {
return await this.seccionService.update(+id, updateSeccionDto);
} catch (error) {
throw new HttpException({
status: HttpStatus.BAD_REQUEST,
error: error.message,
}, HttpStatus.BAD_REQUEST);
}
} }
@Delete(':id') @Delete(':id')
@ApiOperation({ summary: 'Eliminar una sección y sus relaciones' }) @SeccionApiDocumentation.ApiRemove
@ApiParam({ name: 'id', description: 'ID de la sección a eliminar' }) remove(@Param('id') id: string) {
@ApiResponse({ return this.seccionService.remove(+id);
status: HttpStatus.OK,
description: 'Sección eliminada exitosamente'
})
async remove(@Param('id') id: string) {
try {
return await this.seccionService.remove(+id);
} catch (error) {
throw new HttpException({
status: HttpStatus.BAD_REQUEST,
error: error.message,
}, HttpStatus.BAD_REQUEST);
}
}
@Post(':id/preguntas')
@ApiOperation({ summary: 'Agregar una nueva pregunta a la sección' })
@ApiParam({ name: 'id', description: 'ID de la sección' })
@ApiBody({ type: PreguntaEnSeccionDto })
@ApiResponse({
status: HttpStatus.CREATED,
description: 'Pregunta agregada a la sección exitosamente'
})
async agregarPregunta(
@Param('id') seccionId: string,
@Body() preguntaDto: PreguntaEnSeccionDto,
) {
try {
return await this.seccionService.agregarPreguntaExistente(
+seccionId,
preguntaDto.pregunta.id_tipo_pregunta,
preguntaDto.posicion,
);
} catch (error) {
throw new HttpException({
status: error.status || HttpStatus.BAD_REQUEST,
error: error.message,
}, error.status || HttpStatus.BAD_REQUEST);
}
}
@Delete(':seccionId/preguntas/:preguntaId')
@ApiOperation({ summary: 'Remover una pregunta de la sección' })
@ApiParam({ name: 'seccionId', description: 'ID de la sección' })
@ApiParam({ name: 'preguntaId', description: 'ID de la pregunta a remover' })
@ApiResponse({
status: HttpStatus.OK,
description: 'Pregunta removida de la sección exitosamente'
})
async removerPregunta(
@Param('seccionId') seccionId: string,
@Param('preguntaId') preguntaId: string,
) {
try {
return await this.seccionService.eliminarPreguntaDeSeccion(
+seccionId,
+preguntaId,
);
} catch (error) {
throw new HttpException({
status: error.status || HttpStatus.BAD_REQUEST,
error: error.message,
}, error.status || HttpStatus.BAD_REQUEST);
}
} }
} }
+185
View File
@@ -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' })
);
}
+15 -6
View File
@@ -1,18 +1,27 @@
import { Module, forwardRef } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { SeccionService } from './seccion.service'; import { SeccionService } from './seccion.service';
import { SeccionController } from './seccion.controller'; import { SeccionController } from './seccion.controller';
import { Seccion } from './entities/seccion.entity';
import { TypeOrmModule } from '@nestjs/typeorm'; 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 { SeccionPregunta } from '../seccion_pregunta/entities/seccion_pregunta.entity';
import { PreguntaModule } from '../pregunta/pregunta.module'; import { TipoPregunta } from '../tipo_pregunta/entities/tipo_pregunta.entity';
import { Opcion } from '../opcion/entities/opcion.entity';
import { PreguntaOpcion } from '../pregunta_opcion/entities/pregunta_opcion.entity';
@Module({ @Module({
imports: [ imports: [
TypeOrmModule.forFeature([Seccion, SeccionPregunta]), TypeOrmModule.forFeature([
forwardRef(() => PreguntaModule) Seccion,
Pregunta,
SeccionPregunta,
TipoPregunta,
Opcion,
PreguntaOpcion
])
], ],
controllers: [SeccionController], controllers: [SeccionController],
providers: [SeccionService], providers: [SeccionService],
exports: [SeccionService], exports: [TypeOrmModule, SeccionService]
}) })
export class SeccionModule {} export class SeccionModule {}
+161 -133
View File
@@ -1,144 +1,121 @@
import { Injectable, NotFoundException } from '@nestjs/common'; import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { DeleteResult, Repository } from 'typeorm'; import { Repository, DataSource } from 'typeorm';
import { Seccion } from './entities/seccion.entity'; import { CreateSeccionDto } from './dto/create-seccion.dto';
import { PreguntaService } from '../pregunta/pregunta.service';
import { CreateSeccionDto, PreguntaEnSeccionDto } from './dto/create-seccion.dto';
import { Pregunta } from '../pregunta/entities/pregunta.entity';
import { SeccionPregunta } from 'src/seccion_pregunta/entities/seccion_pregunta.entity';
import { UpdateSeccionDto } from './dto/update-seccion.dto'; import { UpdateSeccionDto } from './dto/update-seccion.dto';
import { Seccion } from './entities/seccion.entity';
import { Pregunta } from '../pregunta/entities/pregunta.entity';
import { SeccionPregunta } from '../seccion_pregunta/entities/seccion_pregunta.entity';
import { TipoPregunta } from '../tipo_pregunta/entities/tipo_pregunta.entity';
import { Opcion } from '../opcion/entities/opcion.entity';
import { PreguntaOpcion } from '../pregunta_opcion/entities/pregunta_opcion.entity';
@Injectable() @Injectable()
export class SeccionService { export class SeccionService {
constructor( constructor(
@InjectRepository(Seccion) @InjectRepository(Seccion)
private readonly seccionRepository: Repository<Seccion>, private seccionRepository: Repository<Seccion>,
@InjectRepository(Pregunta)
private preguntaRepository: Repository<Pregunta>,
@InjectRepository(SeccionPregunta) @InjectRepository(SeccionPregunta)
private readonly seccionPreguntaRepository: Repository<SeccionPregunta>, private seccionPreguntaRepository: Repository<SeccionPregunta>,
private readonly preguntaService: PreguntaService, @InjectRepository(TipoPregunta)
private tipoPreguntaRepository: Repository<TipoPregunta>,
@InjectRepository(Opcion)
private opcionRepository: Repository<Opcion>,
@InjectRepository(PreguntaOpcion)
private preguntaOpcionRepository: Repository<PreguntaOpcion>,
private dataSource: DataSource
) {} ) {}
async create(createSeccionDto: CreateSeccionDto) { async create(createSeccionDto: CreateSeccionDto) {
// Crear la sección base const queryRunner = this.dataSource.createQueryRunner();
const seccion = this.seccionRepository.create({ await queryRunner.connect();
titulo: createSeccionDto.titulo, await queryRunner.startTransaction();
descripcion: createSeccionDto.descripcion,
contador_pregunta: createSeccionDto.preguntas?.length || 0,
});
const seccionGuardada = await this.seccionRepository.save(seccion); try {
// 1. Crear sección
const seccion = new Seccion();
seccion.titulo = createSeccionDto.titulo;
seccion.descripcion = createSeccionDto.descripcion;
seccion.contador_pregunta = createSeccionDto.preguntas?.length || 0;
// Si hay preguntas, crearlas y asociarlas const savedSeccion = await queryRunner.manager.save(seccion);
if (createSeccionDto.preguntas && createSeccionDto.preguntas.length > 0) {
await this.agregarPreguntasASeccion(
seccionGuardada.id_seccion,
createSeccionDto.preguntas,
);
}
return this.findOneWithRelations(seccionGuardada.id_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];
private async agregarPreguntasASeccion( // Obtener el tipo de pregunta por su nombre
seccionId: number, const tipoPregunta = await queryRunner.manager.findOne(TipoPregunta, {
preguntas: PreguntaEnSeccionDto[], // Tipo actualizado where: { tipo_pregunta: preguntaDto.tipo },
) { });
for (const { posicion, pregunta } of preguntas) {
const preguntaCreada = await this.preguntaService.create(pregunta);
const seccionPregunta = this.seccionPreguntaRepository.create({ if (!tipoPregunta) {
posicion, throw new Error(`Tipo de pregunta '${preguntaDto.tipo}' no encontrado`);
seccion: { id_seccion: seccionId }, }
pregunta: { id_pregunta: preguntaCreada?.id_pregunta },
});
await this.seccionPreguntaRepository.save(seccionPregunta); // 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();
} }
} }
async findOneWithRelations(id: number) { async findAll() {
return this.seccionRepository.findOne({ return this.seccionRepository.find();
where: { id_seccion: id },
relations: [
'seccionPreguntas',
'seccionPreguntas.pregunta',
'seccionPreguntas.pregunta.tipoPregunta',
'seccionPreguntas.pregunta.opciones',
'seccionPreguntas.pregunta.opciones.opcion',
],
});
} }
async agregarPreguntaExistente( async findOne(id: number) {
seccionId: number,
preguntaId: number,
posicion: number,
) {
// Verificar que existan ambos elementos
const seccion = await this.seccionRepository.findOne({
where: { id_seccion: seccionId },
});
if (!seccion) {
throw new NotFoundException(`Sección con ID ${seccionId} no encontrada`);
}
const pregunta = await this.preguntaService.findOneWithRelations(preguntaId);
if (!pregunta) {
throw new NotFoundException(`Pregunta con ID ${preguntaId} no encontrada`);
}
// Crear la relación
const seccionPregunta = this.seccionPreguntaRepository.create({
posicion,
seccion: { id_seccion: seccionId },
pregunta: { id_pregunta: preguntaId },
});
await this.seccionPreguntaRepository.save(seccionPregunta);
// Actualizar contador de preguntas
await this.seccionRepository.increment(
{ id_seccion: seccionId },
'contador_pregunta',
1,
);
return this.findOneWithRelations(seccionId);
}
async eliminarPreguntaDeSeccion(seccionId: number, preguntaId: number) {
const relacion = await this.seccionPreguntaRepository.findOne({
where: {
seccion: { id_seccion: seccionId },
pregunta: { id_pregunta: preguntaId },
},
});
if (!relacion) {
throw new NotFoundException(
'La pregunta no está asociada a esta sección',
);
}
await this.seccionPreguntaRepository.remove(relacion);
// Actualizar contador de preguntas
await this.seccionRepository.decrement(
{ id_seccion: seccionId },
'contador_pregunta',
1,
);
return { success: true };
}
async findAll(): Promise<Seccion[]> {
return this.seccionRepository.find({
relations: ['seccionPreguntas', 'seccionPreguntas.pregunta'],
});
}
async update(id: number, updateSeccionDto: UpdateSeccionDto): Promise<Seccion> {
const seccion = await this.seccionRepository.findOne({ const seccion = await this.seccionRepository.findOne({
where: { id_seccion: id } where: { id_seccion: id }
}); });
@@ -147,31 +124,82 @@ export class SeccionService {
throw new NotFoundException(`Sección con ID ${id} no encontrada`); throw new NotFoundException(`Sección con ID ${id} no encontrada`);
} }
if (updateSeccionDto.titulo !== undefined) { return seccion;
seccion.titulo = updateSeccionDto.titulo; }
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`);
} }
if (updateSeccionDto.descripcion !== undefined) { // Obtener las relaciones seccion-pregunta para esta sección
seccion.descripcion = updateSeccionDto.descripcion; 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
};
}
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); return this.seccionRepository.save(seccion);
} }
async remove(id: number): Promise<DeleteResult> { async remove(id: number) {
// Primero eliminamos las relaciones con preguntas (esto sigue siendo asíncrono) const seccion = await this.seccionRepository.findOne({
this.seccionPreguntaRepository.delete({ seccion: { id_seccion: id } }) where: { id_seccion: id }
.catch(err => { });
throw new Error(`Error al eliminar relaciones: ${err.message}`);
});
// Eliminación sincrónica usando delete (devuelve DeleteResult directamente) if (!seccion) {
const result = this.seccionRepository.delete(id);
if ((await result).affected === 0) {
throw new NotFoundException(`Sección con ID ${id} no encontrada`); throw new NotFoundException(`Sección con ID ${id} no encontrada`);
} }
return result; return this.seccionRepository.remove(seccion);
} }
} }
@@ -1,20 +1,25 @@
import { Pregunta } from 'src/pregunta/entities/pregunta.entity'; import { Pregunta } from 'src/pregunta/entities/pregunta.entity';
import { Seccion } from 'src/seccion/entities/seccion.entity'; import { Seccion } from 'src/seccion/entities/seccion.entity';
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm'; import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from 'typeorm';
@Entity()
@Entity('seccion_pregunta')
export class SeccionPregunta { export class SeccionPregunta {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
id_seccion_pregunta: number; id_seccion_pregunta: number;
@Column() @Column({ type: 'tinyint' })
posicion: number; posicion: number;
@ManyToOne(() => Seccion, seccion => seccion.seccion_pregunta) @ManyToOne(() => Pregunta, (pregunta) => pregunta.seccionPreguntas)
@JoinColumn({name:'id_seccion'}) pregunta: Pregunta;
@Column({ type: 'int' })
id_pregunta: number;
@ManyToOne(() => Seccion, (seccion) => seccion.seccionPreguntas)
seccion: Seccion; seccion: Seccion;
@ManyToOne(() => Pregunta, pregunta => pregunta.seccionesPregunta) @Column({ type: 'int' })
@JoinColumn({name:'id_pregunta'}) id_seccion: number;
pregunta: Pregunta;
} }
@@ -1,13 +1,19 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { SeccionPreguntaService } from './seccion_pregunta.service'; import { SeccionPreguntaService } from './seccion_pregunta.service';
import { SeccionPreguntaController } from './seccion_pregunta.controller'; import { SeccionPreguntaController } from './seccion_pregunta.controller';
import { SeccionPregunta } from './entities/seccion_pregunta.entity';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { SeccionPregunta } from './entities/seccion_pregunta.entity';
import { PreguntaModule } from '../pregunta/pregunta.module';
import { SeccionModule } from '../seccion/seccion.module';
@Module({ @Module({
imports: [TypeOrmModule.forFeature([SeccionPregunta])], imports: [
TypeOrmModule.forFeature([SeccionPregunta]),
PreguntaModule,
SeccionModule
],
controllers: [SeccionPreguntaController], controllers: [SeccionPreguntaController],
providers: [SeccionPreguntaService], providers: [SeccionPreguntaService],
exports:[SeccionPreguntaService, TypeOrmModule] exports: [TypeOrmModule]
}) })
export class SeccionPreguntaModule {} export class SeccionPreguntaModule {}
@@ -1,5 +1,5 @@
import { Cuestionario } from 'src/cuestionario/entities/cuestionario.entity'; import { Cuestionario } from 'src/cuestionario/entities/cuestionario.entity';
import { Entity, PrimaryGeneratedColumn, Column, ManyToMany, ManyToOne, OneToMany } from 'typeorm'; import { Entity, PrimaryGeneratedColumn, Column, ManyToMany, ManyToOne } from 'typeorm';
@Entity('tipo_cuestionario') @Entity('tipo_cuestionario')
export class TipoCuestionario { export class TipoCuestionario {
@@ -9,6 +9,6 @@ export class TipoCuestionario {
@Column({ type: 'varchar', length: 30 }) @Column({ type: 'varchar', length: 30 })
tipo_cuestionario: string; tipo_cuestionario: string;
@OneToMany(()=> Cuestionario,(cuestionario)=> cuestionario.tipo_cuestionario) @ManyToOne(()=> Cuestionario,(cuestionario)=> cuestionario.id_tipo_cuestionario)
cuestionarios:Cuestionario[]; cuestionario:Cuestionario[];
} }
@@ -1,13 +1,9 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { TipoCuestionarioService } from './tipo_cuestionario.service'; import { TipoCuestionarioService } from './tipo_cuestionario.service';
import { TipoCuestionarioController } from './tipo_cuestionario.controller'; import { TipoCuestionarioController } from './tipo_cuestionario.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TipoCuestionario } from './entities/tipo_cuestionario.entity';
@Module({ @Module({
imports: [TypeOrmModule.forFeature([TipoCuestionario])],
controllers: [TipoCuestionarioController], controllers: [TipoCuestionarioController],
providers: [TipoCuestionarioService], providers: [TipoCuestionarioService],
exports:[TipoCuestionarioService, TypeOrmModule]
}) })
export class TipoCuestionarioModule {} export class TipoCuestionarioModule {}
@@ -1,9 +1 @@
import { IsNotEmpty, IsString, MaxLength, MinLength } from 'class-validator'; export class CreateTipoPreguntaDto {}
export class CreateTipoPreguntaDto {
@IsNotEmpty()
@IsString()
@MinLength(3)
@MaxLength(50)
tipo_pregunta: string;
}
@@ -9,6 +9,6 @@ export class TipoPregunta {
@Column() @Column()
tipo_pregunta: string; tipo_pregunta: string;
@OneToMany(() => Pregunta, pregunta => pregunta.tipo_pregunta) @OneToMany(() => Pregunta, pregunta => pregunta.tipoPregunta)
preguntas: Pregunta[]; preguntas: Pregunta[];
} }
+3 -29
View File
@@ -1,32 +1,6 @@
import { Controller, Post, Body, HttpStatus, HttpException } from '@nestjs/common'; import { Controller } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse, ApiBody } from '@nestjs/swagger';
import { CreateTipoPreguntaDto } from './dto/create-tipo_pregunta.dto';
import { TipoPreguntaService } from './tipo_pregunta.service';
@ApiTags('Tipos de Pregunta') @Controller('tipo_pregunta')
@Controller('tipos-pregunta')
export class TipoPreguntaController { export class TipoPreguntaController {
constructor(private readonly tipoPreguntaService: TipoPreguntaService) {} // Métodos del controlador
@Post()
@ApiOperation({ summary: 'Crear un nuevo tipo de pregunta' })
@ApiBody({ type: CreateTipoPreguntaDto })
@ApiResponse({
status: 201,
description: 'Tipo de pregunta creado exitosamente'
})
@ApiResponse({
status: 409,
description: 'El tipo de pregunta ya existe'
})
async create(@Body() createTipoPreguntaDto: CreateTipoPreguntaDto) {
try {
return await this.tipoPreguntaService.create(createTipoPreguntaDto);
} catch (error) {
throw new HttpException({
status: error.status || HttpStatus.BAD_REQUEST,
error: error.message,
}, error.status || HttpStatus.BAD_REQUEST);
}
}
} }
+4 -7
View File
@@ -1,16 +1,13 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { TipoPreguntaService } from './tipo_pregunta.service';
import { TipoPreguntaController } from './tipo_pregunta.controller';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { TipoPregunta } from './entities/tipo_pregunta.entity'; import { TipoPregunta } from './entities/tipo_pregunta.entity';
import { TipoPreguntaController } from './tipo_pregunta.controller';
import { TipoPreguntaService } from './tipo_pregunta.service';
@Module({ @Module({
imports: [ imports: [TypeOrmModule.forFeature([TipoPregunta])],
TypeOrmModule.forFeature([TipoPregunta]),
],
controllers: [TipoPreguntaController], controllers: [TipoPreguntaController],
providers: [TipoPreguntaService], providers: [TipoPreguntaService],
exports: [TipoPreguntaService, TypeOrmModule], exports: [TypeOrmModule]
}) })
export class TipoPreguntaModule {} export class TipoPreguntaModule {}
+2 -30
View File
@@ -1,34 +1,6 @@
import { Injectable, ConflictException } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { TipoPregunta } from './entities/tipo_pregunta.entity';
import { CreateTipoPreguntaDto } from './dto/create-tipo_pregunta.dto';
@Injectable() @Injectable()
export class TipoPreguntaService { export class TipoPreguntaService {
constructor( // Métodos del servicio
@InjectRepository(TipoPregunta)
private readonly tipoPreguntaRepository: Repository<TipoPregunta>,
) {}
async create(createTipoPreguntaDto: CreateTipoPreguntaDto): Promise<TipoPregunta> {
// Verificar si ya existe un tipo con el mismo nombre
const tipoExistente = await this.tipoPreguntaRepository.findOne({
where: { tipo_pregunta: createTipoPreguntaDto.tipo_pregunta },
});
if (tipoExistente) {
throw new ConflictException(
`El tipo de pregunta '${createTipoPreguntaDto.tipo_pregunta}' ya existe`,
);
}
// Crear y guardar el nuevo tipo
const nuevoTipo = this.tipoPreguntaRepository.create({
tipo_pregunta: createTipoPreguntaDto.tipo_pregunta,
});
return await this.tipoPreguntaRepository.save(nuevoTipo);
}
} }
+53
View File
@@ -0,0 +1,53 @@
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,
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: 'Multiple',
opciones: [{ valor: 'Si' }, { valor: 'No' }],
obligatoria: true,
},
{
titulo: 'Numero de cuenta',
obligatoria: true,
tipo: 'Numero',
},
{
titulo: 'Nombre(s)',
obligatoria: true,
tipo: 'Texto',
},
{
titulo: 'Apellidos',
obligatoria: true,
tipo: 'Texto',
},
{
titulo: 'genero',
obligatoria: true,
tipo: 'Radio',
opciones: [
{ valor: 'Masculino' },
{ valor: 'Femenino' },
{ valor: 'Prefiero no decirlo' },
],
},
{
titulo: 'Institución de procedencia',
tipo: 'Abierto',
obligatoria: true,
},
],
},
],
};
+197
View File
@@ -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',
},
},
],
},
},
],
},
],
},
};
+46
View File
@@ -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);
});
View File
View File
View File
+21
View File
@@ -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',
};
+28
View File
@@ -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'));