reporte csv #21
@@ -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 { CuestionarioRespondidoService } from './cuestionario_respondido.service';
|
||||||
import { CreateCuestionarioRespondidoDto } from './dto/create-cuestionario_respondido.dto';
|
import { CreateCuestionarioRespondidoDto } from './dto/create-cuestionario_respondido.dto';
|
||||||
import { UpdateCuestionarioRespondidoDto } from './dto/update-cuestionario_respondido.dto';
|
import { UpdateCuestionarioRespondidoDto } from './dto/update-cuestionario_respondido.dto';
|
||||||
import { SubmitRespuestasDto } from './dto/submit-respuestas.dto';
|
import { SubmitRespuestasDto } from './dto/submit-respuestas.dto';
|
||||||
import { ApiBody, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
|
import { ApiBody, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||||
import { CuestionarioRespondidoApiDocumentation } from './cuestionario_respondido.documentation';
|
import { CuestionarioRespondidoApiDocumentation } from './cuestionario_respondido.documentation';
|
||||||
|
import { Response } from 'express';
|
||||||
|
|
||||||
@ApiTags('Cuestionario Respondido')
|
@ApiTags('Cuestionario Respondido')
|
||||||
@Controller('cuestionario-respondido')
|
@Controller('cuestionario-respondido')
|
||||||
@@ -37,6 +38,15 @@ export class CuestionarioRespondidoController {
|
|||||||
return this.cuestionarioRespondidoService.findAll();
|
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')
|
@Get(':id')
|
||||||
findOne(@Param('id') id: string) {
|
findOne(@Param('id') id: string) {
|
||||||
return this.cuestionarioRespondidoService.findOne(+id);
|
return this.cuestionarioRespondidoService.findOne(+id);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
|
import { ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||||
|
import { applyDecorators } from '@nestjs/common';
|
||||||
|
|
||||||
export class CuestionarioRespondidoApiDocumentation {
|
export class CuestionarioRespondidoApiDocumentation {
|
||||||
static ApiController = ApiTags('Cuestionario Respondido');
|
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 = {
|
static ApiSubmitRespuestasExamples = {
|
||||||
communityExample: {
|
communityExample: {
|
||||||
summary: 'Ejemplo de respuesta para un miembro de la comunidad',
|
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 { ValidadorRespuestasService } from '../validaciones/validador-respuestas.service';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import * as QRCode from 'qrcode';
|
import * as QRCode from 'qrcode';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CuestionarioRespondidoService {
|
export class CuestionarioRespondidoService {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -267,10 +266,56 @@ export class CuestionarioRespondidoService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
findAll() {
|
async findAll() {
|
||||||
return this.cuestionarioRespondidoRepository.find({
|
// Obtener todos los cuestionarios respondidos con sus relaciones básicas
|
||||||
relations: ['participante', 'cuestionario', 'respuestasAbiertas', 'respuestasCerradas']
|
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) {
|
async findOne(id: number) {
|
||||||
@@ -297,7 +342,7 @@ export class CuestionarioRespondidoService {
|
|||||||
rpc.idRespuestaParticipanteCerrada,
|
rpc.idRespuestaParticipanteCerrada,
|
||||||
rpc.id_pregunta_opcion,
|
rpc.id_pregunta_opcion,
|
||||||
po.id_pregunta,
|
po.id_pregunta,
|
||||||
p.texto_pregunta,
|
p.pregunta,
|
||||||
p.id_tipo_pregunta,
|
p.id_tipo_pregunta,
|
||||||
o.id_opcion,
|
o.id_opcion,
|
||||||
o.opcion
|
o.opcion
|
||||||
@@ -316,7 +361,7 @@ export class CuestionarioRespondidoService {
|
|||||||
id_pregunta_opcion: respuesta.id_pregunta_opcion,
|
id_pregunta_opcion: respuesta.id_pregunta_opcion,
|
||||||
pregunta: {
|
pregunta: {
|
||||||
id_pregunta: respuesta.id_pregunta,
|
id_pregunta: respuesta.id_pregunta,
|
||||||
texto_pregunta: respuesta.texto_pregunta,
|
texto_pregunta: respuesta.pregunta,
|
||||||
id_tipo_pregunta: respuesta.id_tipo_pregunta
|
id_tipo_pregunta: respuesta.id_tipo_pregunta
|
||||||
},
|
},
|
||||||
opcion: {
|
opcion: {
|
||||||
@@ -341,4 +386,98 @@ export class CuestionarioRespondidoService {
|
|||||||
remove(id: number) {
|
remove(id: number) {
|
||||||
return `This action removes a #${id} cuestionarioRespondido`;
|
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 { RespuestaParticipanteCerrada } from 'src/respuesta_participante_cerrada/entities/respuesta_participante_cerrada.entity';
|
||||||
import { RespuestaParticipanteAbierta } from 'src/respuesta_participante_abierta/entities/respuesta_participante_abierta.entity';
|
import { RespuestaParticipanteAbierta } from 'src/respuesta_participante_abierta/entities/respuesta_participante_abierta.entity';
|
||||||
import { Participante } from 'src/participante/participante.entity';
|
import { Participante } from 'src/participante/participante.entity';
|
||||||
|
import { Opcion } from 'src/opcion/entities/opcion.entity';
|
||||||
|
|
||||||
@Entity('cuestionario_respondido')
|
@Entity('cuestionario_respondido')
|
||||||
export class CuestionarioRespondido {
|
export class CuestionarioRespondido {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Opcion } from 'src/opcion/entities/opcion.entity';
|
import { Opcion } from 'src/opcion/entities/opcion.entity';
|
||||||
import { Pregunta } from 'src/pregunta/entities/pregunta.entity';
|
import { Pregunta } from 'src/pregunta/entities/pregunta.entity';
|
||||||
import { RespuestaParticipanteCerrada } from 'src/respuesta_participante_cerrada/entities/respuesta_participante_cerrada.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()
|
@Entity()
|
||||||
@@ -13,13 +13,18 @@ export class PreguntaOpcion {
|
|||||||
posicion: number;
|
posicion: number;
|
||||||
|
|
||||||
@ManyToOne(() => Pregunta)
|
@ManyToOne(() => Pregunta)
|
||||||
|
@JoinColumn({ name: 'id_pregunta' })
|
||||||
pregunta: Pregunta;
|
pregunta: Pregunta;
|
||||||
|
|
||||||
@Column()
|
@Column()
|
||||||
id_pregunta: number;
|
id_pregunta: number;
|
||||||
|
|
||||||
@ManyToOne(() => Opcion, opcion => opcion.preguntasOpciones)
|
@ManyToOne(() => Opcion, opcion => opcion.preguntasOpciones)
|
||||||
|
@JoinColumn({ name: 'id_opcion' })
|
||||||
opcion: Opcion;
|
opcion: Opcion;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
id_opcion: number;
|
||||||
|
|
||||||
@OneToMany(()=> RespuestaParticipanteCerrada, respuestaParticipanteCerrada=>respuestaParticipanteCerrada.preguntaOpcion)
|
@OneToMany(()=> RespuestaParticipanteCerrada, respuestaParticipanteCerrada=>respuestaParticipanteCerrada.preguntaOpcion)
|
||||||
respuestaParticipanteCerrada:RespuestaParticipanteCerrada[];
|
respuestaParticipanteCerrada:RespuestaParticipanteCerrada[];
|
||||||
|
|||||||
Reference in New Issue
Block a user