From e20a381ba679e712bec49a9dfbd06deaec3a900d Mon Sep 17 00:00:00 2001 From: miguel Date: Mon, 23 Jun 2025 12:40:38 -0600 Subject: [PATCH] Add cupo_maximo field and related validations to cuestionario and evento modules --- src/cuestionario/cuestionario.controller.ts | 2 +- src/cuestionario/cuestionario.service.ts | 4 +- .../docs/cuestionario.examples.ts | 2 + .../dto/create-cuestionario.dto.ts | 9 +++ .../entities/cuestionario.entity.ts | 7 ++ .../cuestionario_respondido.service.ts | 23 ++++++- src/db/typeorm.config.ts | 6 +- src/evento/dto/outpus.dto.ts | 68 +++++++++++++++++++ src/evento/evento.service.ts | 56 +++++++++++++-- src/main.ts | 1 - src/tipo_cuestionario/dto/outputs.dto.ts | 9 +++ 11 files changed, 171 insertions(+), 16 deletions(-) create mode 100644 src/evento/dto/outpus.dto.ts create mode 100644 src/tipo_cuestionario/dto/outputs.dto.ts diff --git a/src/cuestionario/cuestionario.controller.ts b/src/cuestionario/cuestionario.controller.ts index c7e5314..ad6c5b4 100644 --- a/src/cuestionario/cuestionario.controller.ts +++ b/src/cuestionario/cuestionario.controller.ts @@ -32,7 +32,7 @@ export class CuestionarioController { return this.cuestionarioService.create(createCuestionarioDto); } - @Post('evento') + @Post('withEvento') @CuestionarioApiDocumentation.ApiCreateWithEvento createCuestionarioEvento( @Body() createCuestionarioDto: CreateCuestionarioEventoDto, diff --git a/src/cuestionario/cuestionario.service.ts b/src/cuestionario/cuestionario.service.ts index 011fb4a..505ebac 100644 --- a/src/cuestionario/cuestionario.service.ts +++ b/src/cuestionario/cuestionario.service.ts @@ -66,6 +66,7 @@ export class CuestionarioService { createCuestionarioDto.secciones?.length || 0; cuestionario.editable = true; cuestionario.id_evento = createCuestionarioDto.id_evento; + cuestionario.cupo_maximo = createCuestionarioDto.cupo_maximo; const savedCuestionario = await queryRunner.manager.save(cuestionario); @@ -203,9 +204,6 @@ export class CuestionarioService { }; evento = await queryRunner.manager.save(Evento, nuevoEvento); - console.log( - `Evento creado: ${evento.nombre_evento}, ID: ${evento.id_evento}`, - ); } else { console.log( `Evento encontrado: ${evento.nombre_evento}, ID: ${evento.id_evento}`, diff --git a/src/cuestionario/docs/cuestionario.examples.ts b/src/cuestionario/docs/cuestionario.examples.ts index 0e4010c..3f43863 100644 --- a/src/cuestionario/docs/cuestionario.examples.ts +++ b/src/cuestionario/docs/cuestionario.examples.ts @@ -11,6 +11,7 @@ export const ejemploCuestionarioAlumno = { fecha_inicio: '2025-08-01T08:00:00', fecha_fin: '2025-08-10T23:59:59', id_tipo_cuestionario: 1, + cupo_maximo: 100, secciones: [ { titulo: 'Información Personal', @@ -88,6 +89,7 @@ export const ejemploCuestionarioTrabajador = { fecha_inicio: '2025-08-01T08:00:00', fecha_fin: '2025-08-10T23:59:59', id_tipo_cuestionario: 2, + cupo_maximo: 100, secciones: [ { titulo: 'Información Personal', diff --git a/src/cuestionario/dto/create-cuestionario.dto.ts b/src/cuestionario/dto/create-cuestionario.dto.ts index eb9c642..f90ee44 100644 --- a/src/cuestionario/dto/create-cuestionario.dto.ts +++ b/src/cuestionario/dto/create-cuestionario.dto.ts @@ -63,6 +63,15 @@ export class CreateCuestionarioDto { @IsNumber() id_tipo_cuestionario: number; + @ApiProperty({ + description: 'Cupo máximo de participantes', + example: 100, + required: false, + }) + @IsOptional() + @IsNumber() + cupo_maximo?: number; + @ApiProperty({ description: 'Nombre del evento asociado', example: 'Feria de la Sexualidad', diff --git a/src/cuestionario/entities/cuestionario.entity.ts b/src/cuestionario/entities/cuestionario.entity.ts index ee2f068..4521691 100644 --- a/src/cuestionario/entities/cuestionario.entity.ts +++ b/src/cuestionario/entities/cuestionario.entity.ts @@ -10,6 +10,7 @@ import { CuestionarioSeccion } from 'src/cuestionario_seccion/entities/cuestiona import { Evento } from 'src/evento/entities/evento.entity'; import { ParticipanteEvento } from 'src/participante_evento/entities/participante_evento.entity'; import { TipoCuestionario } from 'src/tipo_cuestionario/entities/tipo_cuestionario.entity'; +import { CuestionarioRespondido } from 'src/cuestionario_respondido/entities/cuestionario_respondido.entity'; @Entity('cuestionario') export class Cuestionario { @@ -37,6 +38,9 @@ export class Cuestionario { @Column({ type: 'datetime' }) fecha_fin: Date; + @Column({ type: 'int', nullable: true }) + cupo_maximo?: number; + @Column({ type: 'int' }) id_tipo_cuestionario: number; @@ -69,4 +73,7 @@ export class Cuestionario { (participanteEvento) => participanteEvento.cuestionario, ) inscripciones: ParticipanteEvento[]; + + @OneToMany(() => CuestionarioRespondido, (cr) => cr.cuestionario) + cuestionariosRespondidos: CuestionarioRespondido[]; } diff --git a/src/cuestionario_respondido/cuestionario_respondido.service.ts b/src/cuestionario_respondido/cuestionario_respondido.service.ts index df52c7c..ae9fe8e 100644 --- a/src/cuestionario_respondido/cuestionario_respondido.service.ts +++ b/src/cuestionario_respondido/cuestionario_respondido.service.ts @@ -56,6 +56,27 @@ export class CuestionarioRespondidoService { submitDto.id_cuestionario, ); + //1.1 Validar que no se haya rebasado el cupo máximo + if ( + cuestionario.cupo_maximo !== null && + cuestionario.cupo_maximo !== undefined + ) { + const totalRespondidos = + await this.cuestionarioRespondidoRepository.count({ + where: { + cuestionario: { id_cuestionario: submitDto.id_cuestionario }, + }, + }); + + if (totalRespondidos >= cuestionario.cupo_maximo) { + await queryRunner.rollbackTransaction(); + return { + success: false, + message: `El cupo máximo de ${cuestionario.cupo_maximo} participantes ha sido alcanzado para el cuestionario ${cuestionario.nombre_form}.`, + }; + } + } + // 2. Buscar o crear participante let participante = await this.participanteRepository.findOne({ where: { correo: submitDto.correo }, @@ -551,7 +572,7 @@ export class CuestionarioRespondidoService { return csv; } */ - async generarReporteRespuestas(idCuestionario: number): Promise { + async generarReporteRespuestas(idCuestionario: number): Promise { // Buscar el cuestionario para validar que existe const cuestionario = await this.cuestionarioRepository.findOne({ where: { id_cuestionario: idCuestionario }, diff --git a/src/db/typeorm.config.ts b/src/db/typeorm.config.ts index 8748b60..7164f7e 100644 --- a/src/db/typeorm.config.ts +++ b/src/db/typeorm.config.ts @@ -48,14 +48,14 @@ export const createMainDbConfig = async ( SeccionPregunta, TipoCuestionario, TipoPregunta, - TipoUser + TipoUser, ], retryAttempts: 5, retryDelay: 3000, connectTimeout: 30000, - synchronize: false, - dropSchema: false, + synchronize: configService.get('SYNCHRONIZE') || false, + dropSchema: configService.get('DROP_SCHEMA') || false, }); export const createAlumnosDbConfig = async ( diff --git a/src/evento/dto/outpus.dto.ts b/src/evento/dto/outpus.dto.ts new file mode 100644 index 0000000..de1e392 --- /dev/null +++ b/src/evento/dto/outpus.dto.ts @@ -0,0 +1,68 @@ +import { Expose, Type } from 'class-transformer'; +import { TipoCuestionarioOutputDto } from 'src/tipo_cuestionario/dto/outputs.dto'; + +export class CuestionarioConCupoDto { + @Expose() + id_cuestionario: number; + + @Expose() + nombre_form: string; + + @Expose() + descripcion?: string; + + @Expose() + banner: string; + + @Expose() + fecha_inicio: Date; + + @Expose() + fecha_fin: Date; + + @Expose() + cupo_maximo: number | null; + + @Expose() + cupos_usados: number; + + @Expose() + cupos_disponibles: number | null; + + @Expose() + id_evento: number; + + @Expose() + @Type(() => TipoCuestionarioOutputDto) + tipoCuestionario: TipoCuestionarioOutputDto; +} + +export class EventoConCuestionariosDto { + @Expose() + id_evento: number; + + @Expose() + tipo_evento: string; + + @Expose() + nombre_evento: string; + + @Expose() + descripcion_evento: string; + + @Expose() + fecha_inicio: Date; + + @Expose() + fecha_fin: Date; + + @Expose() + asistencias: any; + + @Expose() + banner: string | null; + + @Expose() + @Type(() => CuestionarioConCupoDto) // <- Esto es lo que faltaba + cuestionarios: CuestionarioConCupoDto[]; +} diff --git a/src/evento/evento.service.ts b/src/evento/evento.service.ts index a50ce57..e184d45 100644 --- a/src/evento/evento.service.ts +++ b/src/evento/evento.service.ts @@ -11,6 +11,8 @@ import { CreateEventoDto } from './dto/create-evento.dto'; import { UpdateEventoDto } from './dto/update.evento.dto'; import { join } from 'path'; import { existsSync, unlinkSync } from 'fs'; +import { plainToInstance } from 'class-transformer'; +import { EventoConCuestionariosDto } from './dto/outpus.dto'; @Injectable() export class EventoService { @@ -71,16 +73,36 @@ export class EventoService { } async getCuestionariosEvento(id_evento: number) { - const evento = await this.eventoRepository.findOne({ - where: { id_evento }, - relations: ['cuestionarios'], // solo los cuestionarios, sin secciones + const eventos = await this.eventoRepository.findOne({ + where: { + id_evento: id_evento, + }, + relations: ['cuestionarios', 'cuestionarios.cuestionariosRespondidos'], }); - if (!evento) { + if (!eventos) { throw new NotFoundException(`Evento con ID ${id_evento} no encontrado.`); } - return evento; + const eventoConCupos = { + ...eventos, + cuestionarios: eventos.cuestionarios.map((cuest) => { + const cupoMaximo = cuest.cupo_maximo ?? null; + const usados = cuest.cuestionariosRespondidos?.length || 0; + const disponibles = + cupoMaximo !== null ? Math.max(0, cupoMaximo - usados) : null; + + return { + ...cuest, + cupos_usados: usados, + cupos_disponibles: disponibles, + }; + }), + }; + + return plainToInstance(EventoConCuestionariosDto, eventoConCupos, { + excludeExtraneousValues: true, + }); } async getEvento(id_evento: number) { @@ -96,10 +118,30 @@ export class EventoService { where: { fecha_fin: MoreThan(new Date()), }, - relations: ['cuestionarios'], // solo los cuestionarios, sin secciones + relations: ['cuestionarios', 'cuestionarios.cuestionariosRespondidos'], }); - return eventos; + const eventosConCupos = eventos.map((evento) => { + return { + ...evento, + cuestionarios: evento.cuestionarios.map((cuest) => { + const cupoMaximo = cuest.cupo_maximo ?? null; + const usados = cuest.cuestionariosRespondidos?.length || 0; + const disponibles = + cupoMaximo !== null ? Math.max(0, cupoMaximo - usados) : null; + + return { + ...cuest, + cupos_usados: usados, + cupos_disponibles: disponibles, + }; + }), + }; + }); + + return plainToInstance(EventoConCuestionariosDto, eventosConCupos, { + excludeExtraneousValues: true, + }); } async getEventoOrFail(id_evento: number): Promise { diff --git a/src/main.ts b/src/main.ts index 528f3a9..b72cbda 100644 --- a/src/main.ts +++ b/src/main.ts @@ -14,7 +14,6 @@ async function bootstrap() { app.useGlobalPipes( new ValidationPipe({ whitelist: true, // Elimina propiedades no decoradas - forbidNonWhitelisted: true, // Arroja error si hay propiedades no decoradas transform: true, // Transforma los datos recibidos al tipo definido en el DTO }), ); diff --git a/src/tipo_cuestionario/dto/outputs.dto.ts b/src/tipo_cuestionario/dto/outputs.dto.ts new file mode 100644 index 0000000..1f225ee --- /dev/null +++ b/src/tipo_cuestionario/dto/outputs.dto.ts @@ -0,0 +1,9 @@ +import { Expose } from 'class-transformer'; + +export class TipoCuestionarioOutputDto { + @Expose() + id_tipo_cuestionario: number; + + @Expose() + tipo_cuestionario: string; +} \ No newline at end of file