From 297e9913bb8b72b6a2ac9473413bf3bfc0fd7560 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 7 Apr 2025 15:06:03 -0600 Subject: [PATCH] reporte csv --- .../cuestionario_respondido.controller.ts | 12 +- .../cuestionario_respondido.documentation.ts | 30 ++++ .../cuestionario_respondido.service.ts | 151 +++++++++++++++++- .../cuestionario_respondido.entity.ts | 1 + .../entities/pregunta_opcion.entity.ts | 7 +- 5 files changed, 193 insertions(+), 8 deletions(-) diff --git a/src/cuestionario_respondido/cuestionario_respondido.controller.ts b/src/cuestionario_respondido/cuestionario_respondido.controller.ts index 647c6e1..3e39867 100644 --- a/src/cuestionario_respondido/cuestionario_respondido.controller.ts +++ b/src/cuestionario_respondido/cuestionario_respondido.controller.ts @@ -1,10 +1,11 @@ -import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/common'; +import { Controller, Get, Post, Body, Patch, Param, Delete, Res } from '@nestjs/common'; 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'; +import { Response } from 'express'; @ApiTags('Cuestionario Respondido') @Controller('cuestionario-respondido') @@ -37,6 +38,15 @@ export class CuestionarioRespondidoController { return this.cuestionarioRespondidoService.findAll(); } + @Get('/reporte-respuestas/:id') + @CuestionarioRespondidoApiDocumentation.ApiReporteRespuestas + async descargarReporteRespuestas(@Param('id') id: string, @Res() res: Response) { + const reporte = await this.cuestionarioRespondidoService.generarReporteRespuestas(+id); + res.header('Content-Type', 'text/csv'); + res.header('Content-Disposition', `attachment; filename="reporte-cuestionario-${id}.csv"`); + return res.send(reporte); + } + @Get(':id') findOne(@Param('id') id: string) { return this.cuestionarioRespondidoService.findOne(+id); diff --git a/src/cuestionario_respondido/cuestionario_respondido.documentation.ts b/src/cuestionario_respondido/cuestionario_respondido.documentation.ts index 982d89f..531e303 100644 --- a/src/cuestionario_respondido/cuestionario_respondido.documentation.ts +++ b/src/cuestionario_respondido/cuestionario_respondido.documentation.ts @@ -1,4 +1,5 @@ import { ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { applyDecorators } from '@nestjs/common'; export class CuestionarioRespondidoApiDocumentation { static ApiController = ApiTags('Cuestionario Respondido'); @@ -57,6 +58,35 @@ export class CuestionarioRespondidoApiDocumentation { } }); + static get ApiReporteRespuestas() { + return applyDecorators( + ApiOperation({ + summary: 'Descargar reporte de respuestas de un cuestionario', + description: ` + Este endpoint permite descargar un archivo CSV con todas las respuestas de un cuestionario. + El reporte incluye información del participante, evento asociado, y el contenido de las respuestas. + Se generará un archivo CSV con los siguientes campos: + - ID Respuesta + - Fecha Respuesta + - ID Participante + - Correo Participante + - Evento + - Cuestionario + - Pregunta + - Respuesta + ` + }), + ApiResponse({ + status: 200, + description: 'Reporte de respuestas descargado exitosamente como archivo CSV' + }), + ApiResponse({ + status: 404, + description: 'Cuestionario no encontrado' + }) + ); + } + static ApiSubmitRespuestasExamples = { communityExample: { summary: 'Ejemplo de respuesta para un miembro de la comunidad', diff --git a/src/cuestionario_respondido/cuestionario_respondido.service.ts b/src/cuestionario_respondido/cuestionario_respondido.service.ts index 44c9f64..8a6da34 100644 --- a/src/cuestionario_respondido/cuestionario_respondido.service.ts +++ b/src/cuestionario_respondido/cuestionario_respondido.service.ts @@ -15,7 +15,6 @@ import { Opcion } from '../opcion/entities/opcion.entity'; import { ValidadorRespuestasService } from '../validaciones/validador-respuestas.service'; import axios from 'axios'; import * as QRCode from 'qrcode'; - @Injectable() export class CuestionarioRespondidoService { constructor( @@ -267,10 +266,56 @@ export class CuestionarioRespondidoService { } } - findAll() { - return this.cuestionarioRespondidoRepository.find({ - relations: ['participante', 'cuestionario', 'respuestasAbiertas', 'respuestasCerradas'] + async findAll() { + // Obtener todos los cuestionarios respondidos con sus relaciones básicas + const cuestionariosRespondidos = await this.cuestionarioRespondidoRepository.find({ + relations: ['participante', 'cuestionario', 'respuestasAbiertas', 'respuestasAbiertas.pregunta', 'respuestasCerradas'] }); + + // Para cada cuestionario respondido, obtener y formatear las respuestas cerradas + const resultados = await Promise.all( + cuestionariosRespondidos.map(async (cuestionarioRespondido) => { + // Obtener datos de respuestas cerradas con consulta SQL + const respuestasCerradas = await this.dataSource.query(` + SELECT + rpc.idRespuestaParticipanteCerrada, + rpc.id_pregunta_opcion, + po.id_pregunta, + p.pregunta, + p.id_tipo_pregunta, + o.id_opcion, + o.opcion + FROM respuesta_participante_cerrada rpc + LEFT JOIN pregunta_opcion po ON rpc.id_pregunta_opcion = po.id_pregunta_opcion + LEFT JOIN pregunta p ON po.id_pregunta = p.id_pregunta + LEFT JOIN opcion o ON po.id_opcion = o.id_opcion + WHERE rpc.id_cuestionario_respondido = ? + `, [cuestionarioRespondido.idCuestionarioRespondido]); + + // Formatear respuestas cerradas + const respuestasCerradasFormateadas = respuestasCerradas.map(respuesta => ({ + idRespuestaParticipanteCerrada: respuesta.idRespuestaParticipanteCerrada, + id_pregunta_opcion: respuesta.id_pregunta_opcion, + pregunta: { + id_pregunta: respuesta.id_pregunta, + texto_pregunta: respuesta.pregunta, + id_tipo_pregunta: respuesta.id_tipo_pregunta + }, + opcion: { + id_opcion: respuesta.id_opcion, + opcion: respuesta.opcion + } + })); + + // Devolver cuestionario con respuestas formateadas + return { + ...cuestionarioRespondido, + respuestasCerradas: respuestasCerradasFormateadas + }; + }) + ); + + return resultados; } async findOne(id: number) { @@ -297,7 +342,7 @@ export class CuestionarioRespondidoService { rpc.idRespuestaParticipanteCerrada, rpc.id_pregunta_opcion, po.id_pregunta, - p.texto_pregunta, + p.pregunta, p.id_tipo_pregunta, o.id_opcion, o.opcion @@ -316,7 +361,7 @@ export class CuestionarioRespondidoService { id_pregunta_opcion: respuesta.id_pregunta_opcion, pregunta: { id_pregunta: respuesta.id_pregunta, - texto_pregunta: respuesta.texto_pregunta, + texto_pregunta: respuesta.pregunta, id_tipo_pregunta: respuesta.id_tipo_pregunta }, opcion: { @@ -341,4 +386,98 @@ export class CuestionarioRespondidoService { remove(id: number) { return `This action removes a #${id} cuestionarioRespondido`; } + + async generarReporteRespuestas(idCuestionario: number): Promise { + // Buscar el cuestionario para validar que existe + const cuestionario = await this.cuestionarioRepository.findOne({ + where: { id_cuestionario: idCuestionario }, + relations: ['evento'], + }); + + if (!cuestionario) { + throw new NotFoundException(`Cuestionario con ID ${idCuestionario} no encontrado`); + } + + // Obtener todas las respuestas para este cuestionario con sus relaciones + const respuestas = await this.dataSource.query(` + SELECT + cr.id_cuestionario_respondido, + cr.fecha_respuesta, + p.id_participante, + p.correo AS correo_participante, + c.id_cuestionario, + c.nombre_form AS nombre_cuestionario, + e.id_evento, + e.nombre_evento, + preg.id_pregunta, + preg.pregunta, + -- Para respuestas abiertas + rpa.respuesta AS respuesta_abierta, + -- Para respuestas cerradas + o.id_opcion, + o.opcion AS respuesta_cerrada + FROM cuestionario_respondido cr + JOIN participante p ON cr.id_participante = p.id_participante + JOIN cuestionario c ON cr.id_cuestionario = c.id_cuestionario + LEFT JOIN evento e ON c.id_evento = e.id_evento + -- Respuestas abiertas + LEFT JOIN respuesta_participante_abierta rpa ON rpa.id_cuestionario_respondido = cr.id_cuestionario_respondido + LEFT JOIN pregunta preg_abierta ON rpa.id_pregunta = preg_abierta.id_pregunta + -- Respuestas cerradas + LEFT JOIN respuesta_participante_cerrada rpc ON rpc.id_cuestionario_respondido = cr.id_cuestionario_respondido + LEFT JOIN pregunta_opcion po ON rpc.id_pregunta_opcion = po.id_pregunta_opcion + LEFT JOIN pregunta preg ON po.id_pregunta = preg.id_pregunta + LEFT JOIN opcion o ON po.id_opcion = o.id_opcion + WHERE c.id_cuestionario = ? + ORDER BY cr.id_cuestionario_respondido, preg.id_pregunta + `, [idCuestionario]); + + // Preparar los datos para el CSV + const datosReporte: string[][] = []; + + // Agregar encabezados + const encabezados: string[] = [ + 'ID Respuesta', + 'Fecha Respuesta', + 'ID Participante', + 'Correo Participante', + 'Evento', + 'Cuestionario', + 'Pregunta', + 'Respuesta' + ]; + + datosReporte.push(encabezados); + + // Procesar los resultados para el CSV + for (const fila of respuestas) { + const respuesta = fila.respuesta_abierta || fila.respuesta_cerrada || ''; + + datosReporte.push([ + fila.id_cuestionario_respondido?.toString() || '', + new Date(fila.fecha_respuesta).toLocaleString(), + fila.id_participante?.toString() || '', + fila.correo_participante || '', + fila.nombre_evento || 'Sin evento', + fila.nombre_cuestionario || '', + fila.pregunta || '', + respuesta + ]); + } + + // Generar CSV de forma manual con valores separados por comas y líneas con saltos de línea + // Escapar valores que contengan comas para evitar problemas con el formato CSV + const csvContent = datosReporte.map(row => + row.map(value => { + // Si el valor contiene comas, comillas o saltos de línea, encerrarlo en comillas dobles + // y escapar cualquier comilla doble dentro del valor + if (value.includes(',') || value.includes('"') || value.includes('\n')) { + return `"${value.replace(/"/g, '""')}"`; + } + return value; + }).join(',') + ).join('\n'); + + return csvContent; + } } diff --git a/src/cuestionario_respondido/entities/cuestionario_respondido.entity.ts b/src/cuestionario_respondido/entities/cuestionario_respondido.entity.ts index bd44499..00a3b57 100644 --- a/src/cuestionario_respondido/entities/cuestionario_respondido.entity.ts +++ b/src/cuestionario_respondido/entities/cuestionario_respondido.entity.ts @@ -4,6 +4,7 @@ import { Cuestionario } from 'src/cuestionario/entities/cuestionario.entity'; import { RespuestaParticipanteCerrada } from 'src/respuesta_participante_cerrada/entities/respuesta_participante_cerrada.entity'; import { RespuestaParticipanteAbierta } from 'src/respuesta_participante_abierta/entities/respuesta_participante_abierta.entity'; import { Participante } from 'src/participante/participante.entity'; +import { Opcion } from 'src/opcion/entities/opcion.entity'; @Entity('cuestionario_respondido') export class CuestionarioRespondido { diff --git a/src/pregunta_opcion/entities/pregunta_opcion.entity.ts b/src/pregunta_opcion/entities/pregunta_opcion.entity.ts index 3c0a024..794bb65 100644 --- a/src/pregunta_opcion/entities/pregunta_opcion.entity.ts +++ b/src/pregunta_opcion/entities/pregunta_opcion.entity.ts @@ -1,7 +1,7 @@ import { Opcion } from 'src/opcion/entities/opcion.entity'; import { Pregunta } from 'src/pregunta/entities/pregunta.entity'; import { RespuestaParticipanteCerrada } from 'src/respuesta_participante_cerrada/entities/respuesta_participante_cerrada.entity'; -import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, OneToMany } from 'typeorm'; +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, OneToMany, JoinColumn } from 'typeorm'; @Entity() @@ -13,13 +13,18 @@ export class PreguntaOpcion { posicion: number; @ManyToOne(() => Pregunta) + @JoinColumn({ name: 'id_pregunta' }) pregunta: Pregunta; @Column() id_pregunta: number; @ManyToOne(() => Opcion, opcion => opcion.preguntasOpciones) + @JoinColumn({ name: 'id_opcion' }) opcion: Opcion; + + @Column() + id_opcion: number; @OneToMany(()=> RespuestaParticipanteCerrada, respuestaParticipanteCerrada=>respuestaParticipanteCerrada.preguntaOpcion) respuestaParticipanteCerrada:RespuestaParticipanteCerrada[]; -- 2.43.0