From c4d4fed43d0e72049e27f62a46e2abc5edda445b Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 2 Apr 2025 13:51:04 -0600 Subject: [PATCH] cuestionario contestado, respuestas guardadas --- .../cuestionario.documentation.ts | 2 +- src/cuestionario/cuestionario.service.ts | 36 ++- src/cuestionario/dto/formulario.schema.ts | 272 ++++++++++++------ .../cuestionario_respondido.controller.ts | 20 ++ .../cuestionario_respondido.documentation.ts | 79 +++++ .../cuestionario_respondido.module.ts | 18 +- .../cuestionario_respondido.service.ts | 172 ++++++++++- .../dto/submit-respuestas.dto.ts | 63 ++++ src/pregunta/dto/create-pregunta.dto.ts | 64 ++++- src/pregunta/pregunta.documentation.ts | 43 ++- src/tipo_pregunta/tipo_pregunta.module.ts | 2 +- src/tipo_pregunta/tipo_pregunta.service.ts | 59 +++- utils/crear_formulario_feria.ts | 14 +- 13 files changed, 725 insertions(+), 119 deletions(-) create mode 100644 src/cuestionario_respondido/cuestionario_respondido.documentation.ts create mode 100644 src/cuestionario_respondido/dto/submit-respuestas.dto.ts diff --git a/src/cuestionario/cuestionario.documentation.ts b/src/cuestionario/cuestionario.documentation.ts index c170471..9e951fa 100644 --- a/src/cuestionario/cuestionario.documentation.ts +++ b/src/cuestionario/cuestionario.documentation.ts @@ -1,7 +1,7 @@ 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'; +import { FormularioDto, FormularioFeriaSchema } from './dto/formulario.schema'; export class CuestionarioApiDocumentation { // Decoradores para toda la clase del controlador diff --git a/src/cuestionario/cuestionario.service.ts b/src/cuestionario/cuestionario.service.ts index f73e8dd..0f4454f 100644 --- a/src/cuestionario/cuestionario.service.ts +++ b/src/cuestionario/cuestionario.service.ts @@ -79,15 +79,22 @@ export class CuestionarioService { 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 }, - }); + // Mapear directamente el tipo de pregunta a su ID correspondiente + let idTipoPregunta = 7; // Valor por defecto (desconocido) - if (!tipoPregunta) { - throw new Error(`Tipo de pregunta '${preguntaDto.tipo}' no encontrado`); + // Mapeamos el tipo de pregunta a su ID correspondiente + if (preguntaDto.tipo === 'Cerrada') { + idTipoPregunta = 1; + } else if (preguntaDto.tipo === 'Abierta') { + idTipoPregunta = 2; + } else if (preguntaDto.tipo === 'Multiple') { + idTipoPregunta = 3; + } else { + console.warn(`Tipo de pregunta no reconocido: ${preguntaDto.tipo}. Se asignará como desconocido.`); } + const tipoPregunta = { id_tipo: idTipoPregunta, tipo_pregunta: preguntaDto.tipo }; + // Crear pregunta const pregunta = new Pregunta(); pregunta.pregunta = preguntaDto.titulo; @@ -179,6 +186,13 @@ export class CuestionarioService { tipo_cuestionario: 'Encuesta' }; + // Mapeador de ID de tipo de pregunta a nombres conocidos + const tiposPreguntaMap = { + 1: { id_tipo: 1, tipo_pregunta: 'Cerrada' }, + 2: { id_tipo: 2, tipo_pregunta: 'Abierta' }, + 3: { id_tipo: 3, tipo_pregunta: 'Multiple' } + }; + // Obtener las relaciones cuestionario-seccion const cuestionarioSecciones = await this.cuestionarioSeccionRepository.find({ where: { id_cuestionario: id }, @@ -216,14 +230,8 @@ export class CuestionarioService { 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] || + // Usar el mapeo de tipos de pregunta definido arriba + const tipoPregunta = tiposPreguntaMap[pregunta.id_tipo_pregunta] || { id_tipo: pregunta.id_tipo_pregunta, tipo_pregunta: 'Desconocido' }; // Obtener opciones para preguntas de tipo multiple/radio diff --git a/src/cuestionario/dto/formulario.schema.ts b/src/cuestionario/dto/formulario.schema.ts index d30e259..2191d01 100644 --- a/src/cuestionario/dto/formulario.schema.ts +++ b/src/cuestionario/dto/formulario.schema.ts @@ -1,94 +1,198 @@ import { ApiProperty } from '@nestjs/swagger'; +import { TipoPreguntaEnum } from '../../pregunta/dto/create-pregunta.dto'; -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[]; -} +// Ejemplo de respuesta de cuestionario completo +export const FormularioFeriaSchema = { + tipo_cuestionario: { + id_tipo_cuestionario: 1, + tipo_cuestionario: 'Encuesta' + }, + cuestionario: { + id_cuestionario: 10, + nombre_form: 'Registro para la Feria de la Sexualidad - Comunidad FES Acatlán', + contador_secciones: 2, + descripcion: 'La Feria de la Sexualidad es un espacio seguro e informativo dirigido a toda la comunidad universitaria. Este registro nos ayudará a organizar mejor las actividades, talleres y charlas, así como conocer tus intereses. Por favor, completa el siguiente formulario para confirmar tu participación.', + editable: true, + fecha_inicio: '2025-03-26T00:00:00', + fecha_fin: '2025-03-31T23:59:59', + id_cuestionario_original: null, + id_tipo_cuestionario: 1, + secciones: [ + { + id_cuestionario_seccion: 1, + posicion: 1, + seccion: { + id_seccion: 100, + contador_pregunta: 2, + descripcion: 'Información básica', + titulo: 'Datos personales' + }, + preguntas: [ + { + id_seccion_pregunta: 1000, + posicion: 1, + pregunta: { + id_pregunta: 101, + pregunta: '¿Eres parte de la comunidad de la FES Acatlán?', + contador_opcion: 2, + obligatoria: true, + id_tipo_pregunta: 1, + id_opcion_dependiente: null, + tipo_pregunta: { + id_tipo: 1, + tipo_pregunta: 'Cerrada' + }, + opciones: [ + { + id_pregunta_opcion: 50001, + posicion: 1, + id_opcion: 90001, + opcion: { + id_opcion: 90001, + opcion: 'Sí' + } + }, + { + id_pregunta_opcion: 50002, + posicion: 2, + id_opcion: 90002, + opcion: { + id_opcion: 90002, + opcion: 'No' + } + } + ] + } + }, + { + id_seccion_pregunta: 1001, + posicion: 2, + pregunta: { + id_pregunta: 102, + pregunta: 'Número de cuenta (solo para estudiantes de la FES Acatlán)', + contador_opcion: 0, + obligatoria: false, + id_tipo_pregunta: 2, + id_opcion_dependiente: 90001, // Dependiente de la respuesta "Sí" en la pregunta anterior + tipo_pregunta: { + id_tipo: 2, + tipo_pregunta: 'Abierta' + }, + opciones: [] + } + } + ] + }, + { + id_cuestionario_seccion: 2, + posicion: 2, + seccion: { + id_seccion: 101, + contador_pregunta: 2, + descripcion: 'Información sobre tus intereses', + titulo: 'Intereses' + }, + preguntas: [ + { + id_seccion_pregunta: 1003, + posicion: 1, + pregunta: { + id_pregunta: 106, + pregunta: '¿Qué temas te interesan para la Feria de la Sexualidad?', + contador_opcion: 5, + obligatoria: true, + id_tipo_pregunta: 3, + id_opcion_dependiente: null, + tipo_pregunta: { + id_tipo: 3, + tipo_pregunta: 'Multiple' + }, + opciones: [ + { + id_pregunta_opcion: 50010, + posicion: 1, + id_opcion: 90010, + opcion: { + id_opcion: 90010, + opcion: 'Educación sexual' + } + }, + { + id_pregunta_opcion: 50011, + posicion: 2, + id_opcion: 90011, + opcion: { + id_opcion: 90011, + opcion: 'Prevención de ITS' + } + }, + { + id_pregunta_opcion: 50012, + posicion: 3, + id_opcion: 90012, + opcion: { + id_opcion: 90012, + opcion: 'Salud reproductiva' + } + }, + { + id_pregunta_opcion: 50013, + posicion: 4, + id_opcion: 90013, + opcion: { + id_opcion: 90013, + opcion: 'Diversidad sexual y de género' + } + }, + { + id_pregunta_opcion: 50014, + posicion: 5, + id_opcion: 90014, + opcion: { + id_opcion: 90014, + opcion: 'Relaciones saludables' + } + } + ] + } + }, + { + id_seccion_pregunta: 1004, + posicion: 2, + pregunta: { + id_pregunta: 107, + pregunta: '¿Algún otro tema que te gustaría que se abordara?', + contador_opcion: 0, + obligatoria: false, + id_tipo_pregunta: 2, + id_opcion_dependiente: null, + tipo_pregunta: { + id_tipo: 2, + tipo_pregunta: 'Abierta' + }, + opciones: [] + } + } + ] + } + ] + } +}; +// Ejemplo de tipo FormularioDto para usar en la API export class FormularioDto { @ApiProperty({ - description: 'Nombre del formulario', - example: 'Registro para la Feria de la Sexualidad - FES Acatlán' + description: 'Tipo de cuestionario', + example: { + id_tipo_cuestionario: 1, + tipo_cuestionario: 'Encuesta' + } }) - nombre_form: string; + tipo_cuestionario: any; @ApiProperty({ - description: 'Descripción del formulario', - example: 'La Feria de la Sexualidad es un espacio seguro e informativo...' + description: 'Información completa del cuestionario', + example: FormularioFeriaSchema.cuestionario }) - 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[]; + cuestionario: any; } \ No newline at end of file diff --git a/src/cuestionario_respondido/cuestionario_respondido.controller.ts b/src/cuestionario_respondido/cuestionario_respondido.controller.ts index 654f88a..647c6e1 100644 --- a/src/cuestionario_respondido/cuestionario_respondido.controller.ts +++ b/src/cuestionario_respondido/cuestionario_respondido.controller.ts @@ -2,7 +2,11 @@ import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/commo import { CuestionarioRespondidoService } from './cuestionario_respondido.service'; import { CreateCuestionarioRespondidoDto } from './dto/create-cuestionario_respondido.dto'; import { UpdateCuestionarioRespondidoDto } from './dto/update-cuestionario_respondido.dto'; +import { SubmitRespuestasDto } from './dto/submit-respuestas.dto'; +import { ApiBody, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { CuestionarioRespondidoApiDocumentation } from './cuestionario_respondido.documentation'; +@ApiTags('Cuestionario Respondido') @Controller('cuestionario-respondido') export class CuestionarioRespondidoController { constructor(private readonly cuestionarioRespondidoService: CuestionarioRespondidoService) {} @@ -12,6 +16,22 @@ export class CuestionarioRespondidoController { return this.cuestionarioRespondidoService.create(createCuestionarioRespondidoDto); } + @Post('submit') + @CuestionarioRespondidoApiDocumentation.ApiSubmitRespuestas + @ApiBody({ + type: SubmitRespuestasDto, + examples: { + comunidad: CuestionarioRespondidoApiDocumentation.ApiSubmitRespuestasExamples.communityExample, + externos: CuestionarioRespondidoApiDocumentation.ApiSubmitRespuestasExamples.externalExample + } + }) + @CuestionarioRespondidoApiDocumentation.ApiSubmitResponse201 + @CuestionarioRespondidoApiDocumentation.ApiSubmitResponse400 + @CuestionarioRespondidoApiDocumentation.ApiSubmitResponse404 + submitRespuestas(@Body() submitRespuestasDto: SubmitRespuestasDto) { + return this.cuestionarioRespondidoService.submitRespuestas(submitRespuestasDto); + } + @Get() findAll() { return this.cuestionarioRespondidoService.findAll(); diff --git a/src/cuestionario_respondido/cuestionario_respondido.documentation.ts b/src/cuestionario_respondido/cuestionario_respondido.documentation.ts new file mode 100644 index 0000000..5cbc74d --- /dev/null +++ b/src/cuestionario_respondido/cuestionario_respondido.documentation.ts @@ -0,0 +1,79 @@ +import { ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger'; + +export class CuestionarioRespondidoApiDocumentation { + static ApiController = ApiTags('Cuestionario Respondido'); + + static ApiSubmitRespuestas = ApiOperation({ + summary: 'Enviar respuestas a un cuestionario', + description: 'Registra las respuestas de un participante a un cuestionario. Crea un registro de participante si no existe uno con ese correo.' + }); + + static ApiSubmitResponse201 = ApiResponse({ + status: 201, + description: 'Respuestas guardadas correctamente', + schema: { + properties: { + success: { type: 'boolean', example: true }, + message: { type: 'string', example: 'Respuestas guardadas correctamente' }, + cuestionarioRespondidoId: { type: 'number', example: 1 } + } + } + }); + + static ApiSubmitResponse400 = ApiResponse({ + status: 400, + description: 'Datos inválidos en la solicitud', + schema: { + properties: { + statusCode: { type: 'number', example: 400 }, + message: { type: 'string', example: 'No se encontró la opción \'Opción inválida\' para la pregunta 101' }, + error: { type: 'string', example: 'Bad Request' } + } + } + }); + + static ApiSubmitResponse404 = ApiResponse({ + status: 404, + description: 'Cuestionario o pregunta no encontrada', + schema: { + properties: { + statusCode: { type: 'number', example: 404 }, + message: { type: 'string', example: 'Cuestionario con ID 999 no encontrado' }, + error: { type: 'string', example: 'Not Found' } + } + } + }); + + static ApiSubmitRespuestasExamples = { + communityExample: { + summary: 'Ejemplo de respuesta para un miembro de la comunidad', + value: { + id_formulario: 1, + correo: 'estudiante@acatlan.unam.mx', + respuestas: [ + { id_pregunta: 101, valor: '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' } // Género + ], + fecha_envio: '2025-04-02T13:45:00' + }, + description: 'Datos de un estudiante de la FES Acatlán que proporciona su número de cuenta.' + }, + externalExample: { + summary: 'Ejemplo de respuesta para un participante externo', + value: { + id_formulario: 1, + correo: 'externo@ejemplo.com', + 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' }, // Género + { id_pregunta: 107, valor: 'UAM Azcapotzalco' } // Institución de procedencia + ], + fecha_envio: '2025-04-02T13:45:00' + }, + description: 'Datos de un participante externo que proporciona su nombre e institución de procedencia.' + } + }; +} \ No newline at end of file diff --git a/src/cuestionario_respondido/cuestionario_respondido.module.ts b/src/cuestionario_respondido/cuestionario_respondido.module.ts index 221348e..c5be459 100644 --- a/src/cuestionario_respondido/cuestionario_respondido.module.ts +++ b/src/cuestionario_respondido/cuestionario_respondido.module.ts @@ -5,10 +5,26 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { CuestionarioRespondido } from './entities/cuestionario_respondido.entity'; import { CuestionarioModule } from '../cuestionario/cuestionario.module'; import { ParticipanteModule } from '../participante/participante.module'; +import { Participante } from '../participante/participante.entity'; +import { Cuestionario } from '../cuestionario/entities/cuestionario.entity'; +import { Pregunta } from '../pregunta/entities/pregunta.entity'; +import { RespuestaParticipanteAbierta } from '../respuesta_participante_abierta/entities/respuesta_participante_abierta.entity'; +import { RespuestaParticipanteCerrada } from '../respuesta_participante_cerrada/entities/respuesta_participante_cerrada.entity'; +import { PreguntaOpcion } from '../pregunta_opcion/entities/pregunta_opcion.entity'; +import { Opcion } from '../opcion/entities/opcion.entity'; @Module({ imports: [ - TypeOrmModule.forFeature([CuestionarioRespondido]), + TypeOrmModule.forFeature([ + CuestionarioRespondido, + Participante, + Cuestionario, + Pregunta, + RespuestaParticipanteAbierta, + RespuestaParticipanteCerrada, + PreguntaOpcion, + Opcion + ]), CuestionarioModule, ParticipanteModule ], diff --git a/src/cuestionario_respondido/cuestionario_respondido.service.ts b/src/cuestionario_respondido/cuestionario_respondido.service.ts index ac9461c..c273278 100644 --- a/src/cuestionario_respondido/cuestionario_respondido.service.ts +++ b/src/cuestionario_respondido/cuestionario_respondido.service.ts @@ -1,19 +1,181 @@ -import { Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository, DataSource } from 'typeorm'; import { CreateCuestionarioRespondidoDto } from './dto/create-cuestionario_respondido.dto'; import { UpdateCuestionarioRespondidoDto } from './dto/update-cuestionario_respondido.dto'; +import { SubmitRespuestasDto } from './dto/submit-respuestas.dto'; +import { CuestionarioRespondido } from './entities/cuestionario_respondido.entity'; +import { Participante } from '../participante/participante.entity'; +import { Cuestionario } from '../cuestionario/entities/cuestionario.entity'; +import { Pregunta } from '../pregunta/entities/pregunta.entity'; +import { RespuestaParticipanteAbierta } from '../respuesta_participante_abierta/entities/respuesta_participante_abierta.entity'; +import { RespuestaParticipanteCerrada } from '../respuesta_participante_cerrada/entities/respuesta_participante_cerrada.entity'; +import { PreguntaOpcion } from '../pregunta_opcion/entities/pregunta_opcion.entity'; +import { Opcion } from '../opcion/entities/opcion.entity'; @Injectable() export class CuestionarioRespondidoService { + constructor( + @InjectRepository(CuestionarioRespondido) + private cuestionarioRespondidoRepository: Repository, + @InjectRepository(Participante) + private participanteRepository: Repository, + @InjectRepository(Cuestionario) + private cuestionarioRepository: Repository, + @InjectRepository(Pregunta) + private preguntaRepository: Repository, + @InjectRepository(RespuestaParticipanteAbierta) + private respuestaAbiertaRepository: Repository, + @InjectRepository(RespuestaParticipanteCerrada) + private respuestaCerradaRepository: Repository, + @InjectRepository(PreguntaOpcion) + private preguntaOpcionRepository: Repository, + @InjectRepository(Opcion) + private opcionRepository: Repository, + private dataSource: DataSource, + ) {} + create(createCuestionarioRespondidoDto: CreateCuestionarioRespondidoDto) { return 'This action adds a new cuestionarioRespondido'; } - findAll() { - return `This action returns all cuestionarioRespondido`; + async submitRespuestas(submitDto: SubmitRespuestasDto) { + const queryRunner = this.dataSource.createQueryRunner(); + await queryRunner.connect(); + await queryRunner.startTransaction(); + + try { + // 1. Verificar que el cuestionario existe + const cuestionario = await this.cuestionarioRepository.findOne({ + where: { id_cuestionario: submitDto.id_formulario } + }); + + if (!cuestionario) { + throw new NotFoundException(`Cuestionario con ID ${submitDto.id_formulario} no encontrado`); + } + + // 2. Buscar o crear participante + let participante = await this.participanteRepository.findOne({ + where: { correo: submitDto.correo } + }); + + if (!participante) { + participante = new Participante(); + participante.correo = submitDto.correo; + participante.id_tipo_user = 1; // Usar un tipo de usuario por defecto + participante = await queryRunner.manager.save(participante); + } + + // 3. Crear un nuevo registro de cuestionario respondido + const cuestionarioRespondido = new CuestionarioRespondido(); + cuestionarioRespondido.cuestionario = cuestionario; + cuestionarioRespondido.participante = participante; + cuestionarioRespondido.fechaRespuesta = submitDto.fecha_envio ? + new Date(submitDto.fecha_envio) : new Date(); + + const savedCuestionarioRespondido = await queryRunner.manager.save(cuestionarioRespondido); + + // 4. Procesar cada respuesta + for (const respuestaDto of submitDto.respuestas) { + // Buscar la pregunta + const pregunta = await this.preguntaRepository.findOne({ + where: { id_pregunta: respuestaDto.id_pregunta }, + relations: ['tipoPregunta'] + }); + + if (!pregunta) { + throw new NotFoundException(`Pregunta con ID ${respuestaDto.id_pregunta} no encontrada`); + } + + // Determinar tipo de pregunta por ID + const esPreguntaAbierta = pregunta.id_tipo_pregunta === 2; + + // O determinar tipo de pregunta por su nombre si existe + const esPreguntaTipoAbierta = pregunta.tipoPregunta && + pregunta.tipoPregunta.tipo_pregunta === 'Abierta'; + + // Determinar tipo de pregunta y guardar respuesta adecuadamente + if (esPreguntaAbierta || esPreguntaTipoAbierta) { + // Respuesta abierta (texto) + const respuestaAbierta = new RespuestaParticipanteAbierta(); + respuestaAbierta.pregunta = pregunta; + respuestaAbierta.respuesta = respuestaDto.valor; + respuestaAbierta.cuestionarioRespondido = savedCuestionarioRespondido; + + await queryRunner.manager.save(respuestaAbierta); + } else { + // Respuesta cerrada (opción) + // Buscar la opción correspondiente + const opciones = await this.opcionRepository.find({ + where: { opcion: respuestaDto.valor } + }); + + if (opciones.length === 0) { + throw new BadRequestException(`No se encontró la opción '${respuestaDto.valor}' para la pregunta ${respuestaDto.id_pregunta}`); + } + + // Buscar la relación entre pregunta y opción + const preguntaOpciones = await this.preguntaOpcionRepository + .createQueryBuilder('po') + .innerJoinAndSelect('po.opcion', 'opcion') + .where('po.id_pregunta = :preguntaId', { preguntaId: pregunta.id_pregunta }) + .andWhere('opcion.id_opcion = :opcionId', { opcionId: opciones[0].id_opcion }) + .getMany(); + + if (preguntaOpciones.length === 0) { + throw new BadRequestException(`La opción '${respuestaDto.valor}' no está asociada a la pregunta ${respuestaDto.id_pregunta}`); + } + + // Guardar respuesta cerrada + const respuestaCerrada = new RespuestaParticipanteCerrada(); + respuestaCerrada.preguntaOpcion = preguntaOpciones[0]; + respuestaCerrada.cuestionarioRespondido = savedCuestionarioRespondido; + + await queryRunner.manager.save(respuestaCerrada); + } + } + + await queryRunner.commitTransaction(); + + return { + success: true, + message: 'Respuestas guardadas correctamente', + cuestionarioRespondidoId: savedCuestionarioRespondido.idCuestionarioRespondido + }; + } catch (error) { + await queryRunner.rollbackTransaction(); + throw error; + } finally { + await queryRunner.release(); + } } - findOne(id: number) { - return `This action returns a #${id} cuestionarioRespondido`; + findAll() { + return this.cuestionarioRespondidoRepository.find({ + relations: ['participante', 'cuestionario', 'respuestasAbiertas', 'respuestasCerradas'] + }); + } + + async findOne(id: number) { + const cuestionarioRespondido = await this.cuestionarioRespondidoRepository.findOne({ + where: { idCuestionarioRespondido: id }, + relations: [ + 'participante', + 'cuestionario', + 'respuestasAbiertas', + 'respuestasAbiertas.pregunta', + 'respuestasCerradas', + 'respuestasCerradas.preguntaOpcion', + 'respuestasCerradas.preguntaOpcion.opcion', + 'respuestasCerradas.preguntaOpcion.pregunta' + ] + }); + + if (!cuestionarioRespondido) { + throw new NotFoundException(`Cuestionario respondido con ID ${id} no encontrado`); + } + + return cuestionarioRespondido; } update(id: number, updateCuestionarioRespondidoDto: UpdateCuestionarioRespondidoDto) { diff --git a/src/cuestionario_respondido/dto/submit-respuestas.dto.ts b/src/cuestionario_respondido/dto/submit-respuestas.dto.ts new file mode 100644 index 0000000..305ee7a --- /dev/null +++ b/src/cuestionario_respondido/dto/submit-respuestas.dto.ts @@ -0,0 +1,63 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsArray, IsDateString, IsNotEmpty, IsNumber, IsOptional, IsString, ValidateNested } from 'class-validator'; +import { Type } from 'class-transformer'; + +export class RespuestaDto { + @ApiProperty({ + description: 'ID de la pregunta respondida', + example: 101 + }) + @IsNumber() + id_pregunta: number; + + @ApiProperty({ + description: 'Valor de la respuesta', + example: 'Si' + }) + @IsString() + valor: string; +} + +export class SubmitRespuestasDto { + @ApiProperty({ + description: 'ID del formulario a responder', + example: 1 + }) + @IsNumber() + id_formulario: number; + + @ApiProperty({ + description: 'Correo electrónico del participante', + example: 'usuario@ejemplo.com' + }) + @IsString() + @IsNotEmpty() + correo: string; + + @ApiProperty({ + description: 'Array de respuestas a las preguntas', + type: [RespuestaDto], + examples: [ + { + id_pregunta: 101, + valor: 'Si' + }, + { + id_pregunta: 102, + valor: '123456789' + } + ] + }) + @IsArray() + @ValidateNested({ each: true }) + @Type(() => RespuestaDto) + respuestas: RespuestaDto[]; + + @ApiProperty({ + description: 'Fecha y hora del envío', + example: '2025-04-02T13:45:00' + }) + @IsDateString() + @IsOptional() + fecha_envio?: string; +} \ No newline at end of file diff --git a/src/pregunta/dto/create-pregunta.dto.ts b/src/pregunta/dto/create-pregunta.dto.ts index fd6c832..ddfad5d 100644 --- a/src/pregunta/dto/create-pregunta.dto.ts +++ b/src/pregunta/dto/create-pregunta.dto.ts @@ -1,33 +1,93 @@ import { Type } from 'class-transformer'; -import { IsArray, IsBoolean, IsNotEmpty, IsNumber, IsOptional, IsString, ValidateNested } from 'class-validator'; +import { IsArray, IsBoolean, IsEnum, IsNotEmpty, IsNumber, IsOptional, IsString, ValidateNested } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export enum TipoPreguntaEnum { + CERRADA = 'Cerrada', + ABIERTA = 'Abierta', + MULTIPLE = 'Multiple' +} export class OpcionDto { + @ApiProperty({ + description: 'Valor de la opción', + example: 'Sí' + }) @IsNotEmpty() @IsString() valor: string; } export class CreatePreguntaDto { + @ApiProperty({ + description: 'Título o texto de la pregunta', + example: '¿Eres parte de la comunidad de la FES Acatlán?' + }) @IsNotEmpty() @IsString() titulo: string; + @ApiProperty({ + description: 'Indica si la pregunta es obligatoria', + example: true, + default: false + }) @IsOptional() @IsBoolean() obligatoria?: boolean; + @ApiProperty({ + description: 'Tipo de pregunta', + enum: Object.values(TipoPreguntaEnum), + enumName: 'TipoPreguntaEnum', + example: TipoPreguntaEnum.CERRADA, + examples: { + 'cerrada': { + summary: 'Pregunta de opción única', + value: TipoPreguntaEnum.CERRADA + }, + 'abierta': { + summary: 'Pregunta de texto libre', + value: TipoPreguntaEnum.ABIERTA + }, + 'multiple': { + summary: 'Pregunta de selección múltiple', + value: TipoPreguntaEnum.MULTIPLE + } + } + }) @IsNotEmpty() - @IsString() + @IsEnum(TipoPreguntaEnum, { + message: 'El tipo debe ser uno de los siguientes valores: Cerrada, Abierta, Multiple' + }) tipo: string; + @ApiProperty({ + description: 'Contador de opciones (se calcula automáticamente)', + required: false + }) @IsOptional() @IsNumber() contador_opcion?: number; + @ApiProperty({ + description: 'ID de la opción de la que depende esta pregunta (para preguntas condicionadas)', + required: false, + example: null + }) @IsOptional() @IsNumber() id_opcion_dependiente?: number; + @ApiProperty({ + description: 'Lista de opciones para preguntas de tipo Cerrada o Multiple', + type: [OpcionDto], + required: false, + examples: [ + { valor: 'Sí' }, + { valor: 'No' } + ] + }) @IsOptional() @IsArray() @ValidateNested({ each: true }) diff --git a/src/pregunta/pregunta.documentation.ts b/src/pregunta/pregunta.documentation.ts index 208879a..06eb1ac 100644 --- a/src/pregunta/pregunta.documentation.ts +++ b/src/pregunta/pregunta.documentation.ts @@ -1,5 +1,6 @@ import { ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger'; import { applyDecorators } from '@nestjs/common'; +import { TipoPreguntaEnum } from './dto/create-pregunta.dto'; export class PreguntaApiDocumentation { // Decoradores para toda la clase del controlador @@ -19,7 +20,12 @@ export class PreguntaApiDocumentation { properties: { titulo: { type: 'string', example: '¿Eres parte de la comunidad?' }, obligatoria: { type: 'boolean', example: true }, - tipo: { type: 'string', example: 'Multiple' }, + tipo: { + type: 'string', + enum: Object.values(TipoPreguntaEnum), + description: 'Tipo de pregunta: Cerrada (opción única), Abierta (texto libre), Multiple (varias opciones)', + example: TipoPreguntaEnum.CERRADA + }, contador_opcion: { type: 'number', example: 0 }, id_opcion_dependiente: { type: 'number', example: null }, opciones: { @@ -32,6 +38,41 @@ export class PreguntaApiDocumentation { } } } + }, + examples: { + cerrada: { + summary: 'Pregunta de opción única', + value: { + titulo: '¿Eres parte de la comunidad de la FES Acatlán?', + obligatoria: true, + tipo: TipoPreguntaEnum.CERRADA, + opciones: [ + { valor: 'Sí' }, + { valor: 'No' } + ] + } + }, + abierta: { + summary: 'Pregunta de texto libre', + value: { + titulo: '¿Qué mejorarías en nuestro servicio?', + obligatoria: false, + tipo: TipoPreguntaEnum.ABIERTA + } + }, + multiple: { + summary: 'Pregunta de selección múltiple', + value: { + titulo: '¿Qué aspectos del servicio fueron de tu agrado?', + obligatoria: false, + tipo: TipoPreguntaEnum.MULTIPLE, + opciones: [ + { valor: 'Rapidez' }, + { valor: 'Atención al cliente' }, + { valor: 'Facilidad de uso' } + ] + } + } } }), ApiResponse({ diff --git a/src/tipo_pregunta/tipo_pregunta.module.ts b/src/tipo_pregunta/tipo_pregunta.module.ts index 0e23b66..3ef0222 100644 --- a/src/tipo_pregunta/tipo_pregunta.module.ts +++ b/src/tipo_pregunta/tipo_pregunta.module.ts @@ -8,6 +8,6 @@ import { TipoPregunta } from './entities/tipo_pregunta.entity'; imports: [TypeOrmModule.forFeature([TipoPregunta])], controllers: [TipoPreguntaController], providers: [TipoPreguntaService], - exports: [TypeOrmModule] + exports: [TipoPreguntaService, TypeOrmModule] }) export class TipoPreguntaModule {} diff --git a/src/tipo_pregunta/tipo_pregunta.service.ts b/src/tipo_pregunta/tipo_pregunta.service.ts index 197028b..37f97c0 100644 --- a/src/tipo_pregunta/tipo_pregunta.service.ts +++ b/src/tipo_pregunta/tipo_pregunta.service.ts @@ -1,6 +1,59 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, OnModuleInit } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { CreateTipoPreguntaDto } from './dto/create-tipo_pregunta.dto'; +import { UpdateTipoPreguntaDto } from './dto/update-tipo_pregunta.dto'; +import { TipoPregunta } from './entities/tipo_pregunta.entity'; @Injectable() -export class TipoPreguntaService { - // Métodos del servicio +export class TipoPreguntaService implements OnModuleInit { + constructor( + @InjectRepository(TipoPregunta) + private tipoPreguntaRepository: Repository, + ) {} + + async onModuleInit() { + await this.seedTipoPreguntas(); + } + + private async seedTipoPreguntas() { + const defaultTipos = [ + { tipo_pregunta: 'Cerrada' }, + { tipo_pregunta: 'Abierta' }, + { tipo_pregunta: 'Multiple' }, + { tipo_pregunta: 'Texto' } + ]; + + for (const tipo of defaultTipos) { + const existingTipo = await this.tipoPreguntaRepository.findOne({ + where: { tipo_pregunta: tipo.tipo_pregunta }, + }); + + if (!existingTipo) { + await this.tipoPreguntaRepository.save(tipo); + } + } + } + + create(createTipoPreguntaDto: CreateTipoPreguntaDto) { + return this.tipoPreguntaRepository.save(createTipoPreguntaDto); + } + + findAll() { + return this.tipoPreguntaRepository.find(); + } + + findOne(id: number) { + return this.tipoPreguntaRepository.findOne({ + where: { id_tipo: id } + }); + } + + update(id: number, updateTipoPreguntaDto: UpdateTipoPreguntaDto) { + return this.tipoPreguntaRepository.update(id, updateTipoPreguntaDto); + } + + remove(id: number) { + return this.tipoPreguntaRepository.delete(id); + } } diff --git a/utils/crear_formulario_feria.ts b/utils/crear_formulario_feria.ts index 23fda26..644269f 100644 --- a/utils/crear_formulario_feria.ts +++ b/utils/crear_formulario_feria.ts @@ -13,29 +13,29 @@ export const feriaSexualidad = { preguntas: [ { titulo: '¿Eres parte de la comunidad de la FES Acatlán?', - tipo: 'Multiple', + tipo: 'Cerrada', opciones: [{ valor: 'Si' }, { valor: 'No' }], obligatoria: true, }, { titulo: 'Numero de cuenta', obligatoria: true, - tipo: 'Numero', + tipo: 'Abierta', }, { titulo: 'Nombre(s)', obligatoria: true, - tipo: 'Texto', + tipo: 'Abierta', }, { titulo: 'Apellidos', obligatoria: true, - tipo: 'Texto', + tipo: 'Abierta', }, { - titulo: 'genero', + titulo: 'Género', obligatoria: true, - tipo: 'Radio', + tipo: 'Cerrada', opciones: [ { valor: 'Masculino' }, { valor: 'Femenino' }, @@ -44,7 +44,7 @@ export const feriaSexualidad = { }, { titulo: 'Institución de procedencia', - tipo: 'Abierto', + tipo: 'Abierta', obligatoria: true, }, ],