reporte csv

This commit is contained in:
Your Name
2025-04-07 15:06:03 -06:00
parent 60127068aa
commit 297e9913bb
5 changed files with 193 additions and 8 deletions
@@ -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);
@@ -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',
@@ -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<string> {
// 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;
}
}
@@ -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 {
@@ -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[];