base con autenticacion, registro, modulo encuestas
This commit is contained in:
76
apps/api/src/features/surveys/Untitled-1.json
Normal file
76
apps/api/src/features/surveys/Untitled-1.json
Normal file
@@ -0,0 +1,76 @@
|
||||
[
|
||||
{
|
||||
"id": "q-1",
|
||||
"type": "simple",
|
||||
"position": 0,
|
||||
"question": "Pregunta N° 1 Simple",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"id": "q-2",
|
||||
"type": "multiple_choice",
|
||||
"options": [
|
||||
{
|
||||
"id": "1",
|
||||
"text": "Opcion Prueba 1"
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"text": "Opcion Prueba 2 "
|
||||
},
|
||||
{
|
||||
"id": "3",
|
||||
"text": "Opcion Prueba 3"
|
||||
},
|
||||
{
|
||||
"id": "4",
|
||||
"text": "Opcion Prueba 4"
|
||||
}
|
||||
],
|
||||
"position": 1,
|
||||
"question": "Pregunta de Multiples Opciones N°2 ",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"id": "q-3",
|
||||
"type": "single_choice",
|
||||
"options": [
|
||||
{
|
||||
"id": "1",
|
||||
"text": "Opcion Unica Prueba 1"
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"text": "Opcion Unica Prueba 2"
|
||||
}
|
||||
],
|
||||
"position": 2,
|
||||
"question": "Preguntas de una sola opcion N°3 ",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"id": "q-4",
|
||||
"type": "select",
|
||||
"options": [
|
||||
{
|
||||
"id": "1",
|
||||
"text": "Seleccion 1"
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"text": "Seleccion 2 "
|
||||
},
|
||||
{
|
||||
"id": "3",
|
||||
"text": "Seleccion 3"
|
||||
},
|
||||
{
|
||||
"id": "4",
|
||||
"text": "Seleccion 4"
|
||||
}
|
||||
],
|
||||
"position": 3,
|
||||
"question": "Pregunta seleccion N° 4",
|
||||
"required": true
|
||||
}
|
||||
]
|
||||
73
apps/api/src/features/surveys/dto/create-survey.dto.ts
Normal file
73
apps/api/src/features/surveys/dto/create-survey.dto.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import { IsArray, IsBoolean, IsDate, IsInt, IsNotEmpty, IsOptional, IsString, ValidateNested } from 'class-validator';
|
||||
|
||||
export class CreateSurveyDto {
|
||||
@ApiProperty({ description: 'Survey title' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
title: string;
|
||||
|
||||
@ApiProperty({ description: 'Survey description' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
description: string;
|
||||
|
||||
@ApiProperty({ description: 'Target audience' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
targetAudience: string;
|
||||
|
||||
@ApiProperty({ description: 'Closing date' })
|
||||
@IsOptional()
|
||||
@IsDate()
|
||||
closingDate?: Date;
|
||||
|
||||
@ApiProperty({ description: 'Publication status' })
|
||||
@IsBoolean()
|
||||
published: boolean;
|
||||
|
||||
@ApiProperty({ description: 'Survey questions' })
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true }) // Asegura que cada elemento sea validado individualmente
|
||||
@Type(() => QuestionDto) // Evita que se envuelva en otro array
|
||||
questions: any[];
|
||||
}
|
||||
|
||||
|
||||
|
||||
class QuestionDto {
|
||||
@IsString()
|
||||
id: string;
|
||||
|
||||
@IsInt()
|
||||
position: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
question?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
content?: string;
|
||||
|
||||
@IsBoolean()
|
||||
required: boolean;
|
||||
|
||||
@IsString()
|
||||
type: string;
|
||||
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => OptionsDto)
|
||||
options?: OptionsDto[];
|
||||
}
|
||||
|
||||
class OptionsDto {
|
||||
@IsString()
|
||||
id: string;
|
||||
|
||||
@IsString()
|
||||
text: string;
|
||||
}
|
||||
10
apps/api/src/features/surveys/dto/find-for-user.dto.ts
Normal file
10
apps/api/src/features/surveys/dto/find-for-user.dto.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsArray, IsNotEmpty } from 'class-validator';
|
||||
|
||||
export class FindForUserDto {
|
||||
@ApiProperty({ description: 'Survey rol' })
|
||||
@IsArray()
|
||||
@IsNotEmpty()
|
||||
rol: any;
|
||||
|
||||
}
|
||||
15
apps/api/src/features/surveys/dto/response-survey.dto.ts
Normal file
15
apps/api/src/features/surveys/dto/response-survey.dto.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsArray, IsNotEmpty, IsString } from 'class-validator';
|
||||
|
||||
export class AnswersSurveyDto {
|
||||
@ApiProperty({ description: 'Survey id' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
surveyId: string;
|
||||
|
||||
@ApiProperty({ description: 'Survey answers' })
|
||||
@IsArray()
|
||||
@IsNotEmpty()
|
||||
answers: any;
|
||||
|
||||
}
|
||||
63
apps/api/src/features/surveys/dto/statistics-response.dto.ts
Normal file
63
apps/api/src/features/surveys/dto/statistics-response.dto.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class QuestionStatDto {
|
||||
@ApiProperty({ description: 'Question identifier' })
|
||||
questionId: string;
|
||||
|
||||
@ApiProperty({ description: 'Question label' })
|
||||
label: string;
|
||||
|
||||
@ApiProperty({ description: 'Count of responses for this option' })
|
||||
count: number;
|
||||
}
|
||||
|
||||
export class SurveyDetailDto {
|
||||
@ApiProperty({ description: 'Survey ID' })
|
||||
id: number;
|
||||
|
||||
@ApiProperty({ description: 'Survey title' })
|
||||
title: string;
|
||||
|
||||
@ApiProperty({ description: 'Survey description' })
|
||||
description: string;
|
||||
|
||||
@ApiProperty({ description: 'Total responses received' })
|
||||
totalResponses: number;
|
||||
|
||||
@ApiProperty({ description: 'Target audience' })
|
||||
targetAudience: any;
|
||||
|
||||
@ApiProperty({ description: 'Creation date' })
|
||||
createdAt: string;
|
||||
|
||||
@ApiProperty({ description: 'Closing date' })
|
||||
closingDate?: string | null;
|
||||
|
||||
@ApiProperty({ description: 'Question statistics', type: [QuestionStatDto] })
|
||||
// @ApiProperty({ description: 'Question statistics' })
|
||||
questionStats: QuestionStatDto[];
|
||||
// questionStats: any;
|
||||
}
|
||||
|
||||
export class SurveyStatisticsResponseDto {
|
||||
@ApiProperty({ description: 'Total number of surveys' })
|
||||
totalSurveys: number;
|
||||
|
||||
@ApiProperty({ description: 'Total number of responses across all surveys' })
|
||||
totalResponses: number;
|
||||
|
||||
@ApiProperty({ description: 'Completion rate percentage' })
|
||||
completionRate: number;
|
||||
|
||||
@ApiProperty({ description: 'Surveys created by month' })
|
||||
surveysByMonth: { month: string; count: number }[];
|
||||
|
||||
@ApiProperty({ description: 'Responses by audience type' })
|
||||
responsesByAudience: { name: any; value: number }[];
|
||||
|
||||
@ApiProperty({ description: 'Response distribution by survey' })
|
||||
responseDistribution: { title: string; responses: number }[];
|
||||
|
||||
@ApiProperty({ description: 'Detailed statistics for each survey', type: [SurveyDetailDto] })
|
||||
surveyDetails: SurveyDetailDto[];
|
||||
}
|
||||
5
apps/api/src/features/surveys/dto/update-survey.dto.ts
Normal file
5
apps/api/src/features/surveys/dto/update-survey.dto.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { PartialType } from '@nestjs/swagger';
|
||||
import { CreateSurveyDto } from './create-survey.dto';
|
||||
|
||||
|
||||
export class UpdateSurveyDto extends PartialType(CreateSurveyDto) {}
|
||||
30
apps/api/src/features/surveys/entities/survey.entity.ts
Normal file
30
apps/api/src/features/surveys/entities/survey.entity.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class Survey {
|
||||
@ApiProperty({ description: 'Survey ID' })
|
||||
id: number;
|
||||
|
||||
@ApiProperty({ description: 'Survey title' })
|
||||
title: string;
|
||||
|
||||
@ApiProperty({ description: 'Survey description' })
|
||||
description: string;
|
||||
|
||||
@ApiProperty({ description: 'Target audience for the survey' })
|
||||
targetAudience: string;
|
||||
|
||||
@ApiProperty({ description: 'Survey closing date' })
|
||||
closingDate?: Date;
|
||||
|
||||
@ApiProperty({ description: 'Survey publication status' })
|
||||
published: boolean;
|
||||
|
||||
@ApiProperty({ description: 'Survey questions' })
|
||||
questions: any[];
|
||||
|
||||
@ApiProperty({ description: 'Creation date' })
|
||||
created_at?: Date;
|
||||
|
||||
@ApiProperty({ description: 'Last update date' })
|
||||
updated_at?: Date;
|
||||
}
|
||||
125
apps/api/src/features/surveys/surveys.controller.ts
Normal file
125
apps/api/src/features/surveys/surveys.controller.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { Roles } from '@/common/decorators/roles.decorator';
|
||||
import { PaginationDto } from '@/common/dto/pagination.dto';
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
Patch,
|
||||
Post,
|
||||
Query,
|
||||
Req,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { CreateSurveyDto } from './dto/create-survey.dto';
|
||||
import { UpdateSurveyDto } from './dto/update-survey.dto';
|
||||
import { SurveysService } from './surveys.service';
|
||||
import { AnswersSurveyDto } from './dto/response-survey.dto';
|
||||
import { Request } from 'express';
|
||||
import { FindForUserDto } from './dto/find-for-user.dto';
|
||||
import { SurveyStatisticsResponseDto } from './dto/statistics-response.dto';
|
||||
|
||||
@ApiTags('surveys')
|
||||
@Controller('surveys')
|
||||
export class SurveysController {
|
||||
constructor(private readonly surveysService: SurveysService) {}
|
||||
|
||||
@Get()
|
||||
@Roles('admin')
|
||||
@ApiOperation({ summary: 'Get all surveys with pagination and filters' })
|
||||
@ApiResponse({ status: 200, description: 'Return paginated surveys.' })
|
||||
async findAll(@Query() paginationDto: PaginationDto) {
|
||||
const result = await this.surveysService.findAll(paginationDto);
|
||||
return {
|
||||
message: 'Surveys fetched successfully',
|
||||
data: result.data,
|
||||
meta: result.meta,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Post('for-user')
|
||||
@ApiOperation({ summary: 'Get all surveys with pagination and filters for user' })
|
||||
@ApiResponse({ status: 200, description: 'Return paginated surveys for user.' })
|
||||
async findAllForUser(@Req() req: Request, @Query() paginationDto: PaginationDto, @Body() findForUserDto: FindForUserDto) {
|
||||
|
||||
const userId = req['user'].id;
|
||||
|
||||
const result = await this.surveysService.findAllForUser(paginationDto, userId, findForUserDto);
|
||||
|
||||
return {
|
||||
message: 'Surveys fetched successfully',
|
||||
data: result.data,
|
||||
meta: result.meta,
|
||||
};
|
||||
}
|
||||
|
||||
@Get('statistics')
|
||||
@Roles('admin', 'superadmin', 'autoridad')
|
||||
@ApiOperation({ summary: 'Get survey statistics' })
|
||||
|
||||
@ApiResponse({ status: 200, description: 'Return survey statistics.'})
|
||||
|
||||
async getStatistics() {
|
||||
const data = await this.surveysService.getStatistics();
|
||||
return {
|
||||
message: 'Survey statistics fetched successfully',
|
||||
data
|
||||
};
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: 'Get a survey by ID' })
|
||||
@ApiResponse({ status: 200, description: 'Return the survey.' })
|
||||
@ApiResponse({ status: 404, description: 'Survey not found.' })
|
||||
async findOne(@Param('id') id: string) {
|
||||
const data = await this.surveysService.findOne(id);
|
||||
return { message: 'Survey fetched successfully', data };
|
||||
}
|
||||
|
||||
@Post()
|
||||
@Roles('admin')
|
||||
@ApiOperation({ summary: 'Create a new survey' })
|
||||
@ApiResponse({ status: 201, description: 'Survey created successfully.' })
|
||||
async create(@Body() createSurveyDto: CreateSurveyDto) {
|
||||
console.log(createSurveyDto);
|
||||
|
||||
const data = await this.surveysService.create(createSurveyDto);
|
||||
return { message: 'Survey created successfully', data };
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@Roles('admin')
|
||||
@ApiOperation({ summary: 'Update a survey' })
|
||||
@ApiResponse({ status: 200, description: 'Survey updated successfully.' })
|
||||
@ApiResponse({ status: 404, description: 'Survey not found.' })
|
||||
async update(
|
||||
@Param('id') id: string,
|
||||
@Body() updateSurveyDto: UpdateSurveyDto,
|
||||
) {
|
||||
const data = await this.surveysService.update(id, updateSurveyDto);
|
||||
return { message: 'Survey updated successfully', data };
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@Roles('admin')
|
||||
@ApiOperation({ summary: 'Delete a survey' })
|
||||
@ApiResponse({ status: 200, description: 'Survey deleted successfully.' })
|
||||
@ApiResponse({ status: 404, description: 'Survey not found.' })
|
||||
async remove(@Param('id') id: string) {
|
||||
return await this.surveysService.remove(id);
|
||||
}
|
||||
|
||||
@Post('answers')
|
||||
@ApiOperation({ summary: 'Create a new answers' })
|
||||
@ApiResponse({ status: 201, description: 'Survey answers successfully.' })
|
||||
async answers(@Req() req: Request, @Body() answersSurveyDto: AnswersSurveyDto) {
|
||||
const userId = (req as any).user?.id;
|
||||
const data = await this.surveysService.answers(Number(userId),answersSurveyDto);
|
||||
return { message: 'Survey answers created successfully', data };
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
10
apps/api/src/features/surveys/surveys.module.ts
Normal file
10
apps/api/src/features/surveys/surveys.module.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { SurveysService } from './surveys.service';
|
||||
import { SurveysController } from './surveys.controller';
|
||||
|
||||
@Module({
|
||||
controllers: [SurveysController],
|
||||
providers: [SurveysService],
|
||||
exports: [SurveysService],
|
||||
})
|
||||
export class SurveysModule {}
|
||||
535
apps/api/src/features/surveys/surveys.service.ts
Normal file
535
apps/api/src/features/surveys/surveys.service.ts
Normal file
@@ -0,0 +1,535 @@
|
||||
import { DRIZZLE_PROVIDER } from '@/database/drizzle-provider';
|
||||
import { surveys, answersSurveys, viewSurveys } from '@/database/index';
|
||||
import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common';
|
||||
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
||||
import * as schema from '@/database/index';
|
||||
import { CreateSurveyDto } from './dto/create-survey.dto';
|
||||
import { UpdateSurveyDto } from './dto/update-survey.dto';
|
||||
import { and, count, eq, ilike, isNull, or, sql } from 'drizzle-orm';
|
||||
import { SurveyDetailDto, SurveyStatisticsResponseDto } from './dto/statistics-response.dto';
|
||||
import { PaginationDto } from '@/common/dto/pagination.dto';
|
||||
import { AnswersSurveyDto } from './dto/response-survey.dto';
|
||||
import { FindForUserDto } from './dto/find-for-user.dto';
|
||||
|
||||
@Injectable()
|
||||
export class SurveysService {
|
||||
constructor(
|
||||
@Inject(DRIZZLE_PROVIDER) private drizzle: NodePgDatabase<typeof schema>,
|
||||
) { }
|
||||
|
||||
async findAll(paginationDto: PaginationDto) {
|
||||
const { page = 1, limit = 10, search = '', sortBy = 'id', sortOrder = 'asc' } = paginationDto || {};
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
// Build search condition
|
||||
const searchCondition = ilike(surveys.title, `%${search}%`);
|
||||
|
||||
|
||||
// Build sort condition
|
||||
const orderBy = sortOrder === 'asc'
|
||||
? sql`${surveys[sortBy as keyof typeof surveys]} asc`
|
||||
: sql`${surveys[sortBy as keyof typeof surveys]} desc`;
|
||||
|
||||
// Get total count for pagination
|
||||
const totalCountResult = await this.drizzle
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(surveys)
|
||||
.where(searchCondition);
|
||||
|
||||
const totalCount = Number(totalCountResult[0].count);
|
||||
const totalPages = Math.ceil(totalCount / limit);
|
||||
|
||||
|
||||
// Get paginated data
|
||||
const data = await this.drizzle
|
||||
.select()
|
||||
.from(surveys)
|
||||
.where(searchCondition)
|
||||
.orderBy(orderBy)
|
||||
.limit(limit)
|
||||
.offset(offset);
|
||||
|
||||
const dataSurvey = data.map((survey) => {
|
||||
return {
|
||||
...survey,
|
||||
closingDate: survey.closingDate ? new Date(survey.closingDate) : null,
|
||||
};
|
||||
});
|
||||
|
||||
// Build pagination metadata
|
||||
const meta = {
|
||||
page,
|
||||
limit,
|
||||
totalCount,
|
||||
totalPages,
|
||||
hasNextPage: page < totalPages,
|
||||
hasPreviousPage: page > 1,
|
||||
nextPage: page < totalPages ? page + 1 : null,
|
||||
previousPage: page > 1 ? page - 1 : null,
|
||||
};
|
||||
|
||||
return { data: dataSurvey, meta };
|
||||
|
||||
}
|
||||
|
||||
async findAllForUser(paginationDto: PaginationDto, userId: number, findForUserDto: FindForUserDto) {
|
||||
const { page = 1, limit = 10, search = '', sortBy = 'created_at', sortOrder = 'asc' } = paginationDto || {};
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
let searchCondition : any = false
|
||||
|
||||
// Build search condition
|
||||
// if (findForUserDto.rol[0].rol === 'superadmin' || findForUserDto.rol[0].rol == 'admin') {
|
||||
// searchCondition = and(
|
||||
// or(eq(viewSurveys.targetAudience, 'producers'), eq(viewSurveys.targetAudience, 'organization'), eq(viewSurveys.targetAudience, 'all')),
|
||||
// or(eq(viewSurveys.user_id, userId), isNull(viewSurveys.user_id))
|
||||
// );
|
||||
// } else {
|
||||
// searchCondition = and(
|
||||
// or(eq(viewSurveys.targetAudience, findForUserDto.rol[0].rol), eq(viewSurveys.targetAudience, 'all')),
|
||||
// or(eq(viewSurveys.user_id, userId), isNull(viewSurveys.user_id))
|
||||
// );
|
||||
// }
|
||||
|
||||
if (findForUserDto.rol[0].rol !== 'superadmin' && findForUserDto.rol[0].rol !== 'admin') {
|
||||
searchCondition = or(eq(surveys.targetAudience, findForUserDto.rol[0].rol), eq(surveys.targetAudience, 'all'))
|
||||
}
|
||||
|
||||
// console.log(searchCondition);
|
||||
|
||||
// Build sort condition
|
||||
const orderBy = sortOrder === 'asc'
|
||||
? sql`${surveys[sortBy as keyof typeof surveys]} asc`
|
||||
: sql`${surveys[sortBy as keyof typeof surveys]} desc`;
|
||||
|
||||
// Get total count for pagination
|
||||
const totalCountResult = await this.drizzle
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(surveys)
|
||||
.leftJoin(answersSurveys, and(eq(answersSurveys.surveyId,surveys.id),eq(answersSurveys.userId,userId)))
|
||||
.where(searchCondition);
|
||||
|
||||
const totalCount = Number(totalCountResult[0].count);
|
||||
const totalPages = Math.ceil(totalCount / limit);
|
||||
|
||||
// Get paginated data
|
||||
const data = await this.drizzle
|
||||
.select()
|
||||
.from(surveys)
|
||||
.leftJoin(answersSurveys, and(eq(answersSurveys.surveyId,surveys.id),eq(answersSurveys.userId,userId)))
|
||||
.where(searchCondition)
|
||||
.orderBy(orderBy)
|
||||
.limit(limit)
|
||||
.offset(offset);
|
||||
|
||||
// Build pagination metadata
|
||||
const meta = {
|
||||
page,
|
||||
limit,
|
||||
totalCount,
|
||||
totalPages,
|
||||
hasNextPage: page < totalPages,
|
||||
hasPreviousPage: page > 1,
|
||||
nextPage: page < totalPages ? page + 1 : null,
|
||||
previousPage: page > 1 ? page - 1 : null,
|
||||
};
|
||||
|
||||
return { data, meta };
|
||||
}
|
||||
|
||||
async findOne(id: string) {
|
||||
const survey = await this.drizzle
|
||||
.select()
|
||||
.from(surveys)
|
||||
.where(eq(surveys.id, parseInt(id)));
|
||||
|
||||
if (survey.length === 0) {
|
||||
throw new HttpException('Survey not found', HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
const dataSurvey = survey.map((survey) => {
|
||||
return {
|
||||
...survey,
|
||||
closingDate: survey.closingDate ? new Date(survey.closingDate) : null,
|
||||
};
|
||||
});
|
||||
|
||||
return dataSurvey[0];
|
||||
}
|
||||
|
||||
async findByTitle(title: string) {
|
||||
return await this.drizzle
|
||||
.select()
|
||||
.from(surveys)
|
||||
.where(eq(surveys.title, title));
|
||||
}
|
||||
|
||||
async create(createSurveyDto: CreateSurveyDto) {
|
||||
|
||||
const find = await this.findByTitle(createSurveyDto.title);
|
||||
|
||||
if (find.length !== 0) {
|
||||
throw new HttpException('Survey already exists', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
const survey = await this.drizzle
|
||||
.insert(surveys)
|
||||
.values({
|
||||
...createSurveyDto,
|
||||
closingDate: createSurveyDto.closingDate?.toISOString(),
|
||||
})
|
||||
.returning();
|
||||
const dataSurvey = survey.map((survey) => {
|
||||
return {
|
||||
...survey,
|
||||
closingDate: survey.closingDate ? new Date(survey.closingDate) : null,
|
||||
};
|
||||
});
|
||||
|
||||
return dataSurvey[0];
|
||||
}
|
||||
|
||||
async update(id: string, updateSurveyDto: UpdateSurveyDto) {
|
||||
|
||||
const find = await this.findOne(id)
|
||||
if (!find) {
|
||||
throw new HttpException('Survey not found', HttpStatus.BAD_REQUEST)
|
||||
}
|
||||
|
||||
const find2 = await this.findByTitle(updateSurveyDto.title ?? '');
|
||||
|
||||
if (find2.length !== 0 && find2[0].id !== parseInt(id)) {
|
||||
throw new HttpException('Survey already exists', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
const survey = await this.drizzle
|
||||
.update(surveys)
|
||||
.set({
|
||||
...updateSurveyDto,
|
||||
closingDate: updateSurveyDto.closingDate?.toISOString(),
|
||||
updated_at: new Date(),
|
||||
})
|
||||
.where(eq(surveys.id, parseInt(id)))
|
||||
.returning();
|
||||
|
||||
const dataSurvey = survey.map((survey) => {
|
||||
return {
|
||||
...survey,
|
||||
closingDate: survey.closingDate ? new Date(survey.closingDate) : null,
|
||||
};
|
||||
});
|
||||
|
||||
return dataSurvey[0];
|
||||
}
|
||||
|
||||
async remove(id: string) {
|
||||
const find = await this.findOne(id);
|
||||
if (!find) {
|
||||
throw new HttpException('Survey not found', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
await this.drizzle
|
||||
.delete(surveys)
|
||||
.where(eq(surveys.id, parseInt(id)));
|
||||
|
||||
return { message: 'Survey deleted successfully' };
|
||||
}
|
||||
|
||||
async answers(userId: number, answersSurveyDto: AnswersSurveyDto) {
|
||||
|
||||
const find = await this.drizzle.select()
|
||||
.from(answersSurveys)
|
||||
.where(and(eq(answersSurveys.surveyId, Number(answersSurveyDto.surveyId)), (eq(answersSurveys.userId, userId))));
|
||||
|
||||
|
||||
if (find.length !== 0) {
|
||||
throw new HttpException('Survey answers already exists', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
const survey = await this.drizzle
|
||||
.insert(answersSurveys)
|
||||
.values({
|
||||
...answersSurveyDto,
|
||||
surveyId: Number(answersSurveyDto.surveyId),
|
||||
userId: userId,
|
||||
})
|
||||
.returning();
|
||||
|
||||
return survey[0];
|
||||
}
|
||||
|
||||
async getStatistics(): Promise<SurveyStatisticsResponseDto> {
|
||||
// Obtener el número total de encuestas
|
||||
const totalSurveys = await this.getTotalSurveysCount();
|
||||
// Obtener el número total de respuestas
|
||||
const totalResponses = await this.getTotalResponsesCount();
|
||||
|
||||
// Calcular la tasa de finalización
|
||||
const completionRate = totalSurveys > 0 ? Math.round((totalResponses / totalSurveys) * 100) : 0;
|
||||
|
||||
// Obtener las encuestas por mes
|
||||
const surveysByMonth = await this.getSurveysByMonth();
|
||||
|
||||
// Obtener las respuestas por audiencia
|
||||
const responsesByAudience = await this.getResponsesByAudience();
|
||||
|
||||
// Obtener la distribución de respuestas por encuesta
|
||||
const responseDistribution = await this.getResponseDistribution();
|
||||
|
||||
// Obtener las estadísticas detalladas de las encuestas
|
||||
const surveyDetails = await this.getSurveyDetails();
|
||||
|
||||
return {
|
||||
totalSurveys,
|
||||
totalResponses,
|
||||
completionRate,
|
||||
surveysByMonth,
|
||||
responsesByAudience,
|
||||
responseDistribution,
|
||||
surveyDetails,
|
||||
}
|
||||
}
|
||||
|
||||
private async getTotalSurveysCount(): Promise<number> {
|
||||
const result = await this.drizzle
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(surveys);
|
||||
return Number(result[0].count);
|
||||
}
|
||||
|
||||
private async getTotalResponsesCount(): Promise<number> {
|
||||
const result = await this.drizzle
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(answersSurveys);
|
||||
return Number(result[0].count);
|
||||
}
|
||||
|
||||
private async getSurveysByMonth(): Promise<{ month: string; count: number }[]> {
|
||||
const result = await this.drizzle
|
||||
.select({
|
||||
month: sql<string>`to_char(created_at, 'YYYY-MM')`,
|
||||
count: sql<number>`count(*)`,
|
||||
})
|
||||
.from(surveys)
|
||||
.groupBy(sql`to_char(created_at, 'YYYY-MM')`)
|
||||
.orderBy(sql`to_char(created_at, 'YYYY-MM')`);
|
||||
return result.map(item => ({ month: item.month, count: Number(item.count) }));
|
||||
}
|
||||
|
||||
private async getResponsesByAudience(): Promise<{ name: any; value: number }[]> {
|
||||
const result = await this.drizzle
|
||||
.select({
|
||||
audience: surveys.targetAudience,
|
||||
count: sql<number>`count(*)`,
|
||||
})
|
||||
.from(answersSurveys)
|
||||
.leftJoin(surveys, eq(answersSurveys.surveyId, surveys.id))
|
||||
.groupBy(surveys.targetAudience);
|
||||
return result.map(item =>
|
||||
{
|
||||
let audience = 'Sin definir'
|
||||
if (item.audience == 'all') {
|
||||
audience = 'General'
|
||||
} else if (item.audience == 'organization') {
|
||||
audience = 'Organización'
|
||||
} else if (item.audience == 'producers') {
|
||||
audience = 'Productores'
|
||||
}
|
||||
return ({ name: audience, value: Number(item.count) })
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private async getResponseDistribution(): Promise<{ title: string; responses: number }[]> {
|
||||
const result = await this.drizzle
|
||||
.select({
|
||||
id: surveys.id,
|
||||
title: surveys.title,
|
||||
responses: sql<number>`count(${answersSurveys.id})`,
|
||||
})
|
||||
.from(surveys)
|
||||
.leftJoin(answersSurveys, eq(surveys.id, answersSurveys.surveyId))
|
||||
.groupBy(surveys.id, surveys.title)
|
||||
.orderBy(sql`count(${answersSurveys.id}) desc`)
|
||||
.limit(10);
|
||||
return result.map(item => ({ title: item.title, responses: Number(item.responses) }));
|
||||
}
|
||||
|
||||
private async getSurveyDetails(): Promise<SurveyDetailDto[]> {
|
||||
const allSurveys = await this.drizzle
|
||||
.select({
|
||||
id: surveys.id,
|
||||
title: surveys.title,
|
||||
description: surveys.description,
|
||||
targetAudience: surveys.targetAudience,
|
||||
createdAt: surveys.created_at,
|
||||
closingDate: surveys.closingDate,
|
||||
questions: surveys.questions,
|
||||
})
|
||||
.from(surveys);
|
||||
|
||||
|
||||
return await Promise.all(
|
||||
allSurveys.map(async (survey) => {
|
||||
// Obtener el número total de respuestas para esta encuesta
|
||||
const totalSurveyResponses = await this.getTotalSurveyResponses(survey.id);
|
||||
|
||||
// Obtener todas las respuestas para esta encuesta
|
||||
const answersResult = await this.drizzle
|
||||
.select({ answers: answersSurveys.answers })
|
||||
.from(answersSurveys)
|
||||
.where(eq(answersSurveys.surveyId, survey.id));
|
||||
|
||||
let audience = 'Sin definir'
|
||||
if (survey.targetAudience == 'all') {
|
||||
audience = 'General'
|
||||
} else if (survey.targetAudience == 'organization') {
|
||||
audience = 'Organización'
|
||||
} else if (survey.targetAudience == 'producers') {
|
||||
audience = 'Productores'
|
||||
}
|
||||
|
||||
// Procesar las estadísticas de las preguntas
|
||||
const questionStats = this.processQuestionStats(survey.questions as any[], answersResult);
|
||||
|
||||
return {
|
||||
id: survey.id,
|
||||
title: survey.title,
|
||||
description: survey.description,
|
||||
totalResponses: totalSurveyResponses,
|
||||
// targetAudience: survey.targetAudience,
|
||||
targetAudience: audience,
|
||||
createdAt: survey.createdAt.toISOString(),
|
||||
closingDate: survey.closingDate ? new Date(survey.closingDate).toISOString() : undefined,
|
||||
questionStats,
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private async getTotalSurveyResponses(surveyId: number): Promise<number> {
|
||||
const result = await this.drizzle
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(answersSurveys)
|
||||
.where(eq(answersSurveys.surveyId, surveyId));
|
||||
return Number(result[0].count);
|
||||
}
|
||||
|
||||
// ==================================
|
||||
|
||||
private processQuestionStats(questions: any[], answersResult: { answers: any }[]) {
|
||||
// Initialize counters for each question option
|
||||
const questionStats: Array<{ questionId: string; label: string; count: number }> = [];
|
||||
|
||||
// Skip title questions (type: 'title')
|
||||
const surveyQuestions = questions.filter(q => q.type !== 'title');
|
||||
|
||||
// console.log(surveyQuestions);
|
||||
// console.log('Se llamo a processQuestionStats()');
|
||||
|
||||
for (const question of surveyQuestions) {
|
||||
// console.log('Bucle1 se ejecuto');
|
||||
|
||||
// For single choice, multiple choice, and select questions
|
||||
// if (['single_choice', 'multiple_choice', 'select'].includes(question.type)) {
|
||||
const optionCounts: Record<string, number> = {};
|
||||
|
||||
// // Initialize counts for each option
|
||||
// for (const option of question.options) {
|
||||
// optionCounts[option.text] = 0;
|
||||
// }
|
||||
|
||||
// // Count responses for each option
|
||||
// for (const answerObj of answersResult) {
|
||||
// const answer = answerObj.answers.find(a => a.questionId === question.id);
|
||||
|
||||
// if (answer) {
|
||||
// if (Array.isArray(answer.value)) {
|
||||
// // For multiple choice questions
|
||||
// for (const value of answer.value) {
|
||||
// if (optionCounts[value] !== undefined) {
|
||||
// optionCounts[value]++;
|
||||
// }
|
||||
// }
|
||||
// } else {
|
||||
// // For single choice questions
|
||||
// if (optionCounts[answer.value] !== undefined) {
|
||||
// optionCounts[answer.value]++;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// // Convert to the required format
|
||||
// for (const option of question.options) {
|
||||
// questionStats.push({
|
||||
// questionId: String(question.id),
|
||||
// label: option.text,
|
||||
// count: optionCounts[option.value] || 0,
|
||||
// });
|
||||
// }
|
||||
|
||||
if (question.type == 'multiple_choice') {
|
||||
for (const option of question.options) {
|
||||
console.log(option);
|
||||
let count :number = 0
|
||||
// Count responses for each option
|
||||
for (const obj of answersResult) {
|
||||
console.log(obj.answers)
|
||||
const resp = obj.answers.find(a => a.questionId == question.id)
|
||||
const respArray = resp.value.split(",")
|
||||
// console.log();
|
||||
|
||||
if (respArray[option.id] == 'true') {
|
||||
count++
|
||||
}
|
||||
}
|
||||
optionCounts[option.text] = count
|
||||
// Convert to the required format
|
||||
questionStats.push({
|
||||
questionId: String(question.id),
|
||||
label: `${question.question} ${option.text}`,
|
||||
count: optionCounts[option.text] || 0,
|
||||
})
|
||||
}
|
||||
|
||||
} else if (question.type == 'single_choice' || question.type == 'select') {
|
||||
for (const option of question.options) {
|
||||
let count :number = 0
|
||||
// Count responses for each option
|
||||
for (const obj of answersResult) {
|
||||
const resp = obj.answers.find(a => a.questionId == question.id)
|
||||
if (resp.value == option.id) {
|
||||
count++
|
||||
}
|
||||
}
|
||||
optionCounts[option.text] = count
|
||||
// Convert to the required format
|
||||
questionStats.push({
|
||||
questionId: String(question.id),
|
||||
label: `${question.question} ${option.text}`,
|
||||
count: optionCounts[option.text] || 0,
|
||||
})
|
||||
}
|
||||
} else if (question.type === 'simple') {
|
||||
// For simple text questions, just count how many responses
|
||||
let responseCount = 0;
|
||||
|
||||
for (const answerObj of answersResult) {
|
||||
const answer = answerObj.answers.find(a => a.questionId === question.id);
|
||||
if (answer && answer.value) {
|
||||
responseCount++;
|
||||
}
|
||||
}
|
||||
|
||||
questionStats.push({
|
||||
questionId: String(question.id),
|
||||
label: question.question,
|
||||
count: responseCount,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return questionStats;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user