From a16e82005ee8f5f546ef8b4572f1f3e30e328a42 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 2 Apr 2025 12:43:37 -0600 Subject: [PATCH] formato correcto de cuestionario por verificar --- Dockerfile | 2 +- docker-compose.yml | 2 +- src/cuestionario/cuestionario.controller.ts | 7 + .../cuestionario.documentation.ts | 31 ++- src/cuestionario/cuestionario.module.ts | 4 +- src/cuestionario/cuestionario.service.ts | 156 ++++++++++- src/cuestionario/dto/formulario.schema.ts | 94 +++++++ src/docs/docs.module.ts | 4 +- src/main.ts | 6 +- src/opcion/opcion.controller.ts | 7 + src/opcion/opcion.documentation.ts | 153 +++++++++++ src/pregunta/pregunta.module.ts | 12 +- src/pregunta/pregunta.service.ts | 252 +++++++++++++++++- src/seccion/seccion.module.ts | 18 +- src/seccion/seccion.service.ts | 201 +++++++++++++- utils/get_formulario_feria.ts | 197 ++++++++++++++ utils/load-form.js | 2 +- utils/test-crear-formulario.ts | 4 +- 18 files changed, 1109 insertions(+), 43 deletions(-) create mode 100644 src/cuestionario/dto/formulario.schema.ts create mode 100644 src/opcion/opcion.documentation.ts create mode 100644 utils/get_formulario_feria.ts diff --git a/Dockerfile b/Dockerfile index 0b618ea..d122543 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,7 @@ COPY . . RUN npm run build # Puerto expuesto -EXPOSE 3000 +EXPOSE 4200 # Comando para iniciar en modo desarrollo CMD ["npm", "run", "start:dev"] diff --git a/docker-compose.yml b/docker-compose.yml index 8e9a574..9b4cf9a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -24,7 +24,7 @@ services: - .:/app - /app/node_modules ports: - - "4200:3000" + - "4200:4200" depends_on: - mariadb restart: unless-stopped diff --git a/src/cuestionario/cuestionario.controller.ts b/src/cuestionario/cuestionario.controller.ts index ff9a3b2..f62f04f 100644 --- a/src/cuestionario/cuestionario.controller.ts +++ b/src/cuestionario/cuestionario.controller.ts @@ -3,6 +3,7 @@ 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 @@ -26,6 +27,12 @@ export class CuestionarioController { 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 diff --git a/src/cuestionario/cuestionario.documentation.ts b/src/cuestionario/cuestionario.documentation.ts index 3b8cc7d..c170471 100644 --- a/src/cuestionario/cuestionario.documentation.ts +++ b/src/cuestionario/cuestionario.documentation.ts @@ -1,10 +1,38 @@ -import { ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger'; +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( @@ -14,6 +42,7 @@ export class CuestionarioApiDocumentation { }), ApiBody({ description: 'Datos del cuestionario a crear', + type: FormularioDto, examples: { feriaSexualidad: { summary: 'Ejemplo de cuestionario para feria de sexualidad', diff --git a/src/cuestionario/cuestionario.module.ts b/src/cuestionario/cuestionario.module.ts index 642215f..400dd2a 100644 --- a/src/cuestionario/cuestionario.module.ts +++ b/src/cuestionario/cuestionario.module.ts @@ -10,6 +10,7 @@ import { SeccionPregunta } from '../seccion_pregunta/entities/seccion_pregunta.e 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: [ @@ -21,7 +22,8 @@ import { TipoPregunta } from '../tipo_pregunta/entities/tipo_pregunta.entity'; SeccionPregunta, Opcion, PreguntaOpcion, - TipoPregunta + TipoPregunta, + TipoCuestionario ]) ], controllers: [CuestionarioController], diff --git a/src/cuestionario/cuestionario.service.ts b/src/cuestionario/cuestionario.service.ts index 784cd9d..f73e8dd 100644 --- a/src/cuestionario/cuestionario.service.ts +++ b/src/cuestionario/cuestionario.service.ts @@ -1,6 +1,6 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository, DataSource } from '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'; @@ -11,6 +11,7 @@ import { SeccionPregunta } from '../seccion_pregunta/entities/seccion_pregunta.e 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 { @@ -149,8 +150,155 @@ export class CuestionarioService { return this.cuestionarioRepository.find(); } - findOne(id: number) { - return this.cuestionarioRepository.findOne({ where: { id_cuestionario: id } }); + 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) { diff --git a/src/cuestionario/dto/formulario.schema.ts b/src/cuestionario/dto/formulario.schema.ts new file mode 100644 index 0000000..d30e259 --- /dev/null +++ b/src/cuestionario/dto/formulario.schema.ts @@ -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[]; +} \ No newline at end of file diff --git a/src/docs/docs.module.ts b/src/docs/docs.module.ts index 90d1820..21a9935 100644 --- a/src/docs/docs.module.ts +++ b/src/docs/docs.module.ts @@ -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); } } diff --git a/src/main.ts b/src/main.ts index 3f43b41..bdbcf72 100644 --- a/src/main.ts +++ b/src/main.ts @@ -51,8 +51,8 @@ async function bootstrap() { } // 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'); + await app.listen(process.env.PORT ?? 4200); + console.log('API en ejecución en http://localhost:4200'); + console.log('Swagger disponible en http://localhost:4200/api-docs'); } bootstrap(); diff --git a/src/opcion/opcion.controller.ts b/src/opcion/opcion.controller.ts index 635597a..c64e3dc 100644 --- a/src/opcion/opcion.controller.ts +++ b/src/opcion/opcion.controller.ts @@ -2,32 +2,39 @@ import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/commo import { OpcionService } from './opcion.service'; import { CreateOpcionDto } from './dto/create-opcion.dto'; import { UpdateOpcionDto } from './dto/update-opcion.dto'; +import { OpcionApiDocumentation } from './opcion.documentation'; @Controller('opcion') +@OpcionApiDocumentation.ApiController export class OpcionController { constructor(private readonly opcionService: OpcionService) {} @Post() + @OpcionApiDocumentation.ApiCreate create(@Body() createOpcionDto: CreateOpcionDto) { return this.opcionService.create(createOpcionDto); } @Get() + @OpcionApiDocumentation.ApiGetAll findAll() { return this.opcionService.findAll(); } @Get(':id') + @OpcionApiDocumentation.ApiGetOne findOne(@Param('id') id: string) { return this.opcionService.findOne(+id); } @Patch(':id') + @OpcionApiDocumentation.ApiUpdate update(@Param('id') id: string, @Body() updateOpcionDto: UpdateOpcionDto) { return this.opcionService.update(+id, updateOpcionDto); } @Delete(':id') + @OpcionApiDocumentation.ApiRemove remove(@Param('id') id: string) { return this.opcionService.remove(+id); } diff --git a/src/opcion/opcion.documentation.ts b/src/opcion/opcion.documentation.ts new file mode 100644 index 0000000..701d375 --- /dev/null +++ b/src/opcion/opcion.documentation.ts @@ -0,0 +1,153 @@ +import { ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { applyDecorators } from '@nestjs/common'; + +export class OpcionApiDocumentation { + // Decoradores para toda la clase del controlador + static ApiController = ApiTags('Opciones'); + + // Documentación para crear una opción + static ApiCreate = applyDecorators( + ApiOperation({ + summary: 'Crear una nueva opción', + description: 'Crea una nueva opción para una pregunta' + }), + ApiBody({ + description: 'Datos de la opción a crear', + schema: { + type: 'object', + required: ['opcion'], + properties: { + opcion: { type: 'string', example: 'Si' } + } + } + }), + ApiResponse({ + status: 201, + description: 'Opción creada correctamente', + schema: { + type: 'object', + properties: { + id_opcion: { type: 'number', example: 1 }, + opcion: { type: 'string', example: 'Si' } + } + } + }), + ApiResponse({ status: 400, description: 'Datos de la opción inválidos' }), + ApiResponse({ status: 500, description: 'Error interno del servidor' }) + ); + + // Documentación para obtener todas las opciones + static ApiGetAll = applyDecorators( + ApiOperation({ + summary: 'Obtener todas las opciones', + description: 'Retorna una lista de todas las opciones registradas' + }), + ApiResponse({ + status: 200, + description: 'Lista de opciones obtenida correctamente', + schema: { + type: 'array', + items: { + type: 'object', + properties: { + id_opcion: { type: 'number', example: 1 }, + opcion: { type: 'string', example: 'Si' } + } + } + } + }), + ApiResponse({ status: 500, description: 'Error interno del servidor' }) + ); + + // Documentación para obtener una opción por ID + static ApiGetOne = applyDecorators( + ApiOperation({ + summary: 'Obtener una opción por ID', + description: 'Retorna una opción específica por su ID' + }), + ApiParam({ + name: 'id', + description: 'ID de la opción', + required: true, + type: 'number', + example: 1 + }), + ApiResponse({ + status: 200, + description: 'Opción obtenida correctamente', + schema: { + type: 'object', + properties: { + id_opcion: { type: 'number', example: 1 }, + opcion: { type: 'string', example: 'Si' } + } + } + }), + ApiResponse({ status: 404, description: 'Opción no encontrada' }), + ApiResponse({ status: 500, description: 'Error interno del servidor' }) + ); + + // Documentación para actualizar una opción + static ApiUpdate = applyDecorators( + ApiOperation({ + summary: 'Actualizar una opción', + description: 'Actualiza los datos de una opción existente' + }), + ApiParam({ + name: 'id', + description: 'ID de la opción a actualizar', + required: true, + type: 'number', + example: 1 + }), + ApiBody({ + description: 'Datos a actualizar de la opción', + schema: { + type: 'object', + properties: { + opcion: { type: 'string', example: 'Opción actualizada' } + } + } + }), + ApiResponse({ + status: 200, + description: 'Opción actualizada correctamente', + schema: { + type: 'object', + properties: { + affected: { type: 'number', example: 1 } + } + } + }), + ApiResponse({ status: 400, description: 'Datos de actualización inválidos' }), + ApiResponse({ status: 404, description: 'Opción no encontrada' }), + ApiResponse({ status: 500, description: 'Error interno del servidor' }) + ); + + // Documentación para eliminar una opción + static ApiRemove = applyDecorators( + ApiOperation({ + summary: 'Eliminar una opción', + description: 'Elimina permanentemente una opción por su ID' + }), + ApiParam({ + name: 'id', + description: 'ID de la opción a eliminar', + required: true, + type: 'number', + example: 1 + }), + ApiResponse({ + status: 200, + description: 'Opción eliminada correctamente', + schema: { + type: 'object', + properties: { + affected: { type: 'number', example: 1 } + } + } + }), + ApiResponse({ status: 404, description: 'Opción no encontrada' }), + ApiResponse({ status: 500, description: 'Error interno del servidor' }) + ); +} \ No newline at end of file diff --git a/src/pregunta/pregunta.module.ts b/src/pregunta/pregunta.module.ts index d829b94..158e0a3 100644 --- a/src/pregunta/pregunta.module.ts +++ b/src/pregunta/pregunta.module.ts @@ -4,14 +4,22 @@ 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]), + TypeOrmModule.forFeature([ + Pregunta, + TipoPregunta, + Opcion, + PreguntaOpcion + ]), TipoPreguntaModule ], controllers: [PreguntaController], providers: [PreguntaService], - exports: [TypeOrmModule] + exports: [TypeOrmModule, PreguntaService] }) export class PreguntaModule {} diff --git a/src/pregunta/pregunta.service.ts b/src/pregunta/pregunta.service.ts index b196cc8..b190384 100644 --- a/src/pregunta/pregunta.service.ts +++ b/src/pregunta/pregunta.service.ts @@ -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, + @InjectRepository(TipoPregunta) + private tipoPreguntaRepository: Repository, + @InjectRepository(Opcion) + private opcionRepository: Repository, + @InjectRepository(PreguntaOpcion) + private preguntaOpcionRepository: Repository, + private dataSource: DataSource + ) {} + + async create(createPreguntaDto: CreatePreguntaDto) { + const queryRunner = this.dataSource.createQueryRunner(); + await queryRunner.connect(); + await queryRunner.startTransaction(); + + try { + // Obtener el tipo de pregunta por su nombre + const tipoPregunta = await this.tipoPreguntaRepository.findOne({ + where: { tipo_pregunta: createPreguntaDto.tipo } + }); + + if (!tipoPregunta) { + throw new Error(`Tipo de pregunta '${createPreguntaDto.tipo}' no encontrado`); + } + + // Crear pregunta + const pregunta = new Pregunta(); + pregunta.pregunta = createPreguntaDto.titulo; + pregunta.obligatoria = createPreguntaDto.obligatoria || false; + pregunta.id_tipo_pregunta = tipoPregunta.id_tipo; + pregunta.id_opcion_dependiente = createPreguntaDto.id_opcion_dependiente || undefined; + pregunta.contador_opcion = createPreguntaDto.opciones?.length || 0; + + 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(); + } } } diff --git a/src/seccion/seccion.module.ts b/src/seccion/seccion.module.ts index d0d484c..8baf400 100644 --- a/src/seccion/seccion.module.ts +++ b/src/seccion/seccion.module.ts @@ -3,11 +3,25 @@ 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])], + imports: [ + TypeOrmModule.forFeature([ + Seccion, + Pregunta, + SeccionPregunta, + TipoPregunta, + Opcion, + PreguntaOpcion + ]) + ], controllers: [SeccionController], providers: [SeccionService], - exports: [TypeOrmModule] + exports: [TypeOrmModule, SeccionService] }) export class SeccionModule {} diff --git a/src/seccion/seccion.service.ts b/src/seccion/seccion.service.ts index 789091b..7915dd1 100644 --- a/src/seccion/seccion.service.ts +++ b/src/seccion/seccion.service.ts @@ -1,26 +1,205 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository, DataSource } from 'typeorm'; import { CreateSeccionDto } from './dto/create-seccion.dto'; import { UpdateSeccionDto } from './dto/update-seccion.dto'; +import { Seccion } from './entities/seccion.entity'; +import { Pregunta } from '../pregunta/entities/pregunta.entity'; +import { SeccionPregunta } from '../seccion_pregunta/entities/seccion_pregunta.entity'; +import { TipoPregunta } from '../tipo_pregunta/entities/tipo_pregunta.entity'; +import { Opcion } from '../opcion/entities/opcion.entity'; +import { PreguntaOpcion } from '../pregunta_opcion/entities/pregunta_opcion.entity'; @Injectable() export class SeccionService { - create(createSeccionDto: CreateSeccionDto) { - return 'This action adds a new seccion'; + constructor( + @InjectRepository(Seccion) + private seccionRepository: Repository, + @InjectRepository(Pregunta) + private preguntaRepository: Repository, + @InjectRepository(SeccionPregunta) + private seccionPreguntaRepository: Repository, + @InjectRepository(TipoPregunta) + private tipoPreguntaRepository: Repository, + @InjectRepository(Opcion) + private opcionRepository: Repository, + @InjectRepository(PreguntaOpcion) + private preguntaOpcionRepository: Repository, + private dataSource: DataSource + ) {} + + async create(createSeccionDto: CreateSeccionDto) { + const queryRunner = this.dataSource.createQueryRunner(); + await queryRunner.connect(); + await queryRunner.startTransaction(); + + try { + // 1. Crear sección + const seccion = new Seccion(); + seccion.titulo = createSeccionDto.titulo; + seccion.descripcion = createSeccionDto.descripcion; + seccion.contador_pregunta = createSeccionDto.preguntas?.length || 0; + + const savedSeccion = await queryRunner.manager.save(seccion); + + // 2. Crear preguntas y vincularlas a la sección + if (createSeccionDto.preguntas && createSeccionDto.preguntas.length > 0) { + for (let i = 0; i < createSeccionDto.preguntas.length; i++) { + const preguntaDto = createSeccionDto.preguntas[i]; + + // Obtener el tipo de pregunta por su nombre + const tipoPregunta = await queryRunner.manager.findOne(TipoPregunta, { + where: { tipo_pregunta: preguntaDto.tipo }, + }); + + if (!tipoPregunta) { + throw new Error(`Tipo de pregunta '${preguntaDto.tipo}' no encontrado`); + } + + // Crear pregunta + const pregunta = new Pregunta(); + pregunta.pregunta = preguntaDto.titulo; + pregunta.obligatoria = preguntaDto.obligatoria || false; + pregunta.id_tipo_pregunta = tipoPregunta.id_tipo; + pregunta.id_opcion_dependiente = preguntaDto.id_opcion_dependiente || undefined; + pregunta.contador_opcion = preguntaDto.opciones?.length || 0; + + const savedPregunta = await queryRunner.manager.save(pregunta); + + // Vincular pregunta con sección + const seccionPregunta = new SeccionPregunta(); + seccionPregunta.id_seccion = savedSeccion.id_seccion; + seccionPregunta.id_pregunta = savedPregunta.id_pregunta; + seccionPregunta.posicion = i + 1; + + await queryRunner.manager.save(seccionPregunta); + + // Crear opciones para preguntas de tipo multiple/radio + if (preguntaDto.opciones && preguntaDto.opciones.length > 0) { + for (let j = 0; j < preguntaDto.opciones.length; j++) { + const opcionDto = preguntaDto.opciones[j]; + + // Crear opción + const opcion = new Opcion(); + opcion.opcion = opcionDto.valor; + + const savedOpcion = await queryRunner.manager.save(opcion); + + // Vincular opción con pregunta + const preguntaOpcion = new PreguntaOpcion(); + preguntaOpcion.id_pregunta = savedPregunta.id_pregunta; + preguntaOpcion.opcion = savedOpcion; + preguntaOpcion.posicion = j + 1; + + await queryRunner.manager.save(preguntaOpcion); + } + } + } + } + + await queryRunner.commitTransaction(); + + return { + success: true, + seccion: savedSeccion, + }; + } catch (error) { + await queryRunner.rollbackTransaction(); + throw error; + } finally { + await queryRunner.release(); + } } - findAll() { - return `This action returns all seccion`; + async findAll() { + return this.seccionRepository.find(); } - findOne(id: number) { - return `This action returns a #${id} seccion`; + async findOne(id: number) { + const seccion = await this.seccionRepository.findOne({ + where: { id_seccion: id } + }); + + if (!seccion) { + throw new NotFoundException(`Sección con ID ${id} no encontrada`); + } + + return seccion; } - update(id: number, updateSeccionDto: UpdateSeccionDto) { - return `This action updates a #${id} seccion`; + async findWithPreguntas(id: number) { + const seccion = await this.seccionRepository.findOne({ + where: { id_seccion: id } + }); + + if (!seccion) { + throw new NotFoundException(`Sección con ID ${id} no encontrada`); + } + + // Obtener las relaciones seccion-pregunta para esta sección + const seccionPreguntas = await this.seccionPreguntaRepository.find({ + where: { id_seccion: id }, + relations: ['pregunta'] + }); + + // Extraer y organizar las preguntas + const preguntas = await Promise.all( + seccionPreguntas.map(async (sp) => { + const pregunta = await this.preguntaRepository.findOne({ + where: { id_pregunta: sp.id_pregunta } + }); + + // Obtener las opciones para preguntas de tipo multiple/radio + const preguntaOpciones = await this.preguntaOpcionRepository.find({ + where: { id_pregunta: sp.id_pregunta }, + relations: ['opcion'], + order: { posicion: 'ASC' } + }); + + const opciones = preguntaOpciones.map(po => po.opcion); + + return { + ...pregunta, + posicion: sp.posicion, + opciones + }; + }) + ); + + // Ordenar las preguntas por posición + preguntas.sort((a, b) => a.posicion - b.posicion); + + return { + ...seccion, + preguntas + }; } - remove(id: number) { - return `This action removes a #${id} seccion`; + async update(id: number, updateSeccionDto: UpdateSeccionDto) { + const seccion = await this.seccionRepository.findOne({ + where: { id_seccion: id } + }); + + if (!seccion) { + throw new NotFoundException(`Sección con ID ${id} no encontrada`); + } + + // Actualizar solo los campos proporcionados + if (updateSeccionDto.titulo) seccion.titulo = updateSeccionDto.titulo; + if (updateSeccionDto.descripcion !== undefined) seccion.descripcion = updateSeccionDto.descripcion; + + return this.seccionRepository.save(seccion); + } + + async remove(id: number) { + const seccion = await this.seccionRepository.findOne({ + where: { id_seccion: id } + }); + + if (!seccion) { + throw new NotFoundException(`Sección con ID ${id} no encontrada`); + } + + return this.seccionRepository.remove(seccion); } } diff --git a/utils/get_formulario_feria.ts b/utils/get_formulario_feria.ts new file mode 100644 index 0000000..dfffb3c --- /dev/null +++ b/utils/get_formulario_feria.ts @@ -0,0 +1,197 @@ +export const cuestionario_feria = { + tipo_cuestionario: { + id_tipo_cuestionario: 1, + tipo_cuestionario: 'Encuesta', + }, + cuestionario: { + id_cuestionario: 10, + nombre_form: + 'Registro para la Feria de la Sexualidad - Comunidad FES Acatlán', + contador_secciones: 2, + descripcion: + 'La Feria de la Sexualidad es un espacio seguro e informativo dirigido a toda la comunidad universitaria. Este registro nos ayudará a organizar mejor las actividades, talleres y charlas, así como conocer tus intereses. Por favor, completa el siguiente formulario para confirmar tu participación.', + editable: true, + fecha_inicio: '2025-03-26T00:00:00', + fecha_fin: '2025-03-31T23:59:59', + id_cuestionario_original: null, + id_tipo_cuestionario: 1, + secciones: [ + { + id_cuestionario_seccion: 1, + posicion: 1, + seccion: { + id_seccion: 100, + contador_pregunta: 2, + descripcion: 'Preguntas generales', + titulo: 'General', + }, + preguntas: [ + { + id_seccion_pregunta: 1000, + posicion: 1, + pregunta: { + id_pregunta: 10000, + pregunta: '¿Cómo calificaría nuestro servicio?', + contador_opcion: 4, + obligatoria: true, + id_tipo_pregunta: 1, + id_opcion_dependiente: null, + tipo_pregunta: { + id_tipo: 1, + tipo_pregunta: 'Cerrada', + }, + opciones: [ + { + id_pregunta_opcion: 50000, + posicion: 1, + id_opcion: 90000, + opcion: { + id_opcion: 90000, + opcion: 'Excelente', + }, + }, + { + id_pregunta_opcion: 50001, + posicion: 2, + id_opcion: 90001, + opcion: { + id_opcion: 90001, + opcion: 'Bueno', + }, + }, + { + id_pregunta_opcion: 50002, + posicion: 3, + id_opcion: 90002, + opcion: { + id_opcion: 90002, + opcion: 'Regular', + }, + }, + { + id_pregunta_opcion: 50003, + posicion: 4, + id_opcion: 90003, + opcion: { + id_opcion: 90003, + opcion: 'Malo', + }, + }, + ], + }, + }, + { + id_seccion_pregunta: 1001, + posicion: 2, + pregunta: { + id_pregunta: 10001, + pregunta: '¿Recomendaría nuestro servicio?', + contador_opcion: 2, + obligatoria: true, + id_tipo_pregunta: 1, + id_opcion_dependiente: null, + tipo_pregunta: { + id_tipo: 1, + tipo_pregunta: 'Cerrada', + }, + opciones: [ + { + id_pregunta_opcion: 50004, + posicion: 1, + id_opcion: 90004, + opcion: { + id_opcion: 90004, + opcion: 'Sí', + }, + }, + { + id_pregunta_opcion: 50005, + posicion: 2, + id_opcion: 90005, + opcion: { + id_opcion: 90005, + opcion: 'No', + }, + }, + ], + }, + }, + ], + }, + { + id_cuestionario_seccion: 2, + posicion: 2, + seccion: { + id_seccion: 101, + contador_pregunta: 1, + descripcion: 'Preguntas adicionales para conocer más detalles', + titulo: 'Adicionales', + }, + preguntas: [ + { + id_seccion_pregunta: 1002, + posicion: 1, + pregunta: { + id_pregunta: 10002, + pregunta: '¿Qué mejorarías en nuestro servicio?', + contador_opcion: 0, + obligatoria: false, + id_tipo_pregunta: 2, + id_opcion_dependiente: null, + tipo_pregunta: { + id_tipo: 2, + tipo_pregunta: 'Abierta', + }, + opciones: [], + }, + }, + { + id_seccion_pregunta: 1003, + posicion: 3, + pregunta: { + id_pregunta: 10003, + pregunta: '¿Qué aspectos del servicio fueron de tu agrado?', + contador_opcion: 3, + obligatoria: false, + id_tipo_pregunta: 3, // Puedes usar este ID para distinguir múltiples + id_opcion_dependiente: null, + tipo_pregunta: { + id_tipo: 3, + tipo_pregunta: 'Multiple', + }, + opciones: [ + { + id_pregunta_opcion: 50006, + posicion: 1, + id_opcion: 90006, + opcion: { + id_opcion: 90006, + opcion: 'Rapidez', + }, + }, + { + id_pregunta_opcion: 50007, + posicion: 2, + id_opcion: 90007, + opcion: { + id_opcion: 90007, + opcion: 'Atención al cliente', + }, + }, + { + id_pregunta_opcion: 50008, + posicion: 3, + id_opcion: 90008, + opcion: { + id_opcion: 90008, + opcion: 'Facilidad de uso', + }, + }, + ], + }, + }, + ], + }, + ], + }, + }; \ No newline at end of file diff --git a/utils/load-form.js b/utils/load-form.js index 800e54b..9489b53 100644 --- a/utils/load-form.js +++ b/utils/load-form.js @@ -4,7 +4,7 @@ const axios = require('axios'); const fs = require('fs'); const path = require('path'); -const API_URL = 'http://localhost:3000'; +const API_URL = 'http://localhost:4200'; async function loadFormFromFile(filePath) { try { diff --git a/utils/test-crear-formulario.ts b/utils/test-crear-formulario.ts index 7e88c02..62b9ada 100644 --- a/utils/test-crear-formulario.ts +++ b/utils/test-crear-formulario.ts @@ -1,8 +1,8 @@ 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'; +// Asumiendo que el servidor está corriendo en localhost:4200 +const API_URL = 'http://localhost:4200'; async function crearFormulario() { try {