diff --git a/Dockerfile b/Dockerfile index 4f67ac3..0b618ea 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ -# Etapa de construcción -FROM node:22-alpine AS builder +# Etapa de desarrollo para ejecutar en watch mode +FROM node:20-alpine WORKDIR /app @@ -7,17 +7,12 @@ COPY package*.json ./ RUN npm install COPY . . + +# Asegurarse de compilar el proyecto RUN npm run build -# Etapa de producción -FROM node:20-alpine AS production +# Puerto expuesto +EXPOSE 3000 -WORKDIR /app - -COPY --from=builder /app/node_modules ./node_modules -COPY --from=builder /app/dist ./dist -COPY --from=builder /app/package*.json ./ - -EXPOSE 4100 - -CMD ["node", "dist/main"] +# Comando para iniciar en modo desarrollo +CMD ["npm", "run", "start:dev"] diff --git a/docker-compose.yml b/docker-compose.yml index ab95035..8e9a574 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,6 +11,7 @@ services: MARIADB_DATABASE: formularios_test volumes: - mariadb_data:/var/lib/mysql + restart: unless-stopped api: build: @@ -19,10 +20,14 @@ services: container_name: formularios_api env_file: - .env + volumes: + - .:/app + - /app/node_modules ports: - "4200:3000" depends_on: - mariadb + restart: unless-stopped volumes: mariadb_data: diff --git a/package-lock.json b/package-lock.json index 8815789..7f12beb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,8 @@ "@nestjs/platform-express": "^11.0.1", "@nestjs/swagger": "^11.1.0", "@nestjs/typeorm": "^11.0.0", + "axios": "^1.8.4", + "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "dotenv": "^16.4.7", "mysql2": "^3.14.0", @@ -4191,7 +4193,6 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, "license": "MIT" }, "node_modules/aws-ssl-profiles": { @@ -4203,6 +4204,17 @@ "node": ">= 6.0.0" } }, + "node_modules/axios": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", + "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/b4a": { "version": "1.6.7", "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", @@ -4803,6 +4815,12 @@ "dev": true, "license": "MIT" }, + "node_modules/class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", + "license": "MIT" + }, "node_modules/class-validator": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.1.tgz", @@ -4968,7 +4986,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" @@ -5299,7 +5316,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.4.0" @@ -5543,7 +5559,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -6321,6 +6336,26 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -6434,7 +6469,6 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", - "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -6460,7 +6494,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -6470,7 +6503,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "license": "MIT", "dependencies": { "mime-db": "1.52.0" @@ -6824,7 +6856,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -9262,6 +9293,12 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", diff --git a/package.json b/package.json index f18668e..539948a 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,8 @@ "@nestjs/platform-express": "^11.0.1", "@nestjs/swagger": "^11.1.0", "@nestjs/typeorm": "^11.0.0", + "axios": "^1.8.4", + "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "dotenv": "^16.4.7", "mysql2": "^3.14.0", diff --git a/src/cuestionario/cuestionario.controller.ts b/src/cuestionario/cuestionario.controller.ts index 82adea6..ff9a3b2 100644 --- a/src/cuestionario/cuestionario.controller.ts +++ b/src/cuestionario/cuestionario.controller.ts @@ -2,32 +2,39 @@ import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/commo import { CuestionarioService } from './cuestionario.service'; import { CreateCuestionarioDto } from './dto/create-cuestionario.dto'; import { UpdateCuestionarioDto } from './dto/update-cuestionario.dto'; +import { CuestionarioApiDocumentation } from './cuestionario.documentation'; @Controller('cuestionario') +@CuestionarioApiDocumentation.ApiController export class CuestionarioController { constructor(private readonly cuestionarioService: CuestionarioService) {} @Post() + @CuestionarioApiDocumentation.ApiCreate create(@Body() createCuestionarioDto: CreateCuestionarioDto) { return this.cuestionarioService.create(createCuestionarioDto); } @Get() + @CuestionarioApiDocumentation.ApiGetAll findAll() { return this.cuestionarioService.findAll(); } @Get(':id') + @CuestionarioApiDocumentation.ApiGetOne findOne(@Param('id') id: string) { return this.cuestionarioService.findOne(+id); } @Patch(':id') + @CuestionarioApiDocumentation.ApiUpdate update(@Param('id') id: string, @Body() updateCuestionarioDto: UpdateCuestionarioDto) { return this.cuestionarioService.update(+id, updateCuestionarioDto); } @Delete(':id') + @CuestionarioApiDocumentation.ApiRemove remove(@Param('id') id: string) { return this.cuestionarioService.remove(+id); } diff --git a/src/cuestionario/cuestionario.documentation.ts b/src/cuestionario/cuestionario.documentation.ts new file mode 100644 index 0000000..3b8cc7d --- /dev/null +++ b/src/cuestionario/cuestionario.documentation.ts @@ -0,0 +1,181 @@ +import { ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { feriaSexualidad } from '../../utils/crear_formulario_feria'; +import { applyDecorators } from '@nestjs/common'; + +export class CuestionarioApiDocumentation { + // Decoradores para toda la clase del controlador + static ApiController = ApiTags('Cuestionarios'); + + // 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', + 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' }) + ); +} \ No newline at end of file diff --git a/src/cuestionario/cuestionario.module.ts b/src/cuestionario/cuestionario.module.ts index c6ba022..642215f 100644 --- a/src/cuestionario/cuestionario.module.ts +++ b/src/cuestionario/cuestionario.module.ts @@ -3,11 +3,29 @@ import { CuestionarioService } from './cuestionario.service'; import { CuestionarioController } from './cuestionario.controller'; import { TypeOrmModule } from '@nestjs/typeorm'; import { Cuestionario } from './entities/cuestionario.entity'; +import { Seccion } from '../seccion/entities/seccion.entity'; +import { CuestionarioSeccion } from '../cuestionario_seccion/entities/cuestionario_seccion.entity'; +import { Pregunta } from '../pregunta/entities/pregunta.entity'; +import { SeccionPregunta } from '../seccion_pregunta/entities/seccion_pregunta.entity'; +import { Opcion } from '../opcion/entities/opcion.entity'; +import { PreguntaOpcion } from '../pregunta_opcion/entities/pregunta_opcion.entity'; +import { TipoPregunta } from '../tipo_pregunta/entities/tipo_pregunta.entity'; @Module({ - imports: [TypeOrmModule.forFeature([Cuestionario])], + imports: [ + TypeOrmModule.forFeature([ + Cuestionario, + Seccion, + CuestionarioSeccion, + Pregunta, + SeccionPregunta, + Opcion, + PreguntaOpcion, + TipoPregunta + ]) + ], controllers: [CuestionarioController], providers: [CuestionarioService], - exports: [TypeOrmModule] + exports: [TypeOrmModule, CuestionarioService] }) export class CuestionarioModule {} diff --git a/src/cuestionario/cuestionario.service.ts b/src/cuestionario/cuestionario.service.ts index 6a8f5ac..784cd9d 100644 --- a/src/cuestionario/cuestionario.service.ts +++ b/src/cuestionario/cuestionario.service.ts @@ -1,26 +1,163 @@ import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository, DataSource } from 'typeorm'; import { CreateCuestionarioDto } from './dto/create-cuestionario.dto'; import { UpdateCuestionarioDto } from './dto/update-cuestionario.dto'; +import { Cuestionario } from './entities/cuestionario.entity'; +import { Seccion } from '../seccion/entities/seccion.entity'; +import { CuestionarioSeccion } from '../cuestionario_seccion/entities/cuestionario_seccion.entity'; +import { Pregunta } from '../pregunta/entities/pregunta.entity'; +import { SeccionPregunta } from '../seccion_pregunta/entities/seccion_pregunta.entity'; +import { Opcion } from '../opcion/entities/opcion.entity'; +import { PreguntaOpcion } from '../pregunta_opcion/entities/pregunta_opcion.entity'; +import { TipoPregunta } from '../tipo_pregunta/entities/tipo_pregunta.entity'; @Injectable() export class CuestionarioService { - create(createCuestionarioDto: CreateCuestionarioDto) { - return 'This action adds a new cuestionario'; + constructor( + @InjectRepository(Cuestionario) + private cuestionarioRepository: Repository, + @InjectRepository(Seccion) + private seccionRepository: Repository, + @InjectRepository(CuestionarioSeccion) + private cuestionarioSeccionRepository: Repository, + @InjectRepository(Pregunta) + private preguntaRepository: Repository, + @InjectRepository(SeccionPregunta) + private seccionPreguntaRepository: Repository, + @InjectRepository(Opcion) + private opcionRepository: Repository, + @InjectRepository(PreguntaOpcion) + private preguntaOpcionRepository: Repository, + @InjectRepository(TipoPregunta) + private tipoPreguntaRepository: Repository, + private dataSource: DataSource, + ) {} + + async create(createCuestionarioDto: CreateCuestionarioDto) { + const queryRunner = this.dataSource.createQueryRunner(); + await queryRunner.connect(); + await queryRunner.startTransaction(); + + try { + // 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(); + } } findAll() { - return `This action returns all cuestionario`; + return this.cuestionarioRepository.find(); } findOne(id: number) { - return `This action returns a #${id} cuestionario`; + return this.cuestionarioRepository.findOne({ where: { id_cuestionario: id } }); } update(id: number, updateCuestionarioDto: UpdateCuestionarioDto) { - return `This action updates a #${id} cuestionario`; + return this.cuestionarioRepository.update(id, updateCuestionarioDto); } remove(id: number) { - return `This action removes a #${id} cuestionario`; + return this.cuestionarioRepository.delete(id); } } diff --git a/src/cuestionario/dto/create-cuestionario.dto.ts b/src/cuestionario/dto/create-cuestionario.dto.ts index d752739..2930547 100644 --- a/src/cuestionario/dto/create-cuestionario.dto.ts +++ b/src/cuestionario/dto/create-cuestionario.dto.ts @@ -1 +1,31 @@ -export class CreateCuestionarioDto {} +import { CreateSeccionDto } from '../../seccion/dto/create-seccion.dto'; +import { IsArray, IsDateString, IsNotEmpty, IsNumber, IsOptional, IsString, ValidateNested } from 'class-validator'; +import { Type } from 'class-transformer'; + +export class CreateCuestionarioDto { + @IsNotEmpty() + @IsString() + nombre_form: string; + + @IsOptional() + @IsString() + descripcion?: string; + + @IsOptional() + @IsDateString() + fecha_inicio?: Date; + + @IsOptional() + @IsDateString() + fecha_fin?: Date; + + @IsNotEmpty() + @IsNumber() + id_tipo_cuestionario: number; + + @IsOptional() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => CreateSeccionDto) + secciones?: CreateSeccionDto[]; +} diff --git a/src/cuestionario/entities/cuestionario.entity.ts b/src/cuestionario/entities/cuestionario.entity.ts index f755b13..d7559d8 100644 --- a/src/cuestionario/entities/cuestionario.entity.ts +++ b/src/cuestionario/entities/cuestionario.entity.ts @@ -16,16 +16,16 @@ export class Cuestionario { contador_secciones: number; @Column({ type: 'text', nullable: true }) - descripcion: string; + descripcion?: string; @Column({ type: 'boolean', default: true }) editable: boolean; @Column({ type: 'datetime', nullable: true }) - fecha_fin: Date; + fecha_fin?: Date; @Column({ type: 'datetime', nullable: true }) - fecha_inicio: Date; + fecha_inicio?: Date; @OneToMany(() => CuestionarioSeccion,(cuestionario_seccion) => cuestionario_seccion.id_cuestionario) diff --git a/src/docs/swagger.config.ts b/src/docs/swagger.config.ts index e0e99a1..2f0a0b2 100644 --- a/src/docs/swagger.config.ts +++ b/src/docs/swagger.config.ts @@ -1,11 +1,31 @@ import { DocumentBuilder } from '@nestjs/swagger'; export const swaggerConfig = new DocumentBuilder() - .setTitle('Sistema de Registro de Usuarios y Eventos') - .setDescription('API para gestionar usuarios, eventos, asistencia y códigos QR') + .setTitle('Sistema de Formularios y Eventos CIDWA') + .setDescription(` + API para la gestión de formularios, cuestionarios, eventos y asistencias. + + ## Funcionalidades principales + + - **Cuestionarios**: Creación y gestión de formularios con secciones y preguntas + - **Eventos**: Registro y control de eventos + - **Participantes**: Gestión de usuarios y participantes + - **Asistencia**: Control de asistencia a eventos + - **QR**: Generación y validación de códigos QR + + ## Ejemplos de uso + + La API permite crear cuestionarios complejos con secciones y diferentes tipos de preguntas. + Consulte la documentación de cada endpoint para ver ejemplos específicos. + `) .setVersion('1.0') + .addTag('Cuestionarios') + .addTag('Secciones') + .addTag('Preguntas') + .addTag('Opciones') .addTag('Usuarios') .addTag('Eventos') .addTag('Asistencia') - .addTag('QR') + .addTag('QR') + .addBearerAuth() .build(); diff --git a/src/main.ts b/src/main.ts index 3eabdcd..3f43b41 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,13 +2,55 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import 'dotenv/config'; import { DocsModule } from './docs/docs.module'; +import { DataSource } from 'typeorm'; +import { TipoPregunta } from './tipo_pregunta/entities/tipo_pregunta.entity'; +import { ValidationPipe } from '@nestjs/common'; async function bootstrap() { const app = await NestFactory.create(AppModule); - //await app.listen(process.env.PORT ?? 3000); + + // Configuración de validación global + app.useGlobalPipes( + new ValidationPipe({ + whitelist: true, // Elimina propiedades no decoradas + forbidNonWhitelisted: true, // Arroja error si hay propiedades no decoradas + transform: true, // Transforma los datos recibidos al tipo definido en el DTO + }) + ); + + // CORS configuration + app.enableCors({ + origin: '*', + methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'], + allowedHeaders: ['Content-Type', 'Authorization'], + }); + // Swagger setup DocsModule.setupSwagger(app); + // Seed TipoPregunta data + const dataSource = app.get(DataSource); + const tiposPreguntaRepository = dataSource.getRepository(TipoPregunta); + + // Check if data exists + const count = await tiposPreguntaRepository.count(); + + if (count === 0) { + // Create default tipos de pregunta + const tiposPregunta = [ + { tipo_pregunta: 'Texto' }, + { tipo_pregunta: 'Numero' }, + { tipo_pregunta: 'Radio' }, + { tipo_pregunta: 'Multiple' }, + { tipo_pregunta: 'Abierto' }, + { tipo_pregunta: 'Fecha' }, + ]; + + await tiposPreguntaRepository.save(tiposPregunta); + console.log('Tipos de pregunta creados'); + } + + // Start the server await app.listen(process.env.PORT ?? 3000); console.log('API en ejecución en http://localhost:3000'); console.log('Swagger disponible en http://localhost:3000/api-docs'); diff --git a/src/pregunta/dto/create-pregunta.dto.ts b/src/pregunta/dto/create-pregunta.dto.ts index ee5d15c..fd6c832 100644 --- a/src/pregunta/dto/create-pregunta.dto.ts +++ b/src/pregunta/dto/create-pregunta.dto.ts @@ -1 +1,36 @@ -export class CreatePreguntaDto {} +import { Type } from 'class-transformer'; +import { IsArray, IsBoolean, IsNotEmpty, IsNumber, IsOptional, IsString, ValidateNested } from 'class-validator'; + +export class OpcionDto { + @IsNotEmpty() + @IsString() + valor: string; +} + +export class CreatePreguntaDto { + @IsNotEmpty() + @IsString() + titulo: string; + + @IsOptional() + @IsBoolean() + obligatoria?: boolean; + + @IsNotEmpty() + @IsString() + tipo: string; + + @IsOptional() + @IsNumber() + contador_opcion?: number; + + @IsOptional() + @IsNumber() + id_opcion_dependiente?: number; + + @IsOptional() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => OpcionDto) + opciones?: OpcionDto[]; +} diff --git a/src/pregunta/entities/pregunta.entity.ts b/src/pregunta/entities/pregunta.entity.ts index 1b67797..a912496 100644 --- a/src/pregunta/entities/pregunta.entity.ts +++ b/src/pregunta/entities/pregunta.entity.ts @@ -25,8 +25,8 @@ export class Pregunta { @Column() id_tipo_pregunta: number; - @Column({ nullable: true }) - id_opcion_dependiente: number; + @Column({ type: 'int', nullable: true }) + id_opcion_dependiente?: number; @OneToMany(() => SeccionPregunta, seccionPregunta => seccionPregunta.pregunta) seccionPreguntas: SeccionPregunta[]; diff --git a/src/pregunta/pregunta.controller.ts b/src/pregunta/pregunta.controller.ts index c5672b4..b3c6757 100644 --- a/src/pregunta/pregunta.controller.ts +++ b/src/pregunta/pregunta.controller.ts @@ -2,32 +2,39 @@ import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/commo import { PreguntaService } from './pregunta.service'; import { CreatePreguntaDto } from './dto/create-pregunta.dto'; import { UpdatePreguntaDto } from './dto/update-pregunta.dto'; +import { PreguntaApiDocumentation } from './pregunta.documentation'; @Controller('pregunta') +@PreguntaApiDocumentation.ApiController export class PreguntaController { constructor(private readonly preguntaService: PreguntaService) {} @Post() + @PreguntaApiDocumentation.ApiCreate create(@Body() createPreguntaDto: CreatePreguntaDto) { return this.preguntaService.create(createPreguntaDto); } @Get() + @PreguntaApiDocumentation.ApiGetAll findAll() { return this.preguntaService.findAll(); } @Get(':id') + @PreguntaApiDocumentation.ApiGetOne findOne(@Param('id') id: string) { return this.preguntaService.findOne(+id); } @Patch(':id') + @PreguntaApiDocumentation.ApiUpdate update(@Param('id') id: string, @Body() updatePreguntaDto: UpdatePreguntaDto) { return this.preguntaService.update(+id, updatePreguntaDto); } @Delete(':id') + @PreguntaApiDocumentation.ApiRemove remove(@Param('id') id: string) { return this.preguntaService.remove(+id); } diff --git a/src/pregunta/pregunta.documentation.ts b/src/pregunta/pregunta.documentation.ts new file mode 100644 index 0000000..208879a --- /dev/null +++ b/src/pregunta/pregunta.documentation.ts @@ -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' }) + ); +} \ No newline at end of file diff --git a/src/seccion/dto/create-seccion.dto.ts b/src/seccion/dto/create-seccion.dto.ts index 4753a9c..ccb44c1 100644 --- a/src/seccion/dto/create-seccion.dto.ts +++ b/src/seccion/dto/create-seccion.dto.ts @@ -1 +1,23 @@ -export class CreateSeccionDto {} +import { CreatePreguntaDto } from '../../pregunta/dto/create-pregunta.dto'; +import { IsArray, IsNotEmpty, IsNumber, IsOptional, IsString, ValidateNested } from 'class-validator'; +import { Type } from 'class-transformer'; + +export class CreateSeccionDto { + @IsNotEmpty() + @IsString() + titulo: string; + + @IsOptional() + @IsString() + descripcion?: string; + + @IsOptional() + @IsNumber() + contador_pregunta?: number; + + @IsOptional() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => CreatePreguntaDto) + preguntas?: CreatePreguntaDto[]; +} diff --git a/src/seccion/entities/seccion.entity.ts b/src/seccion/entities/seccion.entity.ts index 6e27cd6..a5c3b6d 100644 --- a/src/seccion/entities/seccion.entity.ts +++ b/src/seccion/entities/seccion.entity.ts @@ -11,7 +11,7 @@ export class Seccion { contador_pregunta: number; @Column({ type: 'text', nullable: true }) - descripcion: string; + descripcion?: string; @Column({ type: 'text', nullable: true }) titulo: string; diff --git a/src/seccion/seccion.controller.ts b/src/seccion/seccion.controller.ts index f3e3dcf..d3967c8 100644 --- a/src/seccion/seccion.controller.ts +++ b/src/seccion/seccion.controller.ts @@ -2,32 +2,39 @@ import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/commo import { SeccionService } from './seccion.service'; import { CreateSeccionDto } from './dto/create-seccion.dto'; import { UpdateSeccionDto } from './dto/update-seccion.dto'; +import { SeccionApiDocumentation } from './seccion.documentation'; @Controller('seccion') +@SeccionApiDocumentation.ApiController export class SeccionController { constructor(private readonly seccionService: SeccionService) {} @Post() + @SeccionApiDocumentation.ApiCreate create(@Body() createSeccionDto: CreateSeccionDto) { return this.seccionService.create(createSeccionDto); } @Get() + @SeccionApiDocumentation.ApiGetAll findAll() { return this.seccionService.findAll(); } @Get(':id') + @SeccionApiDocumentation.ApiGetOne findOne(@Param('id') id: string) { return this.seccionService.findOne(+id); } @Patch(':id') + @SeccionApiDocumentation.ApiUpdate update(@Param('id') id: string, @Body() updateSeccionDto: UpdateSeccionDto) { return this.seccionService.update(+id, updateSeccionDto); } @Delete(':id') + @SeccionApiDocumentation.ApiRemove remove(@Param('id') id: string) { return this.seccionService.remove(+id); } diff --git a/src/seccion/seccion.documentation.ts b/src/seccion/seccion.documentation.ts new file mode 100644 index 0000000..72d4976 --- /dev/null +++ b/src/seccion/seccion.documentation.ts @@ -0,0 +1,185 @@ +import { ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { applyDecorators } from '@nestjs/common'; + +export class SeccionApiDocumentation { + // Decoradores para toda la clase del controlador + static ApiController = ApiTags('Secciones'); + + // Documentación para crear una sección + static ApiCreate = applyDecorators( + ApiOperation({ + summary: 'Crear una nueva sección', + description: 'Crea una nueva sección para un cuestionario' + }), + ApiBody({ + description: 'Datos de la sección a crear', + schema: { + type: 'object', + required: ['titulo'], + properties: { + titulo: { type: 'string', example: 'Información Personal' }, + descripcion: { + type: 'string', + example: 'Proporciona tus datos personales para poder contactarte' + }, + contador_pregunta: { type: 'number', example: 0 }, + preguntas: { + type: 'array', + items: { + type: 'object', + properties: { + titulo: { type: 'string', example: '¿Eres parte de la comunidad?' }, + obligatoria: { type: 'boolean', example: true }, + tipo: { type: 'string', example: 'Multiple' }, + opciones: { + type: 'array', + items: { + type: 'object', + properties: { + valor: { type: 'string', example: 'Si' } + } + } + } + } + } + } + } + } + }), + ApiResponse({ + status: 201, + description: 'Sección creada correctamente', + schema: { + type: 'object', + properties: { + id_seccion: { type: 'number', example: 1 }, + titulo: { type: 'string', example: 'Información Personal' }, + descripcion: { type: 'string' }, + contador_pregunta: { type: 'number', example: 0 } + } + } + }), + ApiResponse({ status: 400, description: 'Datos de la sección inválidos' }), + ApiResponse({ status: 500, description: 'Error interno del servidor' }) + ); + + // Documentación para obtener todas las secciones + static ApiGetAll = applyDecorators( + ApiOperation({ + summary: 'Obtener todas las secciones', + description: 'Retorna una lista de todas las secciones registradas' + }), + ApiResponse({ + status: 200, + description: 'Lista de secciones obtenida correctamente', + schema: { + type: 'array', + items: { + type: 'object', + properties: { + id_seccion: { type: 'number', example: 1 }, + titulo: { type: 'string', example: 'Información Personal' }, + descripcion: { type: 'string' }, + contador_pregunta: { type: 'number', example: 0 } + } + } + } + }), + ApiResponse({ status: 500, description: 'Error interno del servidor' }) + ); + + // Documentación para obtener una sección por ID + static ApiGetOne = applyDecorators( + ApiOperation({ + summary: 'Obtener una sección por ID', + description: 'Retorna una sección específica por su ID' + }), + ApiParam({ + name: 'id', + description: 'ID de la sección', + required: true, + type: 'number', + example: 1 + }), + ApiResponse({ + status: 200, + description: 'Sección obtenida correctamente', + schema: { + type: 'object', + properties: { + id_seccion: { type: 'number', example: 1 }, + titulo: { type: 'string', example: 'Información Personal' }, + descripcion: { type: 'string' }, + contador_pregunta: { type: 'number', example: 0 } + } + } + }), + ApiResponse({ status: 404, description: 'Sección no encontrada' }), + ApiResponse({ status: 500, description: 'Error interno del servidor' }) + ); + + // Documentación para actualizar una sección + static ApiUpdate = applyDecorators( + ApiOperation({ + summary: 'Actualizar una sección', + description: 'Actualiza los datos de una sección existente' + }), + ApiParam({ + name: 'id', + description: 'ID de la sección a actualizar', + required: true, + type: 'number', + example: 1 + }), + ApiBody({ + description: 'Datos a actualizar de la sección', + schema: { + type: 'object', + properties: { + titulo: { type: 'string', example: 'Título actualizado' }, + descripcion: { type: 'string', example: 'Descripción actualizada' } + } + } + }), + ApiResponse({ + status: 200, + description: 'Sección actualizada correctamente', + schema: { + type: 'object', + properties: { + affected: { type: 'number', example: 1 } + } + } + }), + ApiResponse({ status: 400, description: 'Datos de actualización inválidos' }), + ApiResponse({ status: 404, description: 'Sección no encontrada' }), + ApiResponse({ status: 500, description: 'Error interno del servidor' }) + ); + + // Documentación para eliminar una sección + static ApiRemove = applyDecorators( + ApiOperation({ + summary: 'Eliminar una sección', + description: 'Elimina permanentemente una sección por su ID' + }), + ApiParam({ + name: 'id', + description: 'ID de la sección a eliminar', + required: true, + type: 'number', + example: 1 + }), + ApiResponse({ + status: 200, + description: 'Sección eliminada correctamente', + schema: { + type: 'object', + properties: { + affected: { type: 'number', example: 1 } + } + } + }), + ApiResponse({ status: 404, description: 'Sección no encontrada' }), + ApiResponse({ status: 500, description: 'Error interno del servidor' }) + ); +} \ No newline at end of file diff --git a/utils/crear_formulario.json b/utils/crear_formulario.json deleted file mode 100644 index e69de29..0000000 diff --git a/utils/crear_formulario_feria.json b/utils/crear_formulario_feria.json deleted file mode 100644 index e69de29..0000000 diff --git a/utils/crear_formulario_feria.ts b/utils/crear_formulario_feria.ts new file mode 100644 index 0000000..23fda26 --- /dev/null +++ b/utils/crear_formulario_feria.ts @@ -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, + }, + ], + }, + ], + }; \ No newline at end of file diff --git a/utils/load-form.js b/utils/load-form.js new file mode 100644 index 0000000..800e54b --- /dev/null +++ b/utils/load-form.js @@ -0,0 +1,46 @@ +// Script para cargar un formulario desde un archivo JSON + +const axios = require('axios'); +const fs = require('fs'); +const path = require('path'); + +const API_URL = 'http://localhost:3000'; + +async function loadFormFromFile(filePath) { + try { + // Leer el archivo JSON + const formData = require(filePath); + + console.log('Enviando datos del formulario al servidor...'); + console.log('Datos:', JSON.stringify(formData, null, 2)); + + // Enviar los datos al endpoint + const response = await axios.post(`${API_URL}/cuestionario`, formData); + + console.log('Formulario creado exitosamente:'); + console.log(JSON.stringify(response.data, null, 2)); + + return response.data; + } catch (error) { + console.error('Error al crear el formulario:'); + if (error.response) { + console.error('Status:', error.response.status); + console.error('Data:', JSON.stringify(error.response.data, null, 2)); + } else { + console.error(error.message); + } + throw error; + } +} + +// Verificar si se proporcionó un argumento de archivo +const args = process.argv.slice(2); +const filePath = args[0] || './crear_formulario_feria'; + +// Ejecutar la función +loadFormFromFile(filePath) + .then(() => console.log('Proceso completado')) + .catch(() => { + console.log('Proceso fallido'); + process.exit(1); + }); \ No newline at end of file diff --git a/utils/respuesta_formulario_feria.ts b/utils/respuesta_formulario_feria.ts new file mode 100644 index 0000000..0a2b5fe --- /dev/null +++ b/utils/respuesta_formulario_feria.ts @@ -0,0 +1,21 @@ +const comunidad = { + id_formulario: 1, + respuestas: [ + { id_pregunta: 101, valor: 'Si' }, // ¿Eres parte de la comunidad de la FES Acatlán? + { id_pregunta: 102, valor: '123456789' }, // Numero de cuenta + { id_pregunta: 106, valor: 'Prefiero no decirlo' }, + ], + fecha_envio: '2025-04-02T13:45:00', + }; + + const externos = { + id_formulario: 1, + respuestas: [ + { id_pregunta: 101, valor: 'No' }, // ¿Eres parte de la comunidad de la FES Acatlán? + { id_pregunta: 103, valor: 'Juan Carlos' }, // Nombre(s) + { id_pregunta: 104, valor: 'Ramírez López' }, // Apellidos + { id_pregunta: 106, valor: 'Prefiero no decirlo' }, // genero + { id_pregunta: 107, valor: 'UAM Azcapotzalco' }, // Institución de procedencia + ], + fecha_envio: '2025-04-02T13:45:00', + }; \ No newline at end of file diff --git a/utils/test-crear-formulario.ts b/utils/test-crear-formulario.ts new file mode 100644 index 0000000..7e88c02 --- /dev/null +++ b/utils/test-crear-formulario.ts @@ -0,0 +1,28 @@ +import { feriaSexualidad } from './crear_formulario_feria'; +import axios from 'axios'; + +// Asumiendo que el servidor está corriendo en localhost:3000 +const API_URL = 'http://localhost:3000'; + +async function crearFormulario() { + try { + const response = await axios.post(`${API_URL}/cuestionario`, feriaSexualidad); + console.log('Formulario creado exitosamente:'); + console.log(JSON.stringify(response.data, null, 2)); + return response.data; + } catch (error) { + console.error('Error al crear el formulario:'); + if (error.response) { + console.error('Status:', error.response.status); + console.error('Data:', error.response.data); + } else { + console.error(error.message); + } + throw error; + } +} + +// Ejecutar la función +crearFormulario() + .then(() => console.log('Proceso completado')) + .catch(() => console.log('Proceso fallido')); \ No newline at end of file