eithan #14
+2
-2
@@ -1,5 +1,5 @@
|
||||
# Etapa de desarrollo para ejecutar en watch mode
|
||||
FROM node:20-alpine
|
||||
FROM node:22-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@@ -12,7 +12,7 @@ COPY . .
|
||||
RUN npm run build
|
||||
|
||||
# Puerto expuesto
|
||||
EXPOSE 4200
|
||||
EXPOSE 4204
|
||||
|
||||
# Comando para iniciar en modo desarrollo
|
||||
CMD ["npm", "run", "start:dev"]
|
||||
|
||||
@@ -97,3 +97,25 @@ Nest is an MIT-licensed open source project. It can grow thanks to the sponsors
|
||||
## License
|
||||
|
||||
Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE).
|
||||
|
||||
|
||||
|
||||
# usar validadores en el front end
|
||||
|
||||
import { validarRespuesta, FabricaValidadores } from
|
||||
'./validaciones/validador';
|
||||
|
||||
// To validate a single response
|
||||
const respuesta = 'user@example.com';
|
||||
const tipoValidacion = 'correo';
|
||||
const resultado = validarRespuesta(respuesta, tipoValidacion);
|
||||
|
||||
if (resultado.valido) {
|
||||
// proceed
|
||||
} else {
|
||||
// show error message: resultado.mensaje
|
||||
}
|
||||
|
||||
// Or directly use the validators
|
||||
const validadorCorreo = new ValidadorCorreo();
|
||||
const resultadoCorreo = validadorCorreo.validar('user@example.com');
|
||||
+15
-1
@@ -24,10 +24,24 @@ services:
|
||||
- .:/app
|
||||
- /app/node_modules
|
||||
ports:
|
||||
- "4200:4200"
|
||||
- "4204:4204"
|
||||
depends_on:
|
||||
- mariadb
|
||||
restart: unless-stopped
|
||||
|
||||
|
||||
|
||||
front:
|
||||
build: ../formularios_front
|
||||
container_name: formularios_front
|
||||
restart: always
|
||||
depends_on:
|
||||
- api
|
||||
env_file:
|
||||
- ../formularios_front/.env
|
||||
ports:
|
||||
- "4202:3000"
|
||||
|
||||
|
||||
volumes:
|
||||
mariadb_data:
|
||||
|
||||
@@ -23,6 +23,7 @@ import { QrModule } from './qr/qr.module';
|
||||
import { AdministradorModule } from './administrador/administrador.module';
|
||||
import { AsistenciaModule } from './asistencia/asistencia.module';
|
||||
import { ParticipanteEventoModule } from './participante_evento/participante_evento.module';
|
||||
import { ValidacionesModule } from './validaciones/validaciones.module';
|
||||
|
||||
|
||||
@Module({
|
||||
@@ -77,6 +78,7 @@ import { ParticipanteEventoModule } from './participante_evento/participante_eve
|
||||
AdministradorModule,
|
||||
AsistenciaModule,
|
||||
ParticipanteEventoModule,
|
||||
ValidacionesModule,
|
||||
],
|
||||
|
||||
controllers: [AppController],
|
||||
|
||||
@@ -11,7 +11,7 @@ export class CuestionarioApiDocumentation {
|
||||
static ApiGetFormulario = applyDecorators(
|
||||
ApiOperation({
|
||||
summary: 'Obtener formulario completo',
|
||||
description: 'Devuelve el cuestionario completo con la misma estructura que se usa para crearlo'
|
||||
description: 'Devuelve el cuestionario completo con la misma estructura que se usa para crearlo, incluyendo el atributo validacion para las preguntas que tengan reglas de validación específicas'
|
||||
}),
|
||||
ApiParam({
|
||||
name: 'id',
|
||||
@@ -22,7 +22,7 @@ export class CuestionarioApiDocumentation {
|
||||
}),
|
||||
ApiResponse({
|
||||
status: 200,
|
||||
description: 'Formulario completo con todas sus secciones, preguntas y opciones',
|
||||
description: 'Formulario completo con todas sus secciones, preguntas y opciones. Las preguntas incluyen el atributo "validacion" si tienen reglas de validación específicas (como "correo", "cuenta_alumno", "telefono", "nombre", etc.)',
|
||||
type: FormularioDto,
|
||||
content: {
|
||||
'application/json': {
|
||||
@@ -38,22 +38,22 @@ export class CuestionarioApiDocumentation {
|
||||
static ApiCreate = applyDecorators(
|
||||
ApiOperation({
|
||||
summary: 'Crear un nuevo cuestionario',
|
||||
description: 'Crea un nuevo cuestionario con secciones, preguntas y opciones'
|
||||
description: 'Crea un nuevo cuestionario con secciones, preguntas y opciones, incluyendo validaciones personalizadas'
|
||||
}),
|
||||
ApiBody({
|
||||
description: 'Datos del cuestionario a crear',
|
||||
description: 'Datos del cuestionario a crear. Para las preguntas de tipo "Abierta", se puede especificar un tipo de validación usando la propiedad "validacion" con valores como: "correo", "cuenta_alumno", "telefono", "nombre", "entero", "decimal", etc.',
|
||||
type: FormularioDto,
|
||||
examples: {
|
||||
feriaSexualidad: {
|
||||
summary: 'Ejemplo de cuestionario para feria de sexualidad',
|
||||
description: 'Cuestionario con secciones, preguntas y opciones para feria de sexualidad',
|
||||
description: 'Cuestionario con secciones, preguntas y opciones para feria de sexualidad, ahora con validaciones específicas para cada tipo de campo (correo, nombre, cuenta_alumno, etc.)',
|
||||
value: feriaSexualidad
|
||||
}
|
||||
}
|
||||
}),
|
||||
ApiResponse({
|
||||
status: 201,
|
||||
description: 'Cuestionario creado correctamente',
|
||||
description: 'Cuestionario creado correctamente. Los campos con la propiedad "validacion" se validarán según su tipo: "correo", "cuenta_alumno", "telefono", "nombre", "entero", "decimal", etc.',
|
||||
schema: {
|
||||
properties: {
|
||||
success: { type: 'boolean', example: true },
|
||||
|
||||
@@ -139,6 +139,7 @@ export class CuestionarioService {
|
||||
pregunta.id_tipo_pregunta = tipoPregunta.id_tipo;
|
||||
pregunta.id_opcion_dependiente = preguntaDto.id_opcion_dependiente || undefined;
|
||||
pregunta.contador_opcion = preguntaDto.opciones?.length || 0;
|
||||
pregunta.validacion = preguntaDto.validacion || undefined;
|
||||
|
||||
const savedPregunta = await queryRunner.manager.save(pregunta);
|
||||
|
||||
@@ -301,6 +302,7 @@ export class CuestionarioService {
|
||||
obligatoria: pregunta.obligatoria,
|
||||
id_tipo_pregunta: pregunta.id_tipo_pregunta,
|
||||
id_opcion_dependiente: pregunta.id_opcion_dependiente,
|
||||
validacion: pregunta.validacion,
|
||||
tipo_pregunta: {
|
||||
id_tipo: tipoPregunta.id_tipo,
|
||||
tipo_pregunta: tipoPregunta.tipo_pregunta
|
||||
|
||||
@@ -75,6 +75,7 @@ export const FormularioFeriaSchema = {
|
||||
id_tipo_pregunta: 2,
|
||||
id_opcion_dependiente: null,
|
||||
limite: 250, // Límite de caracteres para respuestas de texto
|
||||
validacion: 'correo', // Validación de formato de correo electrónico
|
||||
tipo_pregunta: {
|
||||
id_tipo: 2,
|
||||
tipo_pregunta: 'Abierta'
|
||||
@@ -93,6 +94,7 @@ export const FormularioFeriaSchema = {
|
||||
id_tipo_pregunta: 2,
|
||||
id_opcion_dependiente: 90001, // Dependiente de la respuesta "Sí" en la pregunta anterior
|
||||
limite: 250, // Límite de caracteres para respuestas de texto
|
||||
validacion: 'cuenta_alumno', // Validación de formato de número de cuenta
|
||||
tipo_pregunta: {
|
||||
id_tipo: 2,
|
||||
tipo_pregunta: 'Abierta'
|
||||
|
||||
@@ -12,6 +12,7 @@ import { RespuestaParticipanteAbierta } from '../respuesta_participante_abierta/
|
||||
import { RespuestaParticipanteCerrada } from '../respuesta_participante_cerrada/entities/respuesta_participante_cerrada.entity';
|
||||
import { PreguntaOpcion } from '../pregunta_opcion/entities/pregunta_opcion.entity';
|
||||
import { Opcion } from '../opcion/entities/opcion.entity';
|
||||
import { ValidacionesModule } from '../validaciones/validaciones.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -26,7 +27,8 @@ import { Opcion } from '../opcion/entities/opcion.entity';
|
||||
Opcion
|
||||
]),
|
||||
CuestionarioModule,
|
||||
ParticipanteModule
|
||||
ParticipanteModule,
|
||||
ValidacionesModule
|
||||
],
|
||||
controllers: [CuestionarioRespondidoController],
|
||||
providers: [CuestionarioRespondidoService],
|
||||
|
||||
@@ -12,6 +12,7 @@ import { RespuestaParticipanteAbierta } from '../respuesta_participante_abierta/
|
||||
import { RespuestaParticipanteCerrada } from '../respuesta_participante_cerrada/entities/respuesta_participante_cerrada.entity';
|
||||
import { PreguntaOpcion } from '../pregunta_opcion/entities/pregunta_opcion.entity';
|
||||
import { Opcion } from '../opcion/entities/opcion.entity';
|
||||
import { ValidadorRespuestasService } from '../validaciones/validador-respuestas.service';
|
||||
import axios from 'axios';
|
||||
import * as QRCode from 'qrcode';
|
||||
|
||||
@@ -35,6 +36,7 @@ export class CuestionarioRespondidoService {
|
||||
@InjectRepository(Opcion)
|
||||
private opcionRepository: Repository<Opcion>,
|
||||
private dataSource: DataSource,
|
||||
private validadorRespuestasService: ValidadorRespuestasService,
|
||||
) {}
|
||||
|
||||
create(createCuestionarioRespondidoDto: CreateCuestionarioRespondidoDto) {
|
||||
@@ -90,6 +92,18 @@ export class CuestionarioRespondidoService {
|
||||
throw new NotFoundException(`Pregunta con ID ${respuestaDto.id_pregunta} no encontrada`);
|
||||
}
|
||||
|
||||
// Si la pregunta tiene una validación configurada y es una respuesta abierta, validarla
|
||||
if (pregunta.validacion && typeof respuestaDto.valor === 'string') {
|
||||
const resultadoValidacion = this.validadorRespuestasService.validarRespuesta(
|
||||
respuestaDto.valor,
|
||||
pregunta.validacion
|
||||
);
|
||||
|
||||
if (!resultadoValidacion.valido) {
|
||||
throw new BadRequestException(`Validación fallida en pregunta ${pregunta.id_pregunta}: ${resultadoValidacion.mensaje}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Determinar tipo de pregunta por ID
|
||||
const esPreguntaAbierta = pregunta.id_tipo_pregunta === 2;
|
||||
|
||||
|
||||
@@ -1,7 +1,22 @@
|
||||
import { IsEmail } from "class-validator";
|
||||
import { IsEmail, IsNotEmpty, IsNumber } from "class-validator";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
|
||||
export class CreateParticipanteDto {
|
||||
@IsEmail()
|
||||
correo: string
|
||||
id_tipo_user: number
|
||||
@ApiProperty({
|
||||
description: 'Correo electrónico del participante',
|
||||
example: 'usuario@ejemplo.com',
|
||||
required: true
|
||||
})
|
||||
@IsEmail({}, { message: 'El correo electrónico debe tener un formato válido' })
|
||||
@IsNotEmpty({ message: 'El correo electrónico es requerido' })
|
||||
correo: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'ID del tipo de usuario',
|
||||
example: 1,
|
||||
required: true
|
||||
})
|
||||
@IsNumber({}, { message: 'El ID del tipo de usuario debe ser un número' })
|
||||
@IsNotEmpty({ message: 'El ID del tipo de usuario es requerido' })
|
||||
id_tipo_user: number;
|
||||
}
|
||||
@@ -1,3 +1,22 @@
|
||||
import { IsEmail, IsOptional, IsNumber } from "class-validator";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
|
||||
export class UpdateParticipanteDto {
|
||||
correo: string
|
||||
@ApiProperty({
|
||||
description: 'Nuevo correo electrónico del participante',
|
||||
example: 'nuevo_correo@ejemplo.com',
|
||||
required: false
|
||||
})
|
||||
@IsEmail({}, { message: 'El correo electrónico debe tener un formato válido' })
|
||||
@IsOptional()
|
||||
correo?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Nuevo ID del tipo de usuario',
|
||||
example: 2,
|
||||
required: false
|
||||
})
|
||||
@IsNumber({}, { message: 'El ID del tipo de usuario debe ser un número' })
|
||||
@IsOptional()
|
||||
id_tipo_user?: number;
|
||||
}
|
||||
@@ -3,58 +3,40 @@ import { Participante } from './participante.entity';
|
||||
import { ParticipanteService } from './participante.service';
|
||||
import { CreateParticipanteDto } from './dto/create-participante.dto';
|
||||
import { UpdateParticipanteDto } from './dto/update.participante.dto';
|
||||
import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiBody } from '@nestjs/swagger';
|
||||
import { ParticipanteApiDocumentation } from './participante.documentation';
|
||||
|
||||
@ApiTags('Participantes') // Agrupa los endpoints en Swagger
|
||||
@ParticipanteApiDocumentation.ApiController
|
||||
@Controller('participante')
|
||||
export class ParticipanteController {
|
||||
constructor(private participanteService: ParticipanteService) {}
|
||||
|
||||
@ApiOperation({ summary: 'Obtener todos los participantes' })
|
||||
@ApiResponse({ status: 200, description: 'Lista de participantes obtenida correctamente.' })
|
||||
@ParticipanteApiDocumentation.ApiGetAll
|
||||
@Get()
|
||||
getParticipantes(): Promise<Participante[]> {
|
||||
return this.participanteService.getParticipantes()
|
||||
return this.participanteService.getParticipantes();
|
||||
}
|
||||
|
||||
@ParticipanteApiDocumentation.ApiGetOne
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: 'Obtener un participante por ID' })
|
||||
@ApiParam({ name: 'id', description: 'ID del participante', example: 1 })
|
||||
@ApiResponse({ status: 200, description: 'Participante obtenido correctamente.' })
|
||||
@ApiResponse({ status: 404, description: 'Participante no encontrado.' })
|
||||
getParticipante(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.participanteService.getParticipante(id);
|
||||
}
|
||||
|
||||
@ParticipanteApiDocumentation.ApiCreate
|
||||
@Post()
|
||||
@ApiOperation({ summary: 'Registrar un nuevo participante' })
|
||||
@ApiBody({
|
||||
description: 'Datos del participante a registrar',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
correo: { type: 'string', example: 'user@example.com' },
|
||||
id_tipo_user: { type: 'integer', example: 2 }
|
||||
}
|
||||
}
|
||||
})
|
||||
@ApiResponse({ status: 201, description: 'Participante registrado exitosamente.' })
|
||||
@ApiResponse({ status: 400, description: 'Datos inválidos.' })
|
||||
createParticipante(@Body() newParticipante: CreateParticipanteDto) {
|
||||
return this.participanteService.createParticipante(newParticipante);
|
||||
}
|
||||
|
||||
@ParticipanteApiDocumentation.ApiRemove
|
||||
@Delete(':id')
|
||||
deleteParticipante(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.participanteService.deleteParticipante(id)
|
||||
return this.participanteService.deleteParticipante(id);
|
||||
}
|
||||
|
||||
|
||||
@ParticipanteApiDocumentation.ApiUpdate
|
||||
@Patch(':id')
|
||||
updateParticipante(@Param('correo') id: number, @Body() participante: UpdateParticipanteDto) {
|
||||
updateParticipante(@Param('id', ParseIntPipe) id: number, @Body() participante: UpdateParticipanteDto) {
|
||||
return this.participanteService.updateParticipante(id, participante);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
import { ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { applyDecorators } from '@nestjs/common';
|
||||
|
||||
export class ParticipanteApiDocumentation {
|
||||
// Decorador para toda la clase del controlador
|
||||
static ApiController = ApiTags('Participantes');
|
||||
|
||||
// Documentación para crear un participante
|
||||
static ApiCreate = applyDecorators(
|
||||
ApiOperation({
|
||||
summary: 'Registrar un nuevo participante',
|
||||
description: 'Crea un nuevo registro de participante en el sistema'
|
||||
}),
|
||||
ApiBody({
|
||||
description: 'Datos del participante a registrar',
|
||||
schema: {
|
||||
type: 'object',
|
||||
required: ['correo', 'id_tipo_user'],
|
||||
properties: {
|
||||
correo: {
|
||||
type: 'string',
|
||||
format: 'email',
|
||||
example: 'usuario@ejemplo.com',
|
||||
description: 'Correo electrónico del participante'
|
||||
},
|
||||
id_tipo_user: {
|
||||
type: 'integer',
|
||||
example: 1,
|
||||
description: 'ID del tipo de usuario'
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
ApiResponse({
|
||||
status: 201,
|
||||
description: 'Participante registrado exitosamente',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id_participante: { type: 'number', example: 1 },
|
||||
correo: { type: 'string', example: 'usuario@ejemplo.com' },
|
||||
id_tipo_user: { type: 'number', example: 1 }
|
||||
}
|
||||
}
|
||||
}),
|
||||
ApiResponse({ status: 400, description: 'Datos del participante inválidos' }),
|
||||
ApiResponse({ status: 409, description: 'El participante ya existe' })
|
||||
);
|
||||
|
||||
// Documentación para obtener todos los participantes
|
||||
static ApiGetAll = applyDecorators(
|
||||
ApiOperation({
|
||||
summary: 'Obtener todos los participantes',
|
||||
description: 'Retorna una lista de todos los participantes registrados'
|
||||
}),
|
||||
ApiResponse({
|
||||
status: 200,
|
||||
description: 'Lista de participantes obtenida correctamente',
|
||||
schema: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id_participante: { type: 'number', example: 1 },
|
||||
correo: { type: 'string', example: 'usuario@ejemplo.com' },
|
||||
id_tipo_user: { type: 'number', example: 1 },
|
||||
tipo_user: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id_tipo_user: { type: 'number', example: 1 },
|
||||
tipo_user: { type: 'string', example: 'Estudiante' }
|
||||
}
|
||||
},
|
||||
participanteEventos: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id_participante_evento: { type: 'number', example: 1 },
|
||||
id_participante: { type: 'number', example: 1 },
|
||||
id_evento: { type: 'number', example: 1 },
|
||||
fecha_inscripcion: { type: 'string', format: 'date-time', example: '2025-04-01T10:00:00Z' },
|
||||
estatus: { type: 'boolean', example: true }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Documentación para obtener un participante por ID
|
||||
static ApiGetOne = applyDecorators(
|
||||
ApiOperation({
|
||||
summary: 'Obtener un participante por ID',
|
||||
description: 'Retorna los datos de un participante específico según su ID'
|
||||
}),
|
||||
ApiParam({
|
||||
name: 'id',
|
||||
description: 'ID del participante',
|
||||
type: 'number',
|
||||
example: 1
|
||||
}),
|
||||
ApiResponse({
|
||||
status: 200,
|
||||
description: 'Participante obtenido correctamente',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id_participante: { type: 'number', example: 1 },
|
||||
correo: { type: 'string', example: 'usuario@ejemplo.com' },
|
||||
id_tipo_user: { type: 'number', example: 1 },
|
||||
tipo_user: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id_tipo_user: { type: 'number', example: 1 },
|
||||
tipo_user: { type: 'string', example: 'Estudiante' }
|
||||
}
|
||||
},
|
||||
participanteEventos: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id_participante_evento: { type: 'number', example: 1 },
|
||||
id_participante: { type: 'number', example: 1 },
|
||||
id_evento: { type: 'number', example: 1 },
|
||||
fecha_inscripcion: { type: 'string', format: 'date-time', example: '2025-04-01T10:00:00Z' },
|
||||
estatus: { type: 'boolean', example: true }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
ApiResponse({ status: 404, description: 'Participante no encontrado' })
|
||||
);
|
||||
|
||||
// Documentación para actualizar un participante
|
||||
static ApiUpdate = applyDecorators(
|
||||
ApiOperation({
|
||||
summary: 'Actualizar datos de un participante',
|
||||
description: 'Actualiza la información de un participante existente'
|
||||
}),
|
||||
ApiParam({
|
||||
name: 'id',
|
||||
description: 'ID del participante a actualizar',
|
||||
type: 'number',
|
||||
example: 1
|
||||
}),
|
||||
ApiBody({
|
||||
description: 'Datos a actualizar del participante',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
correo: {
|
||||
type: 'string',
|
||||
format: 'email',
|
||||
example: 'nuevo_correo@ejemplo.com',
|
||||
description: 'Nuevo correo electrónico del participante'
|
||||
},
|
||||
id_tipo_user: {
|
||||
type: 'integer',
|
||||
example: 2,
|
||||
description: 'Nuevo tipo de usuario'
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
ApiResponse({
|
||||
status: 200,
|
||||
description: 'Participante actualizado correctamente',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id_participante: { type: 'number', example: 1 },
|
||||
correo: { type: 'string', example: 'nuevo_correo@ejemplo.com' },
|
||||
id_tipo_user: { type: 'number', example: 2 }
|
||||
}
|
||||
}
|
||||
}),
|
||||
ApiResponse({ status: 400, description: 'Datos de actualización inválidos' }),
|
||||
ApiResponse({ status: 404, description: 'Participante no encontrado' })
|
||||
);
|
||||
|
||||
// Documentación para eliminar un participante
|
||||
static ApiRemove = applyDecorators(
|
||||
ApiOperation({
|
||||
summary: 'Eliminar un participante',
|
||||
description: 'Elimina permanentemente un participante por su ID'
|
||||
}),
|
||||
ApiParam({
|
||||
name: 'id',
|
||||
description: 'ID del participante a eliminar',
|
||||
type: 'number',
|
||||
example: 1
|
||||
}),
|
||||
ApiResponse({
|
||||
status: 200,
|
||||
description: 'Participante eliminado correctamente',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
affected: { type: 'number', example: 1 }
|
||||
}
|
||||
}
|
||||
}),
|
||||
ApiResponse({ status: 404, description: 'Participante no encontrado' })
|
||||
);
|
||||
}
|
||||
@@ -1,62 +1,99 @@
|
||||
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
||||
import { BadRequestException, ConflictException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Participante } from './participante.entity';
|
||||
import { Repository } from 'typeorm';
|
||||
import { CreateParticipanteDto } from './dto/create-participante.dto';
|
||||
//import { UpdateAdminDto } from 'src/admin/dto/update.admin.dto';
|
||||
import { UpdateParticipanteDto } from './dto/update.participante.dto';
|
||||
|
||||
@Injectable()
|
||||
export class ParticipanteService {
|
||||
|
||||
constructor(
|
||||
@InjectRepository(Participante) private participanteRepository: Repository<Participante>
|
||||
) {}
|
||||
|
||||
async createParticipante(participante: CreateParticipanteDto) {
|
||||
/**
|
||||
* Crea un nuevo participante
|
||||
* @param participante Datos del participante a crear
|
||||
* @returns El participante creado
|
||||
* @throws ConflictException si ya existe un participante con el mismo correo
|
||||
*/
|
||||
async createParticipante(participante: CreateParticipanteDto): Promise<Participante> {
|
||||
// Verificar si ya existe un participante con el mismo correo
|
||||
const participanteFound = await this.participanteRepository.findOne({
|
||||
where: {
|
||||
correo: participante.correo
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
if (participanteFound)
|
||||
return new HttpException('Participante already exists', HttpStatus.CONFLICT)
|
||||
if (participanteFound) {
|
||||
throw new ConflictException(`Ya existe un participante con el correo ${participante.correo}`);
|
||||
}
|
||||
|
||||
return this.participanteRepository.save(participante)
|
||||
const nuevoParticipante = this.participanteRepository.create(participante);
|
||||
return this.participanteRepository.save(nuevoParticipante);
|
||||
}
|
||||
|
||||
getParticipantes() {
|
||||
/**
|
||||
* Obtiene todos los participantes
|
||||
* @returns Lista de participantes con sus relaciones
|
||||
*/
|
||||
async getParticipantes(): Promise<Participante[]> {
|
||||
return this.participanteRepository.find({
|
||||
relations: ['tipo_user', 'participanteEventos']
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
async getParticipante(id_participante: number) {
|
||||
/**
|
||||
* Obtiene un participante por su ID
|
||||
* @param id_participante ID del participante a buscar
|
||||
* @returns El participante encontrado con sus relaciones
|
||||
* @throws NotFoundException si no se encuentra el participante
|
||||
*/
|
||||
async getParticipante(id_participante: number): Promise<Participante> {
|
||||
const participanteFound = await this.participanteRepository.findOne({
|
||||
where: {
|
||||
id_participante
|
||||
},
|
||||
relations: ['tipo_user', 'participanteEventos']
|
||||
})
|
||||
});
|
||||
|
||||
if (!participanteFound)
|
||||
return new HttpException('Participante not found', HttpStatus.NOT_FOUND);
|
||||
if (!participanteFound) {
|
||||
throw new NotFoundException(`Participante con ID ${id_participante} no encontrado`);
|
||||
}
|
||||
|
||||
return participanteFound;
|
||||
}
|
||||
|
||||
/**
|
||||
* Elimina un participante por su ID
|
||||
* @param id_participante ID del participante a eliminar
|
||||
* @returns Resultado de la eliminación
|
||||
* @throws NotFoundException si no se encuentra el participante
|
||||
*/
|
||||
async deleteParticipante(id_participante: number) {
|
||||
const result = await this.participanteRepository.delete({ id_participante })
|
||||
const result = await this.participanteRepository.delete({ id_participante });
|
||||
|
||||
if (result.affected === 0) {
|
||||
return new HttpException('Participante not found', HttpStatus.NOT_FOUND);
|
||||
throw new NotFoundException(`Participante con ID ${id_participante} no encontrado`);
|
||||
}
|
||||
|
||||
return result;
|
||||
return {
|
||||
success: true,
|
||||
message: `Participante con ID ${id_participante} eliminado correctamente`,
|
||||
affected: result.affected
|
||||
};
|
||||
}
|
||||
|
||||
async updateParticipante(id_participante: number, participante: UpdateParticipanteDto) {
|
||||
/**
|
||||
* Actualiza los datos de un participante
|
||||
* @param id_participante ID del participante a actualizar
|
||||
* @param participante Datos actualizados del participante
|
||||
* @returns El participante actualizado
|
||||
* @throws NotFoundException si no se encuentra el participante
|
||||
* @throws BadRequestException si los datos son inválidos
|
||||
*/
|
||||
async updateParticipante(id_participante: number, participante: UpdateParticipanteDto): Promise<Participante> {
|
||||
// Verificar si el participante existe
|
||||
const participanteFound = await this.participanteRepository.findOne({
|
||||
where: {
|
||||
id_participante
|
||||
@@ -64,11 +101,24 @@ export class ParticipanteService {
|
||||
});
|
||||
|
||||
if (!participanteFound) {
|
||||
return new HttpException('Participante not found', HttpStatus.NOT_FOUND)
|
||||
throw new NotFoundException(`Participante con ID ${id_participante} no encontrado`);
|
||||
}
|
||||
|
||||
const updateParticipante = Object.assign(participanteFound, participante)
|
||||
return this.participanteRepository.save(updateParticipante)
|
||||
}
|
||||
// Si se está actualizando el correo, verificar que no exista otro participante con ese correo
|
||||
if (participante.correo && participante.correo !== participanteFound.correo) {
|
||||
const existingParticipante = await this.participanteRepository.findOne({
|
||||
where: {
|
||||
correo: participante.correo
|
||||
}
|
||||
});
|
||||
|
||||
if (existingParticipante && existingParticipante.id_participante !== id_participante) {
|
||||
throw new ConflictException(`Ya existe otro participante con el correo ${participante.correo}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Actualizar el participante
|
||||
const updateParticipante = Object.assign(participanteFound, participante);
|
||||
return this.participanteRepository.save(updateParticipante);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,22 +3,92 @@ import { ParticipanteEventoService } from './participante_evento.service';
|
||||
import { ParticipanteEvento } from './participante_evento.entity';
|
||||
import { CreateParticipanteEventoDto } from './dto/create-participante_evento.dto';
|
||||
import { UpdateParticipanteEventoDto } from './dto/update.participante_evento.dto';
|
||||
import { ApiTags, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger';
|
||||
|
||||
@ApiTags('Participantes-Eventos')
|
||||
@Controller('participante-evento')
|
||||
export class ParticipanteEventoController {
|
||||
|
||||
constructor(private participanteEventoService: ParticipanteEventoService) {}
|
||||
|
||||
@Get()
|
||||
getParticipantesEvento(): Promise<ParticipanteEvento[]> {
|
||||
return this.participanteEventoService.getParticipantesEvento()
|
||||
@ApiOperation({ summary: 'Obtener participantes por evento' })
|
||||
@ApiParam({ name: 'idEvento', description: 'ID del evento', type: 'number' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Lista de participantes que están registrados en el evento',
|
||||
schema: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id_participante_evento: { type: 'number' },
|
||||
id_participante: { type: 'number' },
|
||||
id_evento: { type: 'number' },
|
||||
fecha_inscripcion: { type: 'string', format: 'date-time' },
|
||||
estatus: { type: 'boolean' },
|
||||
fecha_asistencia: { type: 'string', format: 'date-time', nullable: true },
|
||||
participante: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id_participante: { type: 'number' },
|
||||
correo: { type: 'string' },
|
||||
id_tipo_user: { type: 'number' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@ApiResponse({ status: 404, description: 'Evento no encontrado' })
|
||||
@Get('evento/:idEvento')
|
||||
getParticipantesPorEvento(@Param('idEvento', ParseIntPipe) idEvento: number) {
|
||||
return this.participanteEventoService.getParticipantesPorEvento(idEvento);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
getParticipanteEvento(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.participanteEventoService.getParticipanteEvento(id)
|
||||
@ApiOperation({ summary: 'Obtener eventos por participante' })
|
||||
@ApiParam({ name: 'idParticipante', description: 'ID del participante', type: 'number' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Lista de eventos en los que está registrado el participante',
|
||||
schema: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id_participante_evento: { type: 'number' },
|
||||
id_participante: { type: 'number' },
|
||||
id_evento: { type: 'number' },
|
||||
fecha_inscripcion: { type: 'string', format: 'date-time' },
|
||||
estatus: { type: 'boolean' },
|
||||
fecha_asistencia: { type: 'string', format: 'date-time', nullable: true },
|
||||
evento: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id_evento: { type: 'number' },
|
||||
nombre_evento: { type: 'string' },
|
||||
tipo_evento: { type: 'string' },
|
||||
fecha_inicio: { type: 'string', format: 'date-time' },
|
||||
fecha_fin: { type: 'string', format: 'date-time' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@ApiResponse({ status: 404, description: 'Participante no encontrado' })
|
||||
@Get('participante/:idParticipante')
|
||||
getEventosPorParticipante(@Param('idParticipante', ParseIntPipe) idParticipante: number) {
|
||||
return this.participanteEventoService.getEventosPorParticipante(idParticipante);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: 'Obtener un registro específico por IDs de participante y evento' })
|
||||
@ApiParam({ name: 'idParticipante', description: 'ID del participante', type: 'number' })
|
||||
@ApiParam({ name: 'idEvento', description: 'ID del evento', type: 'number' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Registro participante-evento obtenido correctamente'
|
||||
})
|
||||
@ApiResponse({ status: 404, description: 'Registro no encontrado' })
|
||||
@Get(':idParticipante/:idEvento')
|
||||
getParticipanteEventoEspecifico(
|
||||
@Param('idParticipante', ParseIntPipe) idParticipante: number,
|
||||
@@ -27,11 +97,61 @@ export class ParticipanteEventoController {
|
||||
return this.participanteEventoService.getParticipanteEventoEspecifico(idParticipante, idEvento)
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: 'Obtener un registro participante-evento por ID' })
|
||||
@ApiParam({ name: 'id', description: 'ID del participante', type: 'number' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Registro participante-evento obtenido correctamente'
|
||||
})
|
||||
@ApiResponse({ status: 404, description: 'Registro no encontrado' })
|
||||
@Get(':id')
|
||||
getParticipanteEvento(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.participanteEventoService.getParticipanteEvento(id)
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: 'Obtener todos los registros de participante-evento' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Lista de todos los registros participante-evento con sus relaciones',
|
||||
schema: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id_participante_evento: { type: 'number' },
|
||||
id_participante: { type: 'number' },
|
||||
id_evento: { type: 'number' },
|
||||
fecha_inscripcion: { type: 'string', format: 'date-time' },
|
||||
estatus: { type: 'boolean' },
|
||||
fecha_asistencia: { type: 'string', format: 'date-time', nullable: true }
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@Get()
|
||||
getParticipantesEvento(): Promise<ParticipanteEvento[]> {
|
||||
return this.participanteEventoService.getParticipantesEvento()
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: 'Registrar un participante en un evento' })
|
||||
@ApiResponse({
|
||||
status: 201,
|
||||
description: 'Participante registrado en el evento correctamente'
|
||||
})
|
||||
@ApiResponse({ status: 409, description: 'El participante ya está registrado en este evento' })
|
||||
@Post()
|
||||
createParticipanteEvento(@Body() newParticipanteEvento: CreateParticipanteEventoDto) {
|
||||
return this.participanteEventoService.createParticipanteEvento(newParticipanteEvento)
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: 'Registrar asistencia de un participante a un evento' })
|
||||
@ApiParam({ name: 'idParticipante', description: 'ID del participante', type: 'number' })
|
||||
@ApiParam({ name: 'idEvento', description: 'ID del evento', type: 'number' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Asistencia registrada correctamente'
|
||||
})
|
||||
@ApiResponse({ status: 404, description: 'Registro no encontrado' })
|
||||
@Post('asistencia/:idParticipante/:idEvento')
|
||||
registrarAsistencia(
|
||||
@Param('idParticipante', ParseIntPipe) idParticipante: number,
|
||||
@@ -40,11 +160,25 @@ export class ParticipanteEventoController {
|
||||
return this.participanteEventoService.registrarAsistencia(idParticipante, idEvento)
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: 'Eliminar un registro participante-evento' })
|
||||
@ApiParam({ name: 'id', description: 'ID del participante', type: 'number' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Registro eliminado correctamente'
|
||||
})
|
||||
@ApiResponse({ status: 404, description: 'Registro no encontrado' })
|
||||
@Delete(':id')
|
||||
deleteParticipanteEvento(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.participanteEventoService.deleteParticipanteEvento(id)
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: 'Actualizar un registro participante-evento' })
|
||||
@ApiParam({ name: 'id', description: 'ID del participante', type: 'number' })
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'Registro actualizado correctamente'
|
||||
})
|
||||
@ApiResponse({ status: 404, description: 'Registro no encontrado' })
|
||||
@Patch(':id')
|
||||
updateParticipanteEvento(@Param('id', ParseIntPipe) id_participante: number, id_evento: number, @Body() participanteEvento: UpdateParticipanteEventoDto) {
|
||||
return this.participanteEventoService.updateParticipanteEvento(id_participante, id_evento, participanteEvento)
|
||||
|
||||
@@ -69,6 +69,54 @@ export class ParticipanteEventoService {
|
||||
return participante_eventoFound
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene todos los participantes de un evento específico
|
||||
* @param id_evento ID del evento
|
||||
* @returns Lista de registros participante-evento con información del participante
|
||||
*/
|
||||
async getParticipantesPorEvento(id_evento: number) {
|
||||
// Verificar si el evento existe
|
||||
const eventoExists = await this.eventoRepository.findOne({
|
||||
where: { id_evento }
|
||||
});
|
||||
|
||||
if (!eventoExists) {
|
||||
throw new HttpException(`Evento con ID ${id_evento} no encontrado`, HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
// Obtener todos los registros participante-evento para este evento
|
||||
const participantesEvento = await this.participanteEventoRepository.find({
|
||||
where: { id_evento },
|
||||
relations: ['participante']
|
||||
});
|
||||
|
||||
return participantesEvento;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene todos los eventos de un participante específico
|
||||
* @param id_participante ID del participante
|
||||
* @returns Lista de registros participante-evento con información del evento
|
||||
*/
|
||||
async getEventosPorParticipante(id_participante: number) {
|
||||
// Verificar si el participante existe
|
||||
const participanteExists = await this.participanteRepository.findOne({
|
||||
where: { id_participante }
|
||||
});
|
||||
|
||||
if (!participanteExists) {
|
||||
throw new HttpException(`Participante con ID ${id_participante} no encontrado`, HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
// Obtener todos los registros participante-evento para este participante
|
||||
const eventosParticipante = await this.participanteEventoRepository.find({
|
||||
where: { id_participante },
|
||||
relations: ['evento']
|
||||
});
|
||||
|
||||
return eventosParticipante;
|
||||
}
|
||||
|
||||
async deleteParticipanteEvento(id_participante: number) {
|
||||
const result = await this.participanteEventoRepository.delete({ id_participante })
|
||||
|
||||
|
||||
@@ -88,6 +88,16 @@ export class CreatePreguntaDto {
|
||||
@IsNumber()
|
||||
id_opcion_dependiente?: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Tipo de validación para la respuesta',
|
||||
required: false,
|
||||
examples: ['cuenta_alumno', 'correo', 'telefono', 'nombre', 'entero', 'decimal'],
|
||||
example: 'correo'
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
validacion?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Lista de opciones para preguntas de tipo Cerrada o Multiple',
|
||||
type: [OpcionDto],
|
||||
|
||||
@@ -28,6 +28,9 @@ export class Pregunta {
|
||||
@Column({ type: 'int', nullable: true })
|
||||
id_opcion_dependiente?: number;
|
||||
|
||||
@Column({ nullable: true })
|
||||
validacion?: string;
|
||||
|
||||
@OneToMany(() => SeccionPregunta, seccionPregunta => seccionPregunta.pregunta)
|
||||
seccionPreguntas: SeccionPregunta[];
|
||||
|
||||
|
||||
@@ -28,6 +28,11 @@ export class PreguntaApiDocumentation {
|
||||
},
|
||||
contador_opcion: { type: 'number', example: 0 },
|
||||
id_opcion_dependiente: { type: 'number', example: null },
|
||||
validacion: {
|
||||
type: 'string',
|
||||
description: 'Tipo de validación para la respuesta (cuenta_alumno, correo, telefono, nombre, entero, decimal, etc)',
|
||||
example: 'correo'
|
||||
},
|
||||
opciones: {
|
||||
type: 'array',
|
||||
items: {
|
||||
@@ -46,6 +51,7 @@ export class PreguntaApiDocumentation {
|
||||
titulo: '¿Eres parte de la comunidad de la FES Acatlán?',
|
||||
obligatoria: true,
|
||||
tipo: TipoPreguntaEnum.CERRADA,
|
||||
validacion: null,
|
||||
opciones: [
|
||||
{ valor: 'Sí' },
|
||||
{ valor: 'No' }
|
||||
@@ -57,7 +63,8 @@ export class PreguntaApiDocumentation {
|
||||
value: {
|
||||
titulo: '¿Qué mejorarías en nuestro servicio?',
|
||||
obligatoria: false,
|
||||
tipo: TipoPreguntaEnum.ABIERTA
|
||||
tipo: TipoPreguntaEnum.ABIERTA,
|
||||
validacion: null
|
||||
}
|
||||
},
|
||||
multiple: {
|
||||
@@ -66,6 +73,7 @@ export class PreguntaApiDocumentation {
|
||||
titulo: '¿Qué aspectos del servicio fueron de tu agrado?',
|
||||
obligatoria: false,
|
||||
tipo: TipoPreguntaEnum.MULTIPLE,
|
||||
validacion: null,
|
||||
opciones: [
|
||||
{ valor: 'Rapidez' },
|
||||
{ valor: 'Atención al cliente' },
|
||||
@@ -86,7 +94,8 @@ export class PreguntaApiDocumentation {
|
||||
obligatoria: { type: 'boolean', example: true },
|
||||
contador_opcion: { type: 'number', example: 0 },
|
||||
id_tipo_pregunta: { type: 'number', example: 1 },
|
||||
id_opcion_dependiente: { type: 'number', example: null }
|
||||
id_opcion_dependiente: { type: 'number', example: null },
|
||||
validacion: { type: 'string', example: 'correo' }
|
||||
}
|
||||
}
|
||||
}),
|
||||
@@ -113,7 +122,8 @@ export class PreguntaApiDocumentation {
|
||||
obligatoria: { type: 'boolean', example: true },
|
||||
contador_opcion: { type: 'number', example: 0 },
|
||||
id_tipo_pregunta: { type: 'number', example: 1 },
|
||||
id_opcion_dependiente: { type: 'number', example: null }
|
||||
id_opcion_dependiente: { type: 'number', example: null },
|
||||
validacion: { type: 'string', example: 'correo' }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -146,6 +156,7 @@ export class PreguntaApiDocumentation {
|
||||
contador_opcion: { type: 'number', example: 0 },
|
||||
id_tipo_pregunta: { type: 'number', example: 1 },
|
||||
id_opcion_dependiente: { type: 'number', example: null },
|
||||
validacion: { type: 'string', example: 'correo' },
|
||||
opciones: {
|
||||
type: 'array',
|
||||
items: {
|
||||
|
||||
@@ -44,6 +44,7 @@ export class PreguntaService {
|
||||
pregunta.id_tipo_pregunta = tipoPregunta.id_tipo;
|
||||
pregunta.id_opcion_dependiente = createPreguntaDto.id_opcion_dependiente || undefined;
|
||||
pregunta.contador_opcion = createPreguntaDto.opciones?.length || 0;
|
||||
pregunta.validacion = createPreguntaDto.validacion || undefined;
|
||||
|
||||
const savedPregunta = await queryRunner.manager.save(pregunta);
|
||||
|
||||
@@ -158,6 +159,7 @@ export class PreguntaService {
|
||||
if (updatePreguntaDto.titulo) pregunta.pregunta = updatePreguntaDto.titulo;
|
||||
if (updatePreguntaDto.obligatoria !== undefined) pregunta.obligatoria = updatePreguntaDto.obligatoria;
|
||||
if (updatePreguntaDto.id_opcion_dependiente !== undefined) pregunta.id_opcion_dependiente = updatePreguntaDto.id_opcion_dependiente;
|
||||
if (updatePreguntaDto.validacion !== undefined) pregunta.validacion = updatePreguntaDto.validacion;
|
||||
|
||||
// Si se proporciona un nuevo tipo de pregunta, actualizarlo
|
||||
if (updatePreguntaDto.tipo) {
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ValidadorRespuestasService } from './validador-respuestas.service';
|
||||
|
||||
@Module({
|
||||
providers: [ValidadorRespuestasService],
|
||||
exports: [ValidadorRespuestasService]
|
||||
})
|
||||
export class ValidacionesModule {}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { validarRespuesta, ResultadoValidacion } from './validador';
|
||||
|
||||
/**
|
||||
* Servicio para validar respuestas de cuestionarios
|
||||
* Utiliza los validadores específicos según el tipo de validación requerido
|
||||
*/
|
||||
@Injectable()
|
||||
export class ValidadorRespuestasService {
|
||||
/**
|
||||
* Valida una respuesta de acuerdo al tipo de validación de la pregunta
|
||||
* @param respuesta Texto de la respuesta a validar
|
||||
* @param tipoValidacion Tipo de validación ('correo', 'telefono', 'nombre', etc.)
|
||||
* @returns Resultado de la validación (válido y mensaje)
|
||||
*/
|
||||
validarRespuesta(respuesta: string, tipoValidacion: string): ResultadoValidacion {
|
||||
// Si no hay tipo de validación, la respuesta se considera válida
|
||||
if (!tipoValidacion) {
|
||||
return {
|
||||
valido: true,
|
||||
mensaje: 'No se requiere validación específica'
|
||||
};
|
||||
}
|
||||
|
||||
return validarRespuesta(respuesta, tipoValidacion);
|
||||
}
|
||||
|
||||
/**
|
||||
* Valida múltiples respuestas basadas en sus tipos de validación
|
||||
* @param respuestas Array de respuestas a validar
|
||||
* @param tiposValidacion Array de tipos de validación correspondientes
|
||||
* @returns Object con resultados de validación usando las posiciones como claves
|
||||
*/
|
||||
validarRespuestas(
|
||||
respuestas: string[],
|
||||
tiposValidacion: string[]
|
||||
): { [key: number]: ResultadoValidacion } {
|
||||
const resultados: { [key: number]: ResultadoValidacion } = {};
|
||||
|
||||
respuestas.forEach((respuesta, index) => {
|
||||
if (index < tiposValidacion.length) {
|
||||
resultados[index] = this.validarRespuesta(respuesta, tiposValidacion[index]);
|
||||
} else {
|
||||
// Si no hay un tipo de validación correspondiente, se asume válida
|
||||
resultados[index] = {
|
||||
valido: true,
|
||||
mensaje: 'No se requiere validación específica'
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return resultados;
|
||||
}
|
||||
|
||||
/**
|
||||
* Comprueba si todas las respuestas son válidas
|
||||
* @param resultados Resultados de validación
|
||||
* @returns true si todas las validaciones son correctas, false en caso contrario
|
||||
*/
|
||||
todasValidas(resultados: { [key: number]: ResultadoValidacion }): boolean {
|
||||
return Object.values(resultados).every(resultado => resultado.valido);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
/**
|
||||
* Módulo de validación para respuestas de cuestionarios
|
||||
* Este módulo puede ser utilizado tanto en backend (NestJS) como en frontend (Angular, React, etc.)
|
||||
*/
|
||||
|
||||
// Interfaz para el resultado de validación
|
||||
export interface ResultadoValidacion {
|
||||
valido: boolean;
|
||||
mensaje: string;
|
||||
}
|
||||
|
||||
// Clase base para todos los validadores
|
||||
export abstract class Validador {
|
||||
abstract validar(valor: string): ResultadoValidacion;
|
||||
}
|
||||
|
||||
// Clase para validar correos electrónicos
|
||||
export class ValidadorCorreo extends Validador {
|
||||
validar(valor: string): ResultadoValidacion {
|
||||
if (!valor) {
|
||||
return {
|
||||
valido: false,
|
||||
mensaje: 'El correo electrónico es requerido'
|
||||
};
|
||||
}
|
||||
|
||||
// Expresión regular para validar correos
|
||||
// Valida el formato básico de correos: usuario@dominio.extensión
|
||||
const regex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
|
||||
|
||||
if (!regex.test(valor)) {
|
||||
return {
|
||||
valido: false,
|
||||
mensaje: 'El formato del correo electrónico no es válido'
|
||||
};
|
||||
}
|
||||
|
||||
// Para correos institucionales (opcional)
|
||||
if (this.validarDominioInstitucional) {
|
||||
const dominios = ['acatlan.unam.mx', 'unam.mx', 'comunidad.unam.mx'];
|
||||
const dominio = valor.split('@')[1];
|
||||
|
||||
if (!dominios.includes(dominio)) {
|
||||
return {
|
||||
valido: false,
|
||||
mensaje: 'Debe ser un correo institucional (@acatlan.unam.mx, @unam.mx, @comunidad.unam.mx)'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
valido: true,
|
||||
mensaje: 'Correo electrónico válido'
|
||||
};
|
||||
}
|
||||
|
||||
// Propiedad que permite configurar si se validan sólo correos institucionales
|
||||
constructor(public validarDominioInstitucional: boolean = false) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
// Clase para validar números telefónicos
|
||||
export class ValidadorTelefono extends Validador {
|
||||
validar(valor: string): ResultadoValidacion {
|
||||
if (!valor) {
|
||||
return {
|
||||
valido: false,
|
||||
mensaje: 'El número telefónico es requerido'
|
||||
};
|
||||
}
|
||||
|
||||
// Eliminar espacios, guiones y paréntesis para la validación
|
||||
const numeroLimpio = valor.replace(/[\s\-()]/g, '');
|
||||
|
||||
// Verificar que solo contenga dígitos
|
||||
if (!/^\d+$/.test(numeroLimpio)) {
|
||||
return {
|
||||
valido: false,
|
||||
mensaje: 'El número telefónico solo debe contener dígitos'
|
||||
};
|
||||
}
|
||||
|
||||
// Verificar longitud (para México, 10 dígitos)
|
||||
if (numeroLimpio.length !== 10) {
|
||||
return {
|
||||
valido: false,
|
||||
mensaje: 'El número telefónico debe tener 10 dígitos'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
valido: true,
|
||||
mensaje: 'Número telefónico válido'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Clase para validar nombres
|
||||
export class ValidadorNombre extends Validador {
|
||||
validar(valor: string): ResultadoValidacion {
|
||||
if (!valor) {
|
||||
return {
|
||||
valido: false,
|
||||
mensaje: 'El nombre es requerido'
|
||||
};
|
||||
}
|
||||
|
||||
// Verificar longitud mínima
|
||||
if (valor.trim().length < 2) {
|
||||
return {
|
||||
valido: false,
|
||||
mensaje: 'El nombre debe tener al menos 2 caracteres'
|
||||
};
|
||||
}
|
||||
|
||||
// Verificar que solo contenga letras y espacios
|
||||
if (!/^[a-zA-ZáéíóúÁÉÍÓÚñÑüÜ\\s]+$/.test(valor)) {
|
||||
return {
|
||||
valido: false,
|
||||
mensaje: 'El nombre solo debe contener letras y espacios'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
valido: true,
|
||||
mensaje: 'Nombre válido'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Clase para validar números enteros
|
||||
export class ValidadorEntero extends Validador {
|
||||
constructor(
|
||||
private min: number = Number.MIN_SAFE_INTEGER,
|
||||
private max: number = Number.MAX_SAFE_INTEGER
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
validar(valor: string): ResultadoValidacion {
|
||||
if (!valor) {
|
||||
return {
|
||||
valido: false,
|
||||
mensaje: 'El valor numérico es requerido'
|
||||
};
|
||||
}
|
||||
|
||||
// Verificar que sea un número entero
|
||||
if (!/^-?\d+$/.test(valor)) {
|
||||
return {
|
||||
valido: false,
|
||||
mensaje: 'El valor debe ser un número entero'
|
||||
};
|
||||
}
|
||||
|
||||
const numero = parseInt(valor, 10);
|
||||
|
||||
// Verificar rango
|
||||
if (numero < this.min || numero > this.max) {
|
||||
return {
|
||||
valido: false,
|
||||
mensaje: `El valor debe estar entre ${this.min} y ${this.max}`
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
valido: true,
|
||||
mensaje: 'Número entero válido'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Clase para validar números decimales
|
||||
export class ValidadorDecimal extends Validador {
|
||||
constructor(
|
||||
private min: number = Number.MIN_SAFE_INTEGER,
|
||||
private max: number = Number.MAX_SAFE_INTEGER,
|
||||
private decimales: number = 2
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
validar(valor: string): ResultadoValidacion {
|
||||
if (!valor) {
|
||||
return {
|
||||
valido: false,
|
||||
mensaje: 'El valor numérico es requerido'
|
||||
};
|
||||
}
|
||||
|
||||
// Verificar que sea un número decimal válido
|
||||
if (!/^-?\d+(\.\d+)?$/.test(valor)) {
|
||||
return {
|
||||
valido: false,
|
||||
mensaje: 'El valor debe ser un número decimal válido'
|
||||
};
|
||||
}
|
||||
|
||||
const numero = parseFloat(valor);
|
||||
|
||||
// Verificar rango
|
||||
if (numero < this.min || numero > this.max) {
|
||||
return {
|
||||
valido: false,
|
||||
mensaje: `El valor debe estar entre ${this.min} y ${this.max}`
|
||||
};
|
||||
}
|
||||
|
||||
// Verificar precisión decimal
|
||||
const partes = valor.split('.');
|
||||
if (partes.length > 1 && partes[1].length > this.decimales) {
|
||||
return {
|
||||
valido: false,
|
||||
mensaje: `El valor debe tener máximo ${this.decimales} decimales`
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
valido: true,
|
||||
mensaje: 'Número decimal válido'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Clase para validar cuentas de alumno (formato específico UNAM - 9 dígitos)
|
||||
export class ValidadorCuentaAlumno extends Validador {
|
||||
validar(valor: string): ResultadoValidacion {
|
||||
if (!valor) {
|
||||
return {
|
||||
valido: false,
|
||||
mensaje: 'La cuenta de alumno es requerida'
|
||||
};
|
||||
}
|
||||
|
||||
// Formato de cuenta UNAM: 9 dígitos para todos los alumnos
|
||||
if (!/^\d{9}$/.test(valor)) {
|
||||
return {
|
||||
valido: false,
|
||||
mensaje: 'La cuenta de alumno debe tener exactamente 9 dígitos numéricos'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
valido: true,
|
||||
mensaje: 'Cuenta de alumno válida'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Fábrica de validadores para crear el validador apropiado según el tipo
|
||||
export class FabricaValidadores {
|
||||
static crear(tipo: string): Validador | null {
|
||||
switch (tipo?.toLowerCase()) {
|
||||
case 'correo':
|
||||
return new ValidadorCorreo();
|
||||
case 'correo_institucional':
|
||||
return new ValidadorCorreo(true);
|
||||
case 'telefono':
|
||||
return new ValidadorTelefono();
|
||||
case 'nombre':
|
||||
return new ValidadorNombre();
|
||||
case 'entero':
|
||||
return new ValidadorEntero();
|
||||
case 'decimal':
|
||||
return new ValidadorDecimal();
|
||||
case 'cuenta_alumno':
|
||||
return new ValidadorCuentaAlumno();
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Función auxiliar para validar respuestas basada en el tipo de validación
|
||||
export function validarRespuesta(respuesta: string, tipoValidacion: string): ResultadoValidacion {
|
||||
const validador = FabricaValidadores.crear(tipoValidacion);
|
||||
|
||||
if (!validador) {
|
||||
return {
|
||||
valido: true, // Si no hay validador, consideramos válida la respuesta
|
||||
mensaje: 'No se requiere validación específica'
|
||||
};
|
||||
}
|
||||
|
||||
return validador.validar(respuesta);
|
||||
}
|
||||
@@ -23,24 +23,28 @@ export const feriaSexualidad = {
|
||||
obligatoria: true,
|
||||
tipo: 'Abierta',
|
||||
limite: 250,
|
||||
validacion: 'correo',
|
||||
},
|
||||
{
|
||||
titulo: 'Numero de cuenta',
|
||||
obligatoria: true,
|
||||
tipo: 'Abierta',
|
||||
limite: 250,
|
||||
validacion: 'cuenta_alumno',
|
||||
},
|
||||
{
|
||||
titulo: 'Nombre(s)',
|
||||
obligatoria: true,
|
||||
tipo: 'Abierta',
|
||||
limite: 250,
|
||||
validacion: 'nombre',
|
||||
},
|
||||
{
|
||||
titulo: 'Apellidos',
|
||||
obligatoria: true,
|
||||
tipo: 'Abierta',
|
||||
limite: 250,
|
||||
validacion: 'nombre',
|
||||
},
|
||||
{
|
||||
titulo: 'Género',
|
||||
|
||||
Reference in New Issue
Block a user