Add cupo_maximo field and related validations to cuestionario and evento modules
This commit is contained in:
@@ -32,7 +32,7 @@ export class CuestionarioController {
|
|||||||
return this.cuestionarioService.create(createCuestionarioDto);
|
return this.cuestionarioService.create(createCuestionarioDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('evento')
|
@Post('withEvento')
|
||||||
@CuestionarioApiDocumentation.ApiCreateWithEvento
|
@CuestionarioApiDocumentation.ApiCreateWithEvento
|
||||||
createCuestionarioEvento(
|
createCuestionarioEvento(
|
||||||
@Body() createCuestionarioDto: CreateCuestionarioEventoDto,
|
@Body() createCuestionarioDto: CreateCuestionarioEventoDto,
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ export class CuestionarioService {
|
|||||||
createCuestionarioDto.secciones?.length || 0;
|
createCuestionarioDto.secciones?.length || 0;
|
||||||
cuestionario.editable = true;
|
cuestionario.editable = true;
|
||||||
cuestionario.id_evento = createCuestionarioDto.id_evento;
|
cuestionario.id_evento = createCuestionarioDto.id_evento;
|
||||||
|
cuestionario.cupo_maximo = createCuestionarioDto.cupo_maximo;
|
||||||
|
|
||||||
const savedCuestionario = await queryRunner.manager.save(cuestionario);
|
const savedCuestionario = await queryRunner.manager.save(cuestionario);
|
||||||
|
|
||||||
@@ -203,9 +204,6 @@ export class CuestionarioService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
evento = await queryRunner.manager.save(Evento, nuevoEvento);
|
evento = await queryRunner.manager.save(Evento, nuevoEvento);
|
||||||
console.log(
|
|
||||||
`Evento creado: ${evento.nombre_evento}, ID: ${evento.id_evento}`,
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
console.log(
|
console.log(
|
||||||
`Evento encontrado: ${evento.nombre_evento}, ID: ${evento.id_evento}`,
|
`Evento encontrado: ${evento.nombre_evento}, ID: ${evento.id_evento}`,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export const ejemploCuestionarioAlumno = {
|
|||||||
fecha_inicio: '2025-08-01T08:00:00',
|
fecha_inicio: '2025-08-01T08:00:00',
|
||||||
fecha_fin: '2025-08-10T23:59:59',
|
fecha_fin: '2025-08-10T23:59:59',
|
||||||
id_tipo_cuestionario: 1,
|
id_tipo_cuestionario: 1,
|
||||||
|
cupo_maximo: 100,
|
||||||
secciones: [
|
secciones: [
|
||||||
{
|
{
|
||||||
titulo: 'Información Personal',
|
titulo: 'Información Personal',
|
||||||
@@ -88,6 +89,7 @@ export const ejemploCuestionarioTrabajador = {
|
|||||||
fecha_inicio: '2025-08-01T08:00:00',
|
fecha_inicio: '2025-08-01T08:00:00',
|
||||||
fecha_fin: '2025-08-10T23:59:59',
|
fecha_fin: '2025-08-10T23:59:59',
|
||||||
id_tipo_cuestionario: 2,
|
id_tipo_cuestionario: 2,
|
||||||
|
cupo_maximo: 100,
|
||||||
secciones: [
|
secciones: [
|
||||||
{
|
{
|
||||||
titulo: 'Información Personal',
|
titulo: 'Información Personal',
|
||||||
|
|||||||
@@ -63,6 +63,15 @@ export class CreateCuestionarioDto {
|
|||||||
@IsNumber()
|
@IsNumber()
|
||||||
id_tipo_cuestionario: number;
|
id_tipo_cuestionario: number;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Cupo máximo de participantes',
|
||||||
|
example: 100,
|
||||||
|
required: false,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
cupo_maximo?: number;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
description: 'Nombre del evento asociado',
|
description: 'Nombre del evento asociado',
|
||||||
example: 'Feria de la Sexualidad',
|
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 { Evento } from 'src/evento/entities/evento.entity';
|
||||||
import { ParticipanteEvento } from 'src/participante_evento/entities/participante_evento.entity';
|
import { ParticipanteEvento } from 'src/participante_evento/entities/participante_evento.entity';
|
||||||
import { TipoCuestionario } from 'src/tipo_cuestionario/entities/tipo_cuestionario.entity';
|
import { TipoCuestionario } from 'src/tipo_cuestionario/entities/tipo_cuestionario.entity';
|
||||||
|
import { CuestionarioRespondido } from 'src/cuestionario_respondido/entities/cuestionario_respondido.entity';
|
||||||
|
|
||||||
@Entity('cuestionario')
|
@Entity('cuestionario')
|
||||||
export class Cuestionario {
|
export class Cuestionario {
|
||||||
@@ -37,6 +38,9 @@ export class Cuestionario {
|
|||||||
@Column({ type: 'datetime' })
|
@Column({ type: 'datetime' })
|
||||||
fecha_fin: Date;
|
fecha_fin: Date;
|
||||||
|
|
||||||
|
@Column({ type: 'int', nullable: true })
|
||||||
|
cupo_maximo?: number;
|
||||||
|
|
||||||
@Column({ type: 'int' })
|
@Column({ type: 'int' })
|
||||||
id_tipo_cuestionario: number;
|
id_tipo_cuestionario: number;
|
||||||
|
|
||||||
@@ -69,4 +73,7 @@ export class Cuestionario {
|
|||||||
(participanteEvento) => participanteEvento.cuestionario,
|
(participanteEvento) => participanteEvento.cuestionario,
|
||||||
)
|
)
|
||||||
inscripciones: ParticipanteEvento[];
|
inscripciones: ParticipanteEvento[];
|
||||||
|
|
||||||
|
@OneToMany(() => CuestionarioRespondido, (cr) => cr.cuestionario)
|
||||||
|
cuestionariosRespondidos: CuestionarioRespondido[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,6 +56,27 @@ export class CuestionarioRespondidoService {
|
|||||||
submitDto.id_cuestionario,
|
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
|
// 2. Buscar o crear participante
|
||||||
let participante = await this.participanteRepository.findOne({
|
let participante = await this.participanteRepository.findOne({
|
||||||
where: { correo: submitDto.correo },
|
where: { correo: submitDto.correo },
|
||||||
@@ -551,7 +572,7 @@ export class CuestionarioRespondidoService {
|
|||||||
return csv;
|
return csv;
|
||||||
} */
|
} */
|
||||||
|
|
||||||
async generarReporteRespuestas(idCuestionario: number): Promise<string> {
|
async generarReporteRespuestas(idCuestionario: number): Promise<string> {
|
||||||
// Buscar el cuestionario para validar que existe
|
// Buscar el cuestionario para validar que existe
|
||||||
const cuestionario = await this.cuestionarioRepository.findOne({
|
const cuestionario = await this.cuestionarioRepository.findOne({
|
||||||
where: { id_cuestionario: idCuestionario },
|
where: { id_cuestionario: idCuestionario },
|
||||||
|
|||||||
@@ -48,14 +48,14 @@ export const createMainDbConfig = async (
|
|||||||
SeccionPregunta,
|
SeccionPregunta,
|
||||||
TipoCuestionario,
|
TipoCuestionario,
|
||||||
TipoPregunta,
|
TipoPregunta,
|
||||||
TipoUser
|
TipoUser,
|
||||||
],
|
],
|
||||||
retryAttempts: 5,
|
retryAttempts: 5,
|
||||||
retryDelay: 3000,
|
retryDelay: 3000,
|
||||||
connectTimeout: 30000,
|
connectTimeout: 30000,
|
||||||
|
|
||||||
synchronize: false,
|
synchronize: configService.get<boolean>('SYNCHRONIZE') || false,
|
||||||
dropSchema: false,
|
dropSchema: configService.get<boolean>('DROP_SCHEMA') || false,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const createAlumnosDbConfig = async (
|
export const createAlumnosDbConfig = async (
|
||||||
|
|||||||
@@ -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[];
|
||||||
|
}
|
||||||
@@ -11,6 +11,8 @@ import { CreateEventoDto } from './dto/create-evento.dto';
|
|||||||
import { UpdateEventoDto } from './dto/update.evento.dto';
|
import { UpdateEventoDto } from './dto/update.evento.dto';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { existsSync, unlinkSync } from 'fs';
|
import { existsSync, unlinkSync } from 'fs';
|
||||||
|
import { plainToInstance } from 'class-transformer';
|
||||||
|
import { EventoConCuestionariosDto } from './dto/outpus.dto';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class EventoService {
|
export class EventoService {
|
||||||
@@ -71,16 +73,36 @@ export class EventoService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getCuestionariosEvento(id_evento: number) {
|
async getCuestionariosEvento(id_evento: number) {
|
||||||
const evento = await this.eventoRepository.findOne({
|
const eventos = await this.eventoRepository.findOne({
|
||||||
where: { id_evento },
|
where: {
|
||||||
relations: ['cuestionarios'], // solo los cuestionarios, sin secciones
|
id_evento: id_evento,
|
||||||
|
},
|
||||||
|
relations: ['cuestionarios', 'cuestionarios.cuestionariosRespondidos'],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!evento) {
|
if (!eventos) {
|
||||||
throw new NotFoundException(`Evento con ID ${id_evento} no encontrado.`);
|
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) {
|
async getEvento(id_evento: number) {
|
||||||
@@ -96,10 +118,30 @@ export class EventoService {
|
|||||||
where: {
|
where: {
|
||||||
fecha_fin: MoreThan(new Date()),
|
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> {
|
async getEventoOrFail(id_evento: number): Promise<Evento> {
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ async function bootstrap() {
|
|||||||
app.useGlobalPipes(
|
app.useGlobalPipes(
|
||||||
new ValidationPipe({
|
new ValidationPipe({
|
||||||
whitelist: true, // Elimina propiedades no decoradas
|
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
|
transform: true, // Transforma los datos recibidos al tipo definido en el DTO
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { Expose } from 'class-transformer';
|
||||||
|
|
||||||
|
export class TipoCuestionarioOutputDto {
|
||||||
|
@Expose()
|
||||||
|
id_tipo_cuestionario: number;
|
||||||
|
|
||||||
|
@Expose()
|
||||||
|
tipo_cuestionario: string;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user