Merge pull request 'eithan' (#7) from eithan into develop

Reviewed-on: #7
This commit was merged in pull request #7.
This commit is contained in:
2025-04-02 12:44:32 -06:00
55 changed files with 2379 additions and 103 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:
+45 -8
View File
@@ -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",
+2
View File
@@ -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",
+9 -4
View File
@@ -29,7 +29,7 @@ import { ParticipanteEventoModule } from './participante_evento/participante_eve
imports: [
ConfigModule.forRoot(),
TypeOrmModule.forRoot({
/*
type: 'mysql',
host: process.env.db_host, //process.env.db_host
username: process.env.db_username,
@@ -40,16 +40,21 @@ import { ParticipanteEventoModule } from './participante_evento/participante_eve
dropSchema: true, // elimina la base de datos
// logging: true, // Habilita los logs para depuración
autoLoadEntities: true, // Carga automáticamente las entidades
*/
//esta es mi base de datos local
/*
type: 'mysql',
host: 'localhost',
port: 3306, //3306
username: 'root',
password: 'admin', //admin
database: 'cidwa', //nestdb
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: true,
*/
// entities: [__dirname + '/**/*.entity{.ts,.js}'],
// synchronize: true,
//extra
retryAttempts: 10, // Intentos para reconectar
retryDelay: 3000, // Tiempo entre reintentos
@@ -2,32 +2,46 @@ import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/commo
import { CuestionarioService } from './cuestionario.service';
import { CreateCuestionarioDto } from './dto/create-cuestionario.dto';
import { UpdateCuestionarioDto } from './dto/update-cuestionario.dto';
import { CuestionarioApiDocumentation } from './cuestionario.documentation';
import { ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger';
@Controller('cuestionario')
@CuestionarioApiDocumentation.ApiController
export class CuestionarioController {
constructor(private readonly cuestionarioService: CuestionarioService) {}
@Post()
@CuestionarioApiDocumentation.ApiCreate
create(@Body() createCuestionarioDto: CreateCuestionarioDto) {
return this.cuestionarioService.create(createCuestionarioDto);
}
@Get()
@CuestionarioApiDocumentation.ApiGetAll
findAll() {
return this.cuestionarioService.findAll();
}
@Get(':id')
@CuestionarioApiDocumentation.ApiGetOne
findOne(@Param('id') id: string) {
return this.cuestionarioService.findOne(+id);
}
@Get(':id/formulario')
@CuestionarioApiDocumentation.ApiGetFormulario
findFormulario(@Param('id') id: string) {
return this.cuestionarioService.findCompleto(+id);
}
@Patch(':id')
@CuestionarioApiDocumentation.ApiUpdate
update(@Param('id') id: string, @Body() updateCuestionarioDto: UpdateCuestionarioDto) {
return this.cuestionarioService.update(+id, updateCuestionarioDto);
}
@Delete(':id')
@CuestionarioApiDocumentation.ApiRemove
remove(@Param('id') id: string) {
return this.cuestionarioService.remove(+id);
}
@@ -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' })
);
}
+24
View File
@@ -1,9 +1,33 @@
import { Module } from '@nestjs/common';
import { CuestionarioService } from './cuestionario.service';
import { CuestionarioController } from './cuestionario.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Cuestionario } from './entities/cuestionario.entity';
import { Seccion } from '../seccion/entities/seccion.entity';
import { CuestionarioSeccion } from '../cuestionario_seccion/entities/cuestionario_seccion.entity';
import { Pregunta } from '../pregunta/entities/pregunta.entity';
import { SeccionPregunta } from '../seccion_pregunta/entities/seccion_pregunta.entity';
import { Opcion } from '../opcion/entities/opcion.entity';
import { PreguntaOpcion } from '../pregunta_opcion/entities/pregunta_opcion.entity';
import { TipoPregunta } from '../tipo_pregunta/entities/tipo_pregunta.entity';
import { TipoCuestionario } from '../tipo_cuestionario/entities/tipo_cuestionario.entity';
@Module({
imports: [
TypeOrmModule.forFeature([
Cuestionario,
Seccion,
CuestionarioSeccion,
Pregunta,
SeccionPregunta,
Opcion,
PreguntaOpcion,
TipoPregunta,
TipoCuestionario
])
],
controllers: [CuestionarioController],
providers: [CuestionarioService],
exports: [TypeOrmModule, CuestionarioService]
})
export class CuestionarioModule {}
+293 -8
View File
@@ -1,26 +1,311 @@
import { Injectable } from '@nestjs/common';
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, DataSource, In } from 'typeorm';
import { CreateCuestionarioDto } from './dto/create-cuestionario.dto';
import { UpdateCuestionarioDto } from './dto/update-cuestionario.dto';
import { Cuestionario } from './entities/cuestionario.entity';
import { Seccion } from '../seccion/entities/seccion.entity';
import { CuestionarioSeccion } from '../cuestionario_seccion/entities/cuestionario_seccion.entity';
import { Pregunta } from '../pregunta/entities/pregunta.entity';
import { SeccionPregunta } from '../seccion_pregunta/entities/seccion_pregunta.entity';
import { Opcion } from '../opcion/entities/opcion.entity';
import { PreguntaOpcion } from '../pregunta_opcion/entities/pregunta_opcion.entity';
import { TipoPregunta } from '../tipo_pregunta/entities/tipo_pregunta.entity';
import { TipoCuestionario } from '../tipo_cuestionario/entities/tipo_cuestionario.entity';
@Injectable()
export class CuestionarioService {
create(createCuestionarioDto: CreateCuestionarioDto) {
return 'This action adds a new cuestionario';
constructor(
@InjectRepository(Cuestionario)
private cuestionarioRepository: Repository<Cuestionario>,
@InjectRepository(Seccion)
private seccionRepository: Repository<Seccion>,
@InjectRepository(CuestionarioSeccion)
private cuestionarioSeccionRepository: Repository<CuestionarioSeccion>,
@InjectRepository(Pregunta)
private preguntaRepository: Repository<Pregunta>,
@InjectRepository(SeccionPregunta)
private seccionPreguntaRepository: Repository<SeccionPregunta>,
@InjectRepository(Opcion)
private opcionRepository: Repository<Opcion>,
@InjectRepository(PreguntaOpcion)
private preguntaOpcionRepository: Repository<PreguntaOpcion>,
@InjectRepository(TipoPregunta)
private tipoPreguntaRepository: Repository<TipoPregunta>,
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`;
async findOne(id: number) {
const cuestionario = await this.cuestionarioRepository.findOne({
where: { id_cuestionario: id }
});
if (!cuestionario) {
throw new NotFoundException(`Cuestionario con ID ${id} no encontrado`);
}
return cuestionario;
}
async findCompleto(id: number) {
// Obtener el cuestionario básico
const cuestionario = await this.cuestionarioRepository.findOne({
where: { id_cuestionario: id }
});
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' }
});
// 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));
// Construir objeto final respetando el formato de get_formulario_feria.ts
return {
tipo_cuestionario: {
id_tipo_cuestionario: tipoCuestionario.id_tipo_cuestionario,
tipo_cuestionario: tipoCuestionario.tipo_cuestionario
},
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
}
};
}
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);
}
}
@@ -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[];
}
+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[];
}
@@ -16,29 +16,27 @@ export class Cuestionario {
contador_secciones: number;
@Column({ type: 'text', nullable: true })
descripcion: string;
descripcion?: string;
@Column({ type: 'boolean', default: true })
editable: boolean;
@Column({ type: 'datetime', nullable: true })
fecha_fin: Date;
fecha_fin?: Date;
@Column({ type: 'datetime', nullable: true })
fecha_inicio: Date;
fecha_inicio?: Date;
@OneToMany(() => CuestionarioSeccion,(cuestionario_seccion) => cuestionario_seccion.id_cuestionario)
cuestionarioSeccion:CuestionarioSeccion[]
@OneToMany(()=> Cuestionario, (cuestionario) => cuestionario.id_cuestionario)
@Column({ type: 'int' })
@Column({ type: 'int', nullable: true })
id_cuestionario_original: number;
@OneToMany(()=> TipoCuestionario, (tipo_cuestionario) => tipo_cuestionario.id_tipo_cuestionario )
@Column({ type: 'int' })
id_tipo_cuestionario: number;
@ManyToOne(()=> Cuestionario, (cuestionario) => cuestionario.id_cuestionario)
Cuestionario: Cuestionario[];
@ManyToOne(()=> Cuestionario)
cuestionarioOriginal: Cuestionario;
}
@@ -1,9 +1,19 @@
import { Module } from '@nestjs/common';
import { CuestionarioRespondidoService } from './cuestionario_respondido.service';
import { CuestionarioRespondidoController } from './cuestionario_respondido.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CuestionarioRespondido } from './entities/cuestionario_respondido.entity';
import { CuestionarioModule } from '../cuestionario/cuestionario.module';
import { ParticipanteModule } from '../participante/participante.module';
@Module({
imports: [
TypeOrmModule.forFeature([CuestionarioRespondido]),
CuestionarioModule,
ParticipanteModule
],
controllers: [CuestionarioRespondidoController],
providers: [CuestionarioRespondidoService],
exports: [TypeOrmModule]
})
export class CuestionarioRespondidoModule {}
@@ -16,18 +16,18 @@ export class CuestionarioRespondido {
// Relación con Participante
@ManyToOne(() => Participante, participante => participante.cuestionariosRespondidos)
@JoinColumn({ name: 'id_participante' })
id_participante: Participante;
participante: Participante;
// Relación con Cuestionario
@ManyToOne(() => Cuestionario, cuestionario => cuestionario.id_cuestionario)
@ManyToOne(() => Cuestionario)
@JoinColumn({ name: 'id_cuestionario' })
id_cuestionario: Cuestionario;
cuestionario: Cuestionario;
// Relación con RespuestasCerradas
@OneToMany(() => RespuestaParticipanteCerrada, respuestaCerrada => respuestaCerrada.id_cuestionario_respondido)
@OneToMany(() => RespuestaParticipanteCerrada, respuestaCerrada => respuestaCerrada.cuestionarioRespondido)
respuestasCerradas: RespuestaParticipanteCerrada[];
// Relación con RespuestasAbiertas
@OneToMany(() => RespuestaParticipanteAbierta, respuestaAbierta => respuestaAbierta.id_cuestionario_respondido)
@OneToMany(() => RespuestaParticipanteAbierta, respuestaAbierta => respuestaAbierta.cuestionarioRespondido)
respuestasAbiertas: RespuestaParticipanteAbierta[];
}
@@ -1,9 +1,19 @@
import { Module } from '@nestjs/common';
import { CuestionarioSeccionService } from './cuestionario_seccion.service';
import { CuestionarioSeccionController } from './cuestionario_seccion.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CuestionarioSeccion } from './entities/cuestionario_seccion.entity';
import { CuestionarioModule } from '../cuestionario/cuestionario.module';
import { SeccionModule } from '../seccion/seccion.module';
@Module({
imports: [
TypeOrmModule.forFeature([CuestionarioSeccion]),
CuestionarioModule,
SeccionModule
],
controllers: [CuestionarioSeccionController],
providers: [CuestionarioSeccionService],
exports: [TypeOrmModule]
})
export class CuestionarioSeccionModule {}
@@ -11,11 +11,15 @@ export class CuestionarioSeccion {
posicion: number;
@ManyToOne(() => Cuestionario, (cuestionario) => cuestionario.id_cuestionario)
@ManyToOne(() => Cuestionario, (cuestionario) => cuestionario.cuestionarioSeccion)
cuestionario: Cuestionario;
@Column({ type: 'int' })
id_cuestionario: number;
@ManyToOne(() => Seccion, (seccion) => seccion.id_seccion)
@ManyToOne(() => Seccion, (seccion) => seccion.seccion)
seccion: Seccion;
@Column({type:'int'})
id_seccion:number;
}
+2 -2
View File
@@ -6,7 +6,7 @@ import { INestApplication } from '@nestjs/common';
@Module({})
export class DocsModule {
static setupSwagger(app: INestApplication) {
const document = SwaggerModule.createDocument(app, swaggerConfig);
SwaggerModule.setup('api-docs', app, document);
const document = SwaggerModule.createDocument(app, swaggerConfig);
SwaggerModule.setup('api-docs', app, document);
}
}
+23 -3
View File
@@ -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();
+46 -4
View File
@@ -2,15 +2,57 @@ 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);
await app.listen(3000);
console.log('API en ejecución en http://localhost:3000');
console.log('Swagger disponible en http://localhost:3000/api-docs');
// 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();
+2 -2
View File
@@ -9,8 +9,8 @@ export class Opcion {
@Column({ type: String, nullable: false, length: 200, default: '' })
opcion: string;
@OneToMany(() => PreguntaOpcion, (preguntaOpcion) => preguntaOpcion.id_opcion)
Preguntas: PreguntaOpcion[];
@OneToMany(() => PreguntaOpcion, (preguntaOpcion) => preguntaOpcion.opcion)
preguntasOpciones: PreguntaOpcion[];
}
+7
View File
@@ -2,32 +2,39 @@ import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/commo
import { OpcionService } from './opcion.service';
import { CreateOpcionDto } from './dto/create-opcion.dto';
import { UpdateOpcionDto } from './dto/update-opcion.dto';
import { OpcionApiDocumentation } from './opcion.documentation';
@Controller('opcion')
@OpcionApiDocumentation.ApiController
export class OpcionController {
constructor(private readonly opcionService: OpcionService) {}
@Post()
@OpcionApiDocumentation.ApiCreate
create(@Body() createOpcionDto: CreateOpcionDto) {
return this.opcionService.create(createOpcionDto);
}
@Get()
@OpcionApiDocumentation.ApiGetAll
findAll() {
return this.opcionService.findAll();
}
@Get(':id')
@OpcionApiDocumentation.ApiGetOne
findOne(@Param('id') id: string) {
return this.opcionService.findOne(+id);
}
@Patch(':id')
@OpcionApiDocumentation.ApiUpdate
update(@Param('id') id: string, @Body() updateOpcionDto: UpdateOpcionDto) {
return this.opcionService.update(+id, updateOpcionDto);
}
@Delete(':id')
@OpcionApiDocumentation.ApiRemove
remove(@Param('id') id: string) {
return this.opcionService.remove(+id);
}
+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' })
);
}
+1 -1
View File
@@ -8,6 +8,6 @@ import { Opcion } from './entities/opcion.entity';
imports: [TypeOrmModule.forFeature([Opcion])], // Importa el repositorio de Opcion
controllers: [OpcionController],
providers: [OpcionService],
exports: [OpcionService]
exports: [OpcionService, TypeOrmModule]
})
export class OpcionModule {}
+1 -1
View File
@@ -39,7 +39,7 @@ export class Participante {
@OneToMany(() => CuestionarioRespondido, cuestionario => cuestionario.id_participante)
@OneToMany(() => CuestionarioRespondido, cuestionario => cuestionario.participante)
cuestionariosRespondidos: CuestionarioRespondido[];
@OneToMany(() => ParticipanteEvento, participanteEvento => participanteEvento.participante)
+1 -1
View File
@@ -8,6 +8,6 @@ import { Participante } from './participante.entity';
imports: [TypeOrmModule.forFeature([Participante])],
controllers: [ParticipanteController],
providers: [ParticipanteService],
exports: [ParticipanteService],
exports: [ParticipanteService, TypeOrmModule],
})
export class ParticipanteModule {}
+36 -1
View File
@@ -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[];
}
+8 -5
View File
@@ -20,14 +20,17 @@ export class Pregunta {
@ManyToOne(() => TipoPregunta, tipo => tipo.preguntas)
@JoinColumn({ name: 'id_tipo_pregunta' })
id_tipo_pregunta: TipoPregunta;
tipoPregunta: TipoPregunta;
@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.id_pregunta)
@OneToMany(() => SeccionPregunta, seccionPregunta => seccionPregunta.pregunta)
seccionPreguntas: SeccionPregunta[];
@OneToMany(() => PreguntaOpcion, preguntaOpcion => preguntaOpcion.id_pregunta)
@OneToMany(() => PreguntaOpcion, preguntaOpcion => preguntaOpcion.pregunta)
opciones: PreguntaOpcion[];
}
+7
View File
@@ -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);
}
+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' })
);
}
+16
View File
@@ -1,9 +1,25 @@
import { Module } from '@nestjs/common';
import { PreguntaService } from './pregunta.service';
import { PreguntaController } from './pregunta.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Pregunta } from './entities/pregunta.entity';
import { TipoPreguntaModule } from '../tipo_pregunta/tipo_pregunta.module';
import { TipoPregunta } from '../tipo_pregunta/entities/tipo_pregunta.entity';
import { Opcion } from '../opcion/entities/opcion.entity';
import { PreguntaOpcion } from '../pregunta_opcion/entities/pregunta_opcion.entity';
@Module({
imports: [
TypeOrmModule.forFeature([
Pregunta,
TipoPregunta,
Opcion,
PreguntaOpcion
]),
TipoPreguntaModule
],
controllers: [PreguntaController],
providers: [PreguntaService],
exports: [TypeOrmModule, PreguntaService]
})
export class PreguntaModule {}
+240 -12
View File
@@ -1,26 +1,254 @@
import { Injectable } from '@nestjs/common';
import { CreatePreguntaDto } from './dto/create-pregunta.dto';
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, DataSource } from 'typeorm';
import { CreatePreguntaDto, OpcionDto } from './dto/create-pregunta.dto';
import { UpdatePreguntaDto } from './dto/update-pregunta.dto';
import { Pregunta } from './entities/pregunta.entity';
import { TipoPregunta } from '../tipo_pregunta/entities/tipo_pregunta.entity';
import { Opcion } from '../opcion/entities/opcion.entity';
import { PreguntaOpcion } from '../pregunta_opcion/entities/pregunta_opcion.entity';
@Injectable()
export class PreguntaService {
create(createPreguntaDto: CreatePreguntaDto) {
return 'This action adds a new pregunta';
constructor(
@InjectRepository(Pregunta)
private preguntaRepository: Repository<Pregunta>,
@InjectRepository(TipoPregunta)
private tipoPreguntaRepository: Repository<TipoPregunta>,
@InjectRepository(Opcion)
private opcionRepository: Repository<Opcion>,
@InjectRepository(PreguntaOpcion)
private preguntaOpcionRepository: Repository<PreguntaOpcion>,
private dataSource: DataSource
) {}
async create(createPreguntaDto: CreatePreguntaDto) {
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
// Obtener el tipo de pregunta por su nombre
const tipoPregunta = await this.tipoPreguntaRepository.findOne({
where: { tipo_pregunta: createPreguntaDto.tipo }
});
if (!tipoPregunta) {
throw new Error(`Tipo de pregunta '${createPreguntaDto.tipo}' no encontrado`);
}
// Crear pregunta
const pregunta = new Pregunta();
pregunta.pregunta = createPreguntaDto.titulo;
pregunta.obligatoria = createPreguntaDto.obligatoria || false;
pregunta.id_tipo_pregunta = tipoPregunta.id_tipo;
pregunta.id_opcion_dependiente = createPreguntaDto.id_opcion_dependiente || undefined;
pregunta.contador_opcion = createPreguntaDto.opciones?.length || 0;
const savedPregunta = await queryRunner.manager.save(pregunta);
// Crear opciones para preguntas de tipo multiple/radio
if (createPreguntaDto.opciones && createPreguntaDto.opciones.length > 0) {
await this.createOpciones(
queryRunner,
savedPregunta.id_pregunta,
createPreguntaDto.opciones
);
}
await queryRunner.commitTransaction();
return {
success: true,
pregunta: savedPregunta
};
} catch (error) {
await queryRunner.rollbackTransaction();
throw error;
} finally {
await queryRunner.release();
}
}
findAll() {
return `This action returns all pregunta`;
private async createOpciones(
queryRunner: any,
preguntaId: number,
opciones: OpcionDto[]
) {
for (let i = 0; i < opciones.length; i++) {
const opcionDto = opciones[i];
// Crear opción
const opcion = new Opcion();
opcion.opcion = opcionDto.valor;
const savedOpcion = await queryRunner.manager.save(opcion);
// Vincular opción con pregunta
const preguntaOpcion = new PreguntaOpcion();
preguntaOpcion.id_pregunta = preguntaId;
preguntaOpcion.opcion = savedOpcion;
preguntaOpcion.posicion = i + 1;
await queryRunner.manager.save(preguntaOpcion);
}
}
findOne(id: number) {
return `This action returns a #${id} pregunta`;
async findAll() {
return this.preguntaRepository.find();
}
update(id: number, updatePreguntaDto: UpdatePreguntaDto) {
return `This action updates a #${id} pregunta`;
async findOne(id: number) {
const pregunta = await this.preguntaRepository.findOne({
where: { id_pregunta: id }
});
if (!pregunta) {
throw new NotFoundException(`Pregunta con ID ${id} no encontrada`);
}
return pregunta;
}
remove(id: number) {
return `This action removes a #${id} pregunta`;
async findWithOptions(id: number) {
const pregunta = await this.preguntaRepository.findOne({
where: { id_pregunta: id },
relations: ['tipoPregunta']
});
if (!pregunta) {
throw new NotFoundException(`Pregunta con ID ${id} no encontrada`);
}
// Obtener las opciones para esta pregunta
const preguntaOpciones = await this.preguntaOpcionRepository.find({
where: { id_pregunta: id },
relations: ['opcion'],
order: { posicion: 'ASC' }
});
const opciones = preguntaOpciones.map(po => ({
id: po.opcion.id_opcion,
valor: po.opcion.opcion,
posicion: po.posicion
}));
return {
...pregunta,
tipo: pregunta.tipoPregunta?.tipo_pregunta,
opciones
};
}
async update(id: number, updatePreguntaDto: UpdatePreguntaDto) {
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
const pregunta = await this.preguntaRepository.findOne({
where: { id_pregunta: id }
});
if (!pregunta) {
throw new NotFoundException(`Pregunta con ID ${id} no encontrada`);
}
// Actualizar los campos básicos de la pregunta
if (updatePreguntaDto.titulo) pregunta.pregunta = updatePreguntaDto.titulo;
if (updatePreguntaDto.obligatoria !== undefined) pregunta.obligatoria = updatePreguntaDto.obligatoria;
if (updatePreguntaDto.id_opcion_dependiente !== undefined) pregunta.id_opcion_dependiente = updatePreguntaDto.id_opcion_dependiente;
// 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();
}
}
}
@@ -12,14 +12,15 @@ export class PreguntaOpcion {
@Column()
posicion: number;
@ManyToOne(() => Pregunta, pregunta => pregunta.id_pregunta)
//@Column()
id_pregunta: Pregunta;
@ManyToOne(() => Pregunta)
pregunta: Pregunta;
@Column()
id_pregunta: number;
@ManyToOne(() => Opcion, opcion => opcion.id_opcion)
//@Column()
id_opcion: Opcion;
@ManyToOne(() => Opcion, opcion => opcion.preguntasOpciones)
opcion: Opcion;
@OneToMany(()=> RespuestaParticipanteCerrada, respuestaParticipanteCerrada=>respuestaParticipanteCerrada.id_pregunta_opcion)
@OneToMany(()=> RespuestaParticipanteCerrada, respuestaParticipanteCerrada=>respuestaParticipanteCerrada.preguntaOpcion)
respuestaParticipanteCerrada:RespuestaParticipanteCerrada[];
}
@@ -1,9 +1,19 @@
import { Module } from '@nestjs/common';
import { PreguntaOpcionService } from './pregunta_opcion.service';
import { PreguntaOpcionController } from './pregunta_opcion.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { PreguntaOpcion } from './entities/pregunta_opcion.entity';
import { OpcionModule } from '../opcion/opcion.module';
import { PreguntaModule } from '../pregunta/pregunta.module';
@Module({
imports: [
TypeOrmModule.forFeature([PreguntaOpcion]),
OpcionModule,
PreguntaModule
],
controllers: [PreguntaOpcionController],
providers: [PreguntaOpcionService],
exports: [TypeOrmModule]
})
export class PreguntaOpcionModule {}
@@ -14,17 +14,14 @@ export class RespuestaParticipanteAbierta {
// Relación con CuestionarioRespondido
@ManyToOne(
() => CuestionarioRespondido,
cuestionarioRespondido => cuestionarioRespondido.idCuestionarioRespondido
cuestionarioRespondido => cuestionarioRespondido.respuestasAbiertas
)
@JoinColumn({ name: 'id_cuestionario_respondido' })
id_cuestionario_respondido: CuestionarioRespondido;
cuestionarioRespondido: CuestionarioRespondido;
@ManyToOne(
() => Pregunta,
pregunta => pregunta.id_pregunta
)
@ManyToOne(() => Pregunta)
@JoinColumn({ name: 'id_pregunta' })
id_pregunta: Pregunta;
pregunta: Pregunta;
}
@@ -1,9 +1,19 @@
import { Module } from '@nestjs/common';
import { RespuestaParticipanteAbiertaService } from './respuesta_participante_abierta.service';
import { RespuestaParticipanteAbiertaController } from './respuesta_participante_abierta.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { RespuestaParticipanteAbierta } from './entities/respuesta_participante_abierta.entity';
import { CuestionarioRespondidoModule } from '../cuestionario_respondido/cuestionario_respondido.module';
import { PreguntaModule } from '../pregunta/pregunta.module';
@Module({
imports: [
TypeOrmModule.forFeature([RespuestaParticipanteAbierta]),
CuestionarioRespondidoModule,
PreguntaModule
],
controllers: [RespuestaParticipanteAbiertaController],
providers: [RespuestaParticipanteAbiertaService],
exports: [TypeOrmModule]
})
export class RespuestaParticipanteAbiertaModule {}
@@ -10,11 +10,11 @@ export class RespuestaParticipanteCerrada {
@ManyToOne(() => CuestionarioRespondido, cuestionarioRespondido => cuestionarioRespondido.respuestasCerradas)
@JoinColumn({ name: 'id_cuestionario_respondido' })
id_cuestionario_respondido: CuestionarioRespondido;
cuestionarioRespondido: CuestionarioRespondido;
@ManyToOne(() => PreguntaOpcion, preguntaOpcion => preguntaOpcion.id_pregunta_opcion)
@ManyToOne(() => PreguntaOpcion, preguntaOpcion => preguntaOpcion.respuestaParticipanteCerrada)
@JoinColumn({ name: 'id_pregunta_opcion' })
id_pregunta_opcion: PreguntaOpcion;
preguntaOpcion: PreguntaOpcion;
}
@@ -1,9 +1,19 @@
import { Module } from '@nestjs/common';
import { RespuestaParticipanteCerradaService } from './respuesta_participante_cerrada.service';
import { RespuestaParticipanteCerradaController } from './respuesta_participante_cerrada.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { RespuestaParticipanteCerrada } from './entities/respuesta_participante_cerrada.entity';
import { PreguntaOpcionModule } from '../pregunta_opcion/pregunta_opcion.module';
import { CuestionarioRespondidoModule } from '../cuestionario_respondido/cuestionario_respondido.module';
@Module({
imports: [
TypeOrmModule.forFeature([RespuestaParticipanteCerrada]),
PreguntaOpcionModule,
CuestionarioRespondidoModule
],
controllers: [RespuestaParticipanteCerradaController],
providers: [RespuestaParticipanteCerradaService],
exports: [TypeOrmModule]
})
export class RespuestaParticipanteCerradaModule {}
+23 -1
View File
@@ -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[];
}
+3 -3
View File
@@ -11,7 +11,7 @@ export class Seccion {
contador_pregunta: number;
@Column({ type: 'text', nullable: true })
descripcion: string;
descripcion?: string;
@Column({ type: 'text', nullable: true })
titulo: string;
@@ -19,6 +19,6 @@ export class Seccion {
@OneToMany(() => CuestionarioSeccion,(cuestionario_seccion) => cuestionario_seccion.id_seccion )
seccion:CuestionarioSeccion[];
@OneToMany(()=> SeccionPregunta, (seccion_pregunta)=> seccion_pregunta.id_seccion)
seccion_pregunta:SeccionPregunta[];
@OneToMany(()=> SeccionPregunta, (seccion_pregunta)=> seccion_pregunta.seccion)
seccionPreguntas:SeccionPregunta[];
}
+7
View File
@@ -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);
}
+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' })
);
}
+18
View File
@@ -1,9 +1,27 @@
import { Module } from '@nestjs/common';
import { SeccionService } from './seccion.service';
import { SeccionController } from './seccion.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Seccion } from './entities/seccion.entity';
import { Pregunta } from '../pregunta/entities/pregunta.entity';
import { SeccionPregunta } from '../seccion_pregunta/entities/seccion_pregunta.entity';
import { TipoPregunta } from '../tipo_pregunta/entities/tipo_pregunta.entity';
import { Opcion } from '../opcion/entities/opcion.entity';
import { PreguntaOpcion } from '../pregunta_opcion/entities/pregunta_opcion.entity';
@Module({
imports: [
TypeOrmModule.forFeature([
Seccion,
Pregunta,
SeccionPregunta,
TipoPregunta,
Opcion,
PreguntaOpcion
])
],
controllers: [SeccionController],
providers: [SeccionService],
exports: [TypeOrmModule, SeccionService]
})
export class SeccionModule {}
+190 -11
View File
@@ -1,26 +1,205 @@
import { Injectable } from '@nestjs/common';
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, DataSource } from 'typeorm';
import { CreateSeccionDto } from './dto/create-seccion.dto';
import { UpdateSeccionDto } from './dto/update-seccion.dto';
import { Seccion } from './entities/seccion.entity';
import { Pregunta } from '../pregunta/entities/pregunta.entity';
import { SeccionPregunta } from '../seccion_pregunta/entities/seccion_pregunta.entity';
import { TipoPregunta } from '../tipo_pregunta/entities/tipo_pregunta.entity';
import { Opcion } from '../opcion/entities/opcion.entity';
import { PreguntaOpcion } from '../pregunta_opcion/entities/pregunta_opcion.entity';
@Injectable()
export class SeccionService {
create(createSeccionDto: CreateSeccionDto) {
return 'This action adds a new seccion';
constructor(
@InjectRepository(Seccion)
private seccionRepository: Repository<Seccion>,
@InjectRepository(Pregunta)
private preguntaRepository: Repository<Pregunta>,
@InjectRepository(SeccionPregunta)
private seccionPreguntaRepository: Repository<SeccionPregunta>,
@InjectRepository(TipoPregunta)
private tipoPreguntaRepository: Repository<TipoPregunta>,
@InjectRepository(Opcion)
private opcionRepository: Repository<Opcion>,
@InjectRepository(PreguntaOpcion)
private preguntaOpcionRepository: Repository<PreguntaOpcion>,
private dataSource: DataSource
) {}
async create(createSeccionDto: CreateSeccionDto) {
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
// 1. Crear sección
const seccion = new Seccion();
seccion.titulo = createSeccionDto.titulo;
seccion.descripcion = createSeccionDto.descripcion;
seccion.contador_pregunta = createSeccionDto.preguntas?.length || 0;
const savedSeccion = await queryRunner.manager.save(seccion);
// 2. Crear preguntas y vincularlas a la sección
if (createSeccionDto.preguntas && createSeccionDto.preguntas.length > 0) {
for (let i = 0; i < createSeccionDto.preguntas.length; i++) {
const preguntaDto = createSeccionDto.preguntas[i];
// Obtener el tipo de pregunta por su nombre
const tipoPregunta = await queryRunner.manager.findOne(TipoPregunta, {
where: { tipo_pregunta: preguntaDto.tipo },
});
if (!tipoPregunta) {
throw new Error(`Tipo de pregunta '${preguntaDto.tipo}' no encontrado`);
}
// Crear pregunta
const pregunta = new Pregunta();
pregunta.pregunta = preguntaDto.titulo;
pregunta.obligatoria = preguntaDto.obligatoria || false;
pregunta.id_tipo_pregunta = tipoPregunta.id_tipo;
pregunta.id_opcion_dependiente = preguntaDto.id_opcion_dependiente || undefined;
pregunta.contador_opcion = preguntaDto.opciones?.length || 0;
const savedPregunta = await queryRunner.manager.save(pregunta);
// Vincular pregunta con sección
const seccionPregunta = new SeccionPregunta();
seccionPregunta.id_seccion = savedSeccion.id_seccion;
seccionPregunta.id_pregunta = savedPregunta.id_pregunta;
seccionPregunta.posicion = i + 1;
await queryRunner.manager.save(seccionPregunta);
// Crear opciones para preguntas de tipo multiple/radio
if (preguntaDto.opciones && preguntaDto.opciones.length > 0) {
for (let j = 0; j < preguntaDto.opciones.length; j++) {
const opcionDto = preguntaDto.opciones[j];
// Crear opción
const opcion = new Opcion();
opcion.opcion = opcionDto.valor;
const savedOpcion = await queryRunner.manager.save(opcion);
// Vincular opción con pregunta
const preguntaOpcion = new PreguntaOpcion();
preguntaOpcion.id_pregunta = savedPregunta.id_pregunta;
preguntaOpcion.opcion = savedOpcion;
preguntaOpcion.posicion = j + 1;
await queryRunner.manager.save(preguntaOpcion);
}
}
}
}
await queryRunner.commitTransaction();
return {
success: true,
seccion: savedSeccion,
};
} catch (error) {
await queryRunner.rollbackTransaction();
throw error;
} finally {
await queryRunner.release();
}
}
findAll() {
return `This action returns all seccion`;
async findAll() {
return this.seccionRepository.find();
}
findOne(id: number) {
return `This action returns a #${id} seccion`;
async findOne(id: number) {
const seccion = await this.seccionRepository.findOne({
where: { id_seccion: id }
});
if (!seccion) {
throw new NotFoundException(`Sección con ID ${id} no encontrada`);
}
return seccion;
}
update(id: number, updateSeccionDto: UpdateSeccionDto) {
return `This action updates a #${id} seccion`;
async findWithPreguntas(id: number) {
const seccion = await this.seccionRepository.findOne({
where: { id_seccion: id }
});
if (!seccion) {
throw new NotFoundException(`Sección con ID ${id} no encontrada`);
}
// Obtener las relaciones seccion-pregunta para esta sección
const seccionPreguntas = await this.seccionPreguntaRepository.find({
where: { id_seccion: id },
relations: ['pregunta']
});
// Extraer y organizar las preguntas
const preguntas = await Promise.all(
seccionPreguntas.map(async (sp) => {
const pregunta = await this.preguntaRepository.findOne({
where: { id_pregunta: sp.id_pregunta }
});
// Obtener las opciones para preguntas de tipo multiple/radio
const preguntaOpciones = await this.preguntaOpcionRepository.find({
where: { id_pregunta: sp.id_pregunta },
relations: ['opcion'],
order: { posicion: 'ASC' }
});
const opciones = preguntaOpciones.map(po => po.opcion);
return {
...pregunta,
posicion: sp.posicion,
opciones
};
})
);
// Ordenar las preguntas por posición
preguntas.sort((a, b) => a.posicion - b.posicion);
return {
...seccion,
preguntas
};
}
remove(id: number) {
return `This action removes a #${id} seccion`;
async update(id: number, updateSeccionDto: UpdateSeccionDto) {
const seccion = await this.seccionRepository.findOne({
where: { id_seccion: id }
});
if (!seccion) {
throw new NotFoundException(`Sección con ID ${id} no encontrada`);
}
// Actualizar solo los campos proporcionados
if (updateSeccionDto.titulo) seccion.titulo = updateSeccionDto.titulo;
if (updateSeccionDto.descripcion !== undefined) seccion.descripcion = updateSeccionDto.descripcion;
return this.seccionRepository.save(seccion);
}
async remove(id: number) {
const seccion = await this.seccionRepository.findOne({
where: { id_seccion: id }
});
if (!seccion) {
throw new NotFoundException(`Sección con ID ${id} no encontrada`);
}
return this.seccionRepository.remove(seccion);
}
}
@@ -11,11 +11,15 @@ export class SeccionPregunta {
@Column({ type: 'tinyint' })
posicion: number;
@ManyToOne(() => Pregunta, (pregunta) => pregunta.id_pregunta)
@ManyToOne(() => Pregunta, (pregunta) => pregunta.seccionPreguntas)
pregunta: Pregunta;
@Column({ type: 'int' })
id_pregunta: number;
@ManyToOne(() => Seccion, (seccion) => seccion.id_seccion)
@ManyToOne(() => Seccion, (seccion) => seccion.seccionPreguntas)
seccion: Seccion;
@Column({ type: 'int' })
id_seccion: number;
}
@@ -1,9 +1,19 @@
import { Module } from '@nestjs/common';
import { SeccionPreguntaService } from './seccion_pregunta.service';
import { SeccionPreguntaController } from './seccion_pregunta.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { SeccionPregunta } from './entities/seccion_pregunta.entity';
import { PreguntaModule } from '../pregunta/pregunta.module';
import { SeccionModule } from '../seccion/seccion.module';
@Module({
imports: [
TypeOrmModule.forFeature([SeccionPregunta]),
PreguntaModule,
SeccionModule
],
controllers: [SeccionPreguntaController],
providers: [SeccionPreguntaService],
exports: [TypeOrmModule]
})
export class SeccionPreguntaModule {}
@@ -9,6 +9,6 @@ export class TipoPregunta {
@Column()
tipo_pregunta: string;
@OneToMany(() => Pregunta, pregunta => pregunta.id_tipo_pregunta)
@OneToMany(() => Pregunta, pregunta => pregunta.tipoPregunta)
preguntas: Pregunta[];
}
@@ -1,9 +1,13 @@
import { Module } from '@nestjs/common';
import { TipoPreguntaService } from './tipo_pregunta.service';
import { TipoPreguntaController } from './tipo_pregunta.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TipoPregunta } from './entities/tipo_pregunta.entity';
@Module({
imports: [TypeOrmModule.forFeature([TipoPregunta])],
controllers: [TipoPreguntaController],
providers: [TipoPreguntaService],
exports: [TypeOrmModule]
})
export class TipoPreguntaModule {}
+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'));