Add cupo_maximo field and related validations to cuestionario and evento modules

This commit is contained in:
miguel
2025-06-23 12:40:38 -06:00
parent 8fe2b6cc72
commit e20a381ba6
11 changed files with 171 additions and 16 deletions
+1 -1
View File
@@ -32,7 +32,7 @@ export class CuestionarioController {
return this.cuestionarioService.create(createCuestionarioDto);
}
@Post('evento')
@Post('withEvento')
@CuestionarioApiDocumentation.ApiCreateWithEvento
createCuestionarioEvento(
@Body() createCuestionarioDto: CreateCuestionarioEventoDto,
+1 -3
View File
@@ -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}`,
@@ -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',
@@ -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',
@@ -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[];
}
@@ -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<string> {
async generarReporteRespuestas(idCuestionario: number): Promise<string> {
// Buscar el cuestionario para validar que existe
const cuestionario = await this.cuestionarioRepository.findOne({
where: { id_cuestionario: idCuestionario },
+3 -3
View File
@@ -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<boolean>('SYNCHRONIZE') || false,
dropSchema: configService.get<boolean>('DROP_SCHEMA') || false,
});
export const createAlumnosDbConfig = async (
+68
View File
@@ -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[];
}
+49 -7
View File
@@ -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<Evento> {
-1
View File
@@ -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
}),
);
+9
View File
@@ -0,0 +1,9 @@
import { Expose } from 'class-transformer';
export class TipoCuestionarioOutputDto {
@Expose()
id_tipo_cuestionario: number;
@Expose()
tipo_cuestionario: string;
}