From e5c6f611f44dec0485f442c8765f64fa4f22e4aa Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 4 Apr 2025 14:39:04 -0600 Subject: [PATCH] docker compose front, modulo de validacion de preguntas, crud de participante, get participantes por evento y evento por participantes --- Dockerfile | 4 +- README.md | 22 ++ docker-compose.yml | 16 +- src/app.module.ts | 2 + .../cuestionario.documentation.ts | 12 +- src/cuestionario/cuestionario.service.ts | 2 + src/cuestionario/dto/formulario.schema.ts | 2 + .../cuestionario_respondido.module.ts | 4 +- .../cuestionario_respondido.service.ts | 14 + .../dto/create-participante.dto.ts | 23 +- .../dto/update.participante.dto.ts | 21 +- src/participante/participante.controller.ts | 38 +-- .../participante.documentation.ts | 211 +++++++++++++ src/participante/participante.service.ts | 94 ++++-- .../participante_evento.controller.ts | 146 ++++++++- .../participante_evento.service.ts | 48 +++ src/pregunta/dto/create-pregunta.dto.ts | 10 + src/pregunta/entities/pregunta.entity.ts | 3 + src/pregunta/pregunta.documentation.ts | 17 +- src/pregunta/pregunta.service.ts | 2 + src/validaciones/validaciones.module.ts | 8 + .../validador-respuestas.service.ts | 63 ++++ src/validaciones/validador.ts | 287 ++++++++++++++++++ utils/crear_formulario_feria.ts | 4 + 24 files changed, 979 insertions(+), 74 deletions(-) create mode 100644 src/participante/participante.documentation.ts create mode 100644 src/validaciones/validaciones.module.ts create mode 100644 src/validaciones/validador-respuestas.service.ts create mode 100644 src/validaciones/validador.ts diff --git a/Dockerfile b/Dockerfile index d122543..9619991 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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"] diff --git a/README.md b/README.md index c17103c..35f1236 100644 --- a/README.md +++ b/README.md @@ -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'); \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 9b4cf9a..f7facd9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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: diff --git a/src/app.module.ts b/src/app.module.ts index 155c9b8..7cfaaff 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -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], diff --git a/src/cuestionario/cuestionario.documentation.ts b/src/cuestionario/cuestionario.documentation.ts index 9e951fa..8caf16a 100644 --- a/src/cuestionario/cuestionario.documentation.ts +++ b/src/cuestionario/cuestionario.documentation.ts @@ -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 }, diff --git a/src/cuestionario/cuestionario.service.ts b/src/cuestionario/cuestionario.service.ts index c31361a..f3e05ad 100644 --- a/src/cuestionario/cuestionario.service.ts +++ b/src/cuestionario/cuestionario.service.ts @@ -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 diff --git a/src/cuestionario/dto/formulario.schema.ts b/src/cuestionario/dto/formulario.schema.ts index d1378dc..f1478be 100644 --- a/src/cuestionario/dto/formulario.schema.ts +++ b/src/cuestionario/dto/formulario.schema.ts @@ -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' diff --git a/src/cuestionario_respondido/cuestionario_respondido.module.ts b/src/cuestionario_respondido/cuestionario_respondido.module.ts index c5be459..d0cdb61 100644 --- a/src/cuestionario_respondido/cuestionario_respondido.module.ts +++ b/src/cuestionario_respondido/cuestionario_respondido.module.ts @@ -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], diff --git a/src/cuestionario_respondido/cuestionario_respondido.service.ts b/src/cuestionario_respondido/cuestionario_respondido.service.ts index e1848c0..44c9f64 100644 --- a/src/cuestionario_respondido/cuestionario_respondido.service.ts +++ b/src/cuestionario_respondido/cuestionario_respondido.service.ts @@ -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, 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; diff --git a/src/participante/dto/create-participante.dto.ts b/src/participante/dto/create-participante.dto.ts index 3aae94f..871599b 100644 --- a/src/participante/dto/create-participante.dto.ts +++ b/src/participante/dto/create-participante.dto.ts @@ -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; } \ No newline at end of file diff --git a/src/participante/dto/update.participante.dto.ts b/src/participante/dto/update.participante.dto.ts index 9cc03ba..9a1d769 100644 --- a/src/participante/dto/update.participante.dto.ts +++ b/src/participante/dto/update.participante.dto.ts @@ -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; } \ No newline at end of file diff --git a/src/participante/participante.controller.ts b/src/participante/participante.controller.ts index f688879..e25b387 100644 --- a/src/participante/participante.controller.ts +++ b/src/participante/participante.controller.ts @@ -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 { - 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); } - - - } diff --git a/src/participante/participante.documentation.ts b/src/participante/participante.documentation.ts new file mode 100644 index 0000000..9140c5d --- /dev/null +++ b/src/participante/participante.documentation.ts @@ -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' }) + ); +} \ No newline at end of file diff --git a/src/participante/participante.service.ts b/src/participante/participante.service.ts index 746cabc..c500033 100644 --- a/src/participante/participante.service.ts +++ b/src/participante/participante.service.ts @@ -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 ) {} - 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 { + // 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 { 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 { 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 { + // 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); + } } diff --git a/src/participante_evento/participante_evento.controller.ts b/src/participante_evento/participante_evento.controller.ts index ce29d58..5b78ac4 100644 --- a/src/participante_evento/participante_evento.controller.ts +++ b/src/participante_evento/participante_evento.controller.ts @@ -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 { - 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 { + 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) diff --git a/src/participante_evento/participante_evento.service.ts b/src/participante_evento/participante_evento.service.ts index e989664..77f9f5d 100644 --- a/src/participante_evento/participante_evento.service.ts +++ b/src/participante_evento/participante_evento.service.ts @@ -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 }) diff --git a/src/pregunta/dto/create-pregunta.dto.ts b/src/pregunta/dto/create-pregunta.dto.ts index dad95d2..6508cb9 100644 --- a/src/pregunta/dto/create-pregunta.dto.ts +++ b/src/pregunta/dto/create-pregunta.dto.ts @@ -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], diff --git a/src/pregunta/entities/pregunta.entity.ts b/src/pregunta/entities/pregunta.entity.ts index a912496..b936a3f 100644 --- a/src/pregunta/entities/pregunta.entity.ts +++ b/src/pregunta/entities/pregunta.entity.ts @@ -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[]; diff --git a/src/pregunta/pregunta.documentation.ts b/src/pregunta/pregunta.documentation.ts index 06eb1ac..0641201 100644 --- a/src/pregunta/pregunta.documentation.ts +++ b/src/pregunta/pregunta.documentation.ts @@ -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: { diff --git a/src/pregunta/pregunta.service.ts b/src/pregunta/pregunta.service.ts index b190384..c339468 100644 --- a/src/pregunta/pregunta.service.ts +++ b/src/pregunta/pregunta.service.ts @@ -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) { diff --git a/src/validaciones/validaciones.module.ts b/src/validaciones/validaciones.module.ts new file mode 100644 index 0000000..2ad3103 --- /dev/null +++ b/src/validaciones/validaciones.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { ValidadorRespuestasService } from './validador-respuestas.service'; + +@Module({ + providers: [ValidadorRespuestasService], + exports: [ValidadorRespuestasService] +}) +export class ValidacionesModule {} \ No newline at end of file diff --git a/src/validaciones/validador-respuestas.service.ts b/src/validaciones/validador-respuestas.service.ts new file mode 100644 index 0000000..6dd7598 --- /dev/null +++ b/src/validaciones/validador-respuestas.service.ts @@ -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); + } +} \ No newline at end of file diff --git a/src/validaciones/validador.ts b/src/validaciones/validador.ts new file mode 100644 index 0000000..1ca4be1 --- /dev/null +++ b/src/validaciones/validador.ts @@ -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); +} \ No newline at end of file diff --git a/utils/crear_formulario_feria.ts b/utils/crear_formulario_feria.ts index 95cd10e..0b2d77d 100644 --- a/utils/crear_formulario_feria.ts +++ b/utils/crear_formulario_feria.ts @@ -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', -- 2.43.0