Merge pull request 'develop' (#22) from develop into master
Reviewed-on: #22
This commit was merged in pull request #22.
This commit is contained in:
@@ -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[];
|
||||
|
||||
Reference in New Issue
Block a user