From bf7c0e93a8ee1ce21bdc92b933588ed27c466c92 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 2 Apr 2025 11:14:45 -0600 Subject: [PATCH 1/3] variables de entorno, correcciones de codigo actual, docker file --- Dockerfile | 23 +++++++++++++++ docker-compose.yml | 28 +++++++++++++++++++ src/app.module.ts | 13 ++++++--- src/cuestionario/cuestionario.module.ts | 4 +++ .../entities/cuestionario.entity.ts | 8 ++---- .../cuestionario_respondido.module.ts | 10 +++++++ .../cuestionario_respondido.entity.ts | 10 +++---- .../cuestionario_seccion.module.ts | 10 +++++++ .../entities/cuestionario_seccion.entity.ts | 8 ++++-- src/main.ts | 2 +- src/opcion/entities/opcion.entity.ts | 4 +-- src/opcion/opcion.module.ts | 2 +- src/participante/participante.entity.ts | 2 +- src/participante/participante.module.ts | 2 +- src/pregunta/entities/pregunta.entity.ts | 9 ++++-- src/pregunta/pregunta.module.ts | 8 ++++++ .../entities/pregunta_opcion.entity.ts | 15 +++++----- src/pregunta_opcion/pregunta_opcion.module.ts | 10 +++++++ .../respuesta_participante_abierta.entity.ts | 11 +++----- .../respuesta_participante_abierta.module.ts | 10 +++++++ .../respuesta_participante_cerrada.entity.ts | 6 ++-- .../respuesta_participante_cerrada.module.ts | 10 +++++++ src/seccion/entities/seccion.entity.ts | 4 +-- src/seccion/seccion.module.ts | 4 +++ .../entities/seccion_pregunta.entity.ts | 8 ++++-- .../seccion_pregunta.module.ts | 10 +++++++ .../entities/tipo_pregunta.entity.ts | 2 +- src/tipo_pregunta/tipo_pregunta.module.ts | 4 +++ utils/crear_formulario.json | 0 utils/crear_formulario_feria.json | 0 utils/mandar_formulario.json | 0 utils/respuesta_1.json | 0 utils/respuesta_2.json | 0 33 files changed, 190 insertions(+), 47 deletions(-) create mode 100644 Dockerfile create mode 100644 docker-compose.yml create mode 100644 utils/crear_formulario.json create mode 100644 utils/crear_formulario_feria.json create mode 100644 utils/mandar_formulario.json create mode 100644 utils/respuesta_1.json create mode 100644 utils/respuesta_2.json diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..4f67ac3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +# Etapa de construcción +FROM node:22-alpine AS builder + +WORKDIR /app + +COPY package*.json ./ +RUN npm install + +COPY . . +RUN npm run build + +# Etapa de producción +FROM node:20-alpine AS production + +WORKDIR /app + +COPY --from=builder /app/node_modules ./node_modules +COPY --from=builder /app/dist ./dist +COPY --from=builder /app/package*.json ./ + +EXPOSE 4100 + +CMD ["node", "dist/main"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..ab95035 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,28 @@ +version: '3.8' + +services: + mariadb: + image: mariadb:latest + container_name: formularios_db + ports: + - "4201:3306" + environment: + MARIADB_ROOT_PASSWORD: mypass + MARIADB_DATABASE: formularios_test + volumes: + - mariadb_data:/var/lib/mysql + + api: + build: + context: . + dockerfile: Dockerfile + container_name: formularios_api + env_file: + - .env + ports: + - "4200:3000" + depends_on: + - mariadb + +volumes: + mariadb_data: diff --git a/src/app.module.ts b/src/app.module.ts index 1bcc365..155c9b8 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -29,7 +29,7 @@ import { ParticipanteEventoModule } from './participante_evento/participante_eve imports: [ ConfigModule.forRoot(), TypeOrmModule.forRoot({ - /* + type: 'mysql', host: process.env.db_host, //process.env.db_host username: process.env.db_username, @@ -40,16 +40,21 @@ import { ParticipanteEventoModule } from './participante_evento/participante_eve dropSchema: true, // elimina la base de datos // logging: true, // Habilita los logs para depuración autoLoadEntities: true, // Carga automáticamente las entidades - */ + //esta es mi base de datos local + + /* type: 'mysql', host: 'localhost', port: 3306, //3306 username: 'root', password: 'admin', //admin database: 'cidwa', //nestdb - entities: [__dirname + '/**/*.entity{.ts,.js}'], - synchronize: true, + */ + // entities: [__dirname + '/**/*.entity{.ts,.js}'], + // synchronize: true, + + //extra retryAttempts: 10, // Intentos para reconectar retryDelay: 3000, // Tiempo entre reintentos diff --git a/src/cuestionario/cuestionario.module.ts b/src/cuestionario/cuestionario.module.ts index 478b3d6..c6ba022 100644 --- a/src/cuestionario/cuestionario.module.ts +++ b/src/cuestionario/cuestionario.module.ts @@ -1,9 +1,13 @@ import { Module } from '@nestjs/common'; import { CuestionarioService } from './cuestionario.service'; import { CuestionarioController } from './cuestionario.controller'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Cuestionario } from './entities/cuestionario.entity'; @Module({ + imports: [TypeOrmModule.forFeature([Cuestionario])], controllers: [CuestionarioController], providers: [CuestionarioService], + exports: [TypeOrmModule] }) export class CuestionarioModule {} diff --git a/src/cuestionario/entities/cuestionario.entity.ts b/src/cuestionario/entities/cuestionario.entity.ts index f6add87..f755b13 100644 --- a/src/cuestionario/entities/cuestionario.entity.ts +++ b/src/cuestionario/entities/cuestionario.entity.ts @@ -31,14 +31,12 @@ export class Cuestionario { @OneToMany(() => CuestionarioSeccion,(cuestionario_seccion) => cuestionario_seccion.id_cuestionario) cuestionarioSeccion:CuestionarioSeccion[] - @OneToMany(()=> Cuestionario, (cuestionario) => cuestionario.id_cuestionario) - @Column({ type: 'int' }) + @Column({ type: 'int', nullable: true }) id_cuestionario_original: number; - @OneToMany(()=> TipoCuestionario, (tipo_cuestionario) => tipo_cuestionario.id_tipo_cuestionario ) @Column({ type: 'int' }) id_tipo_cuestionario: number; - @ManyToOne(()=> Cuestionario, (cuestionario) => cuestionario.id_cuestionario) - Cuestionario: Cuestionario[]; + @ManyToOne(()=> Cuestionario) + cuestionarioOriginal: Cuestionario; } diff --git a/src/cuestionario_respondido/cuestionario_respondido.module.ts b/src/cuestionario_respondido/cuestionario_respondido.module.ts index 49b24eb..221348e 100644 --- a/src/cuestionario_respondido/cuestionario_respondido.module.ts +++ b/src/cuestionario_respondido/cuestionario_respondido.module.ts @@ -1,9 +1,19 @@ import { Module } from '@nestjs/common'; import { CuestionarioRespondidoService } from './cuestionario_respondido.service'; import { CuestionarioRespondidoController } from './cuestionario_respondido.controller'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { CuestionarioRespondido } from './entities/cuestionario_respondido.entity'; +import { CuestionarioModule } from '../cuestionario/cuestionario.module'; +import { ParticipanteModule } from '../participante/participante.module'; @Module({ + imports: [ + TypeOrmModule.forFeature([CuestionarioRespondido]), + CuestionarioModule, + ParticipanteModule + ], controllers: [CuestionarioRespondidoController], providers: [CuestionarioRespondidoService], + exports: [TypeOrmModule] }) export class CuestionarioRespondidoModule {} diff --git a/src/cuestionario_respondido/entities/cuestionario_respondido.entity.ts b/src/cuestionario_respondido/entities/cuestionario_respondido.entity.ts index 77e21e7..bd44499 100644 --- a/src/cuestionario_respondido/entities/cuestionario_respondido.entity.ts +++ b/src/cuestionario_respondido/entities/cuestionario_respondido.entity.ts @@ -16,18 +16,18 @@ export class CuestionarioRespondido { // Relación con Participante @ManyToOne(() => Participante, participante => participante.cuestionariosRespondidos) @JoinColumn({ name: 'id_participante' }) - id_participante: Participante; + participante: Participante; // Relación con Cuestionario - @ManyToOne(() => Cuestionario, cuestionario => cuestionario.id_cuestionario) + @ManyToOne(() => Cuestionario) @JoinColumn({ name: 'id_cuestionario' }) - id_cuestionario: Cuestionario; + cuestionario: Cuestionario; // Relación con RespuestasCerradas - @OneToMany(() => RespuestaParticipanteCerrada, respuestaCerrada => respuestaCerrada.id_cuestionario_respondido) + @OneToMany(() => RespuestaParticipanteCerrada, respuestaCerrada => respuestaCerrada.cuestionarioRespondido) respuestasCerradas: RespuestaParticipanteCerrada[]; // Relación con RespuestasAbiertas - @OneToMany(() => RespuestaParticipanteAbierta, respuestaAbierta => respuestaAbierta.id_cuestionario_respondido) + @OneToMany(() => RespuestaParticipanteAbierta, respuestaAbierta => respuestaAbierta.cuestionarioRespondido) respuestasAbiertas: RespuestaParticipanteAbierta[]; } \ No newline at end of file diff --git a/src/cuestionario_seccion/cuestionario_seccion.module.ts b/src/cuestionario_seccion/cuestionario_seccion.module.ts index d3635ed..8d70889 100644 --- a/src/cuestionario_seccion/cuestionario_seccion.module.ts +++ b/src/cuestionario_seccion/cuestionario_seccion.module.ts @@ -1,9 +1,19 @@ import { Module } from '@nestjs/common'; import { CuestionarioSeccionService } from './cuestionario_seccion.service'; import { CuestionarioSeccionController } from './cuestionario_seccion.controller'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { CuestionarioSeccion } from './entities/cuestionario_seccion.entity'; +import { CuestionarioModule } from '../cuestionario/cuestionario.module'; +import { SeccionModule } from '../seccion/seccion.module'; @Module({ + imports: [ + TypeOrmModule.forFeature([CuestionarioSeccion]), + CuestionarioModule, + SeccionModule + ], controllers: [CuestionarioSeccionController], providers: [CuestionarioSeccionService], + exports: [TypeOrmModule] }) export class CuestionarioSeccionModule {} diff --git a/src/cuestionario_seccion/entities/cuestionario_seccion.entity.ts b/src/cuestionario_seccion/entities/cuestionario_seccion.entity.ts index 7bd9b70..9efcd9a 100644 --- a/src/cuestionario_seccion/entities/cuestionario_seccion.entity.ts +++ b/src/cuestionario_seccion/entities/cuestionario_seccion.entity.ts @@ -11,11 +11,15 @@ export class CuestionarioSeccion { posicion: number; - @ManyToOne(() => Cuestionario, (cuestionario) => cuestionario.id_cuestionario) + @ManyToOne(() => Cuestionario, (cuestionario) => cuestionario.cuestionarioSeccion) + cuestionario: Cuestionario; + @Column({ type: 'int' }) id_cuestionario: number; - @ManyToOne(() => Seccion, (seccion) => seccion.id_seccion) + @ManyToOne(() => Seccion, (seccion) => seccion.seccion) + seccion: Seccion; + @Column({type:'int'}) id_seccion:number; } diff --git a/src/main.ts b/src/main.ts index e1c5710..3eabdcd 100644 --- a/src/main.ts +++ b/src/main.ts @@ -9,7 +9,7 @@ async function bootstrap() { DocsModule.setupSwagger(app); - await app.listen(3000); + await app.listen(process.env.PORT ?? 3000); console.log('API en ejecución en http://localhost:3000'); console.log('Swagger disponible en http://localhost:3000/api-docs'); } diff --git a/src/opcion/entities/opcion.entity.ts b/src/opcion/entities/opcion.entity.ts index 3e2f2c4..3523e76 100644 --- a/src/opcion/entities/opcion.entity.ts +++ b/src/opcion/entities/opcion.entity.ts @@ -9,8 +9,8 @@ export class Opcion { @Column({ type: String, nullable: false, length: 200, default: '' }) opcion: string; - @OneToMany(() => PreguntaOpcion, (preguntaOpcion) => preguntaOpcion.id_opcion) - Preguntas: PreguntaOpcion[]; + @OneToMany(() => PreguntaOpcion, (preguntaOpcion) => preguntaOpcion.opcion) + preguntasOpciones: PreguntaOpcion[]; } \ No newline at end of file diff --git a/src/opcion/opcion.module.ts b/src/opcion/opcion.module.ts index d5cb46a..0e1a6a8 100644 --- a/src/opcion/opcion.module.ts +++ b/src/opcion/opcion.module.ts @@ -8,6 +8,6 @@ import { Opcion } from './entities/opcion.entity'; imports: [TypeOrmModule.forFeature([Opcion])], // Importa el repositorio de Opcion controllers: [OpcionController], providers: [OpcionService], - exports: [OpcionService] + exports: [OpcionService, TypeOrmModule] }) export class OpcionModule {} diff --git a/src/participante/participante.entity.ts b/src/participante/participante.entity.ts index 7bf4ae1..3579682 100644 --- a/src/participante/participante.entity.ts +++ b/src/participante/participante.entity.ts @@ -39,7 +39,7 @@ export class Participante { - @OneToMany(() => CuestionarioRespondido, cuestionario => cuestionario.id_participante) + @OneToMany(() => CuestionarioRespondido, cuestionario => cuestionario.participante) cuestionariosRespondidos: CuestionarioRespondido[]; @OneToMany(() => ParticipanteEvento, participanteEvento => participanteEvento.participante) diff --git a/src/participante/participante.module.ts b/src/participante/participante.module.ts index 912cd0c..41a0dd0 100644 --- a/src/participante/participante.module.ts +++ b/src/participante/participante.module.ts @@ -8,6 +8,6 @@ import { Participante } from './participante.entity'; imports: [TypeOrmModule.forFeature([Participante])], controllers: [ParticipanteController], providers: [ParticipanteService], - exports: [ParticipanteService], + exports: [ParticipanteService, TypeOrmModule], }) export class ParticipanteModule {} diff --git a/src/pregunta/entities/pregunta.entity.ts b/src/pregunta/entities/pregunta.entity.ts index d4c6671..1b67797 100644 --- a/src/pregunta/entities/pregunta.entity.ts +++ b/src/pregunta/entities/pregunta.entity.ts @@ -20,14 +20,17 @@ export class Pregunta { @ManyToOne(() => TipoPregunta, tipo => tipo.preguntas) @JoinColumn({ name: 'id_tipo_pregunta' }) - id_tipo_pregunta: TipoPregunta; + tipoPregunta: TipoPregunta; + + @Column() + id_tipo_pregunta: number; @Column({ nullable: true }) id_opcion_dependiente: number; - @OneToMany(() => SeccionPregunta, seccionPregunta => seccionPregunta.id_pregunta) + @OneToMany(() => SeccionPregunta, seccionPregunta => seccionPregunta.pregunta) seccionPreguntas: SeccionPregunta[]; - @OneToMany(() => PreguntaOpcion, preguntaOpcion => preguntaOpcion.id_pregunta) + @OneToMany(() => PreguntaOpcion, preguntaOpcion => preguntaOpcion.pregunta) opciones: PreguntaOpcion[]; } \ No newline at end of file diff --git a/src/pregunta/pregunta.module.ts b/src/pregunta/pregunta.module.ts index 905ec29..d829b94 100644 --- a/src/pregunta/pregunta.module.ts +++ b/src/pregunta/pregunta.module.ts @@ -1,9 +1,17 @@ import { Module } from '@nestjs/common'; import { PreguntaService } from './pregunta.service'; import { PreguntaController } from './pregunta.controller'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Pregunta } from './entities/pregunta.entity'; +import { TipoPreguntaModule } from '../tipo_pregunta/tipo_pregunta.module'; @Module({ + imports: [ + TypeOrmModule.forFeature([Pregunta]), + TipoPreguntaModule + ], controllers: [PreguntaController], providers: [PreguntaService], + exports: [TypeOrmModule] }) export class PreguntaModule {} diff --git a/src/pregunta_opcion/entities/pregunta_opcion.entity.ts b/src/pregunta_opcion/entities/pregunta_opcion.entity.ts index 7e76438..3c0a024 100644 --- a/src/pregunta_opcion/entities/pregunta_opcion.entity.ts +++ b/src/pregunta_opcion/entities/pregunta_opcion.entity.ts @@ -12,14 +12,15 @@ export class PreguntaOpcion { @Column() posicion: number; - @ManyToOne(() => Pregunta, pregunta => pregunta.id_pregunta) - //@Column() - id_pregunta: Pregunta; + @ManyToOne(() => Pregunta) + pregunta: Pregunta; + + @Column() + id_pregunta: number; - @ManyToOne(() => Opcion, opcion => opcion.id_opcion) - //@Column() - id_opcion: Opcion; + @ManyToOne(() => Opcion, opcion => opcion.preguntasOpciones) + opcion: Opcion; - @OneToMany(()=> RespuestaParticipanteCerrada, respuestaParticipanteCerrada=>respuestaParticipanteCerrada.id_pregunta_opcion) + @OneToMany(()=> RespuestaParticipanteCerrada, respuestaParticipanteCerrada=>respuestaParticipanteCerrada.preguntaOpcion) respuestaParticipanteCerrada:RespuestaParticipanteCerrada[]; } \ No newline at end of file diff --git a/src/pregunta_opcion/pregunta_opcion.module.ts b/src/pregunta_opcion/pregunta_opcion.module.ts index 6eff7d3..3386b19 100644 --- a/src/pregunta_opcion/pregunta_opcion.module.ts +++ b/src/pregunta_opcion/pregunta_opcion.module.ts @@ -1,9 +1,19 @@ import { Module } from '@nestjs/common'; import { PreguntaOpcionService } from './pregunta_opcion.service'; import { PreguntaOpcionController } from './pregunta_opcion.controller'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { PreguntaOpcion } from './entities/pregunta_opcion.entity'; +import { OpcionModule } from '../opcion/opcion.module'; +import { PreguntaModule } from '../pregunta/pregunta.module'; @Module({ + imports: [ + TypeOrmModule.forFeature([PreguntaOpcion]), + OpcionModule, + PreguntaModule + ], controllers: [PreguntaOpcionController], providers: [PreguntaOpcionService], + exports: [TypeOrmModule] }) export class PreguntaOpcionModule {} diff --git a/src/respuesta_participante_abierta/entities/respuesta_participante_abierta.entity.ts b/src/respuesta_participante_abierta/entities/respuesta_participante_abierta.entity.ts index 1990bee..4a6262e 100644 --- a/src/respuesta_participante_abierta/entities/respuesta_participante_abierta.entity.ts +++ b/src/respuesta_participante_abierta/entities/respuesta_participante_abierta.entity.ts @@ -14,17 +14,14 @@ export class RespuestaParticipanteAbierta { // Relación con CuestionarioRespondido @ManyToOne( () => CuestionarioRespondido, - cuestionarioRespondido => cuestionarioRespondido.idCuestionarioRespondido + cuestionarioRespondido => cuestionarioRespondido.respuestasAbiertas ) @JoinColumn({ name: 'id_cuestionario_respondido' }) - id_cuestionario_respondido: CuestionarioRespondido; + cuestionarioRespondido: CuestionarioRespondido; - @ManyToOne( - () => Pregunta, - pregunta => pregunta.id_pregunta - ) + @ManyToOne(() => Pregunta) @JoinColumn({ name: 'id_pregunta' }) - id_pregunta: Pregunta; + pregunta: Pregunta; } \ No newline at end of file diff --git a/src/respuesta_participante_abierta/respuesta_participante_abierta.module.ts b/src/respuesta_participante_abierta/respuesta_participante_abierta.module.ts index 125f0d5..8fada88 100644 --- a/src/respuesta_participante_abierta/respuesta_participante_abierta.module.ts +++ b/src/respuesta_participante_abierta/respuesta_participante_abierta.module.ts @@ -1,9 +1,19 @@ import { Module } from '@nestjs/common'; import { RespuestaParticipanteAbiertaService } from './respuesta_participante_abierta.service'; import { RespuestaParticipanteAbiertaController } from './respuesta_participante_abierta.controller'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { RespuestaParticipanteAbierta } from './entities/respuesta_participante_abierta.entity'; +import { CuestionarioRespondidoModule } from '../cuestionario_respondido/cuestionario_respondido.module'; +import { PreguntaModule } from '../pregunta/pregunta.module'; @Module({ + imports: [ + TypeOrmModule.forFeature([RespuestaParticipanteAbierta]), + CuestionarioRespondidoModule, + PreguntaModule + ], controllers: [RespuestaParticipanteAbiertaController], providers: [RespuestaParticipanteAbiertaService], + exports: [TypeOrmModule] }) export class RespuestaParticipanteAbiertaModule {} diff --git a/src/respuesta_participante_cerrada/entities/respuesta_participante_cerrada.entity.ts b/src/respuesta_participante_cerrada/entities/respuesta_participante_cerrada.entity.ts index ba96744..0d65cb0 100644 --- a/src/respuesta_participante_cerrada/entities/respuesta_participante_cerrada.entity.ts +++ b/src/respuesta_participante_cerrada/entities/respuesta_participante_cerrada.entity.ts @@ -10,11 +10,11 @@ export class RespuestaParticipanteCerrada { @ManyToOne(() => CuestionarioRespondido, cuestionarioRespondido => cuestionarioRespondido.respuestasCerradas) @JoinColumn({ name: 'id_cuestionario_respondido' }) - id_cuestionario_respondido: CuestionarioRespondido; + cuestionarioRespondido: CuestionarioRespondido; - @ManyToOne(() => PreguntaOpcion, preguntaOpcion => preguntaOpcion.id_pregunta_opcion) + @ManyToOne(() => PreguntaOpcion, preguntaOpcion => preguntaOpcion.respuestaParticipanteCerrada) @JoinColumn({ name: 'id_pregunta_opcion' }) - id_pregunta_opcion: PreguntaOpcion; + preguntaOpcion: PreguntaOpcion; } \ No newline at end of file diff --git a/src/respuesta_participante_cerrada/respuesta_participante_cerrada.module.ts b/src/respuesta_participante_cerrada/respuesta_participante_cerrada.module.ts index 5fdf968..7f826bd 100644 --- a/src/respuesta_participante_cerrada/respuesta_participante_cerrada.module.ts +++ b/src/respuesta_participante_cerrada/respuesta_participante_cerrada.module.ts @@ -1,9 +1,19 @@ import { Module } from '@nestjs/common'; import { RespuestaParticipanteCerradaService } from './respuesta_participante_cerrada.service'; import { RespuestaParticipanteCerradaController } from './respuesta_participante_cerrada.controller'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { RespuestaParticipanteCerrada } from './entities/respuesta_participante_cerrada.entity'; +import { PreguntaOpcionModule } from '../pregunta_opcion/pregunta_opcion.module'; +import { CuestionarioRespondidoModule } from '../cuestionario_respondido/cuestionario_respondido.module'; @Module({ + imports: [ + TypeOrmModule.forFeature([RespuestaParticipanteCerrada]), + PreguntaOpcionModule, + CuestionarioRespondidoModule + ], controllers: [RespuestaParticipanteCerradaController], providers: [RespuestaParticipanteCerradaService], + exports: [TypeOrmModule] }) export class RespuestaParticipanteCerradaModule {} diff --git a/src/seccion/entities/seccion.entity.ts b/src/seccion/entities/seccion.entity.ts index ee2701d..6e27cd6 100644 --- a/src/seccion/entities/seccion.entity.ts +++ b/src/seccion/entities/seccion.entity.ts @@ -19,6 +19,6 @@ export class Seccion { @OneToMany(() => CuestionarioSeccion,(cuestionario_seccion) => cuestionario_seccion.id_seccion ) seccion:CuestionarioSeccion[]; - @OneToMany(()=> SeccionPregunta, (seccion_pregunta)=> seccion_pregunta.id_seccion) - seccion_pregunta:SeccionPregunta[]; + @OneToMany(()=> SeccionPregunta, (seccion_pregunta)=> seccion_pregunta.seccion) + seccionPreguntas:SeccionPregunta[]; } diff --git a/src/seccion/seccion.module.ts b/src/seccion/seccion.module.ts index a3b53de..d0d484c 100644 --- a/src/seccion/seccion.module.ts +++ b/src/seccion/seccion.module.ts @@ -1,9 +1,13 @@ import { Module } from '@nestjs/common'; import { SeccionService } from './seccion.service'; import { SeccionController } from './seccion.controller'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Seccion } from './entities/seccion.entity'; @Module({ + imports: [TypeOrmModule.forFeature([Seccion])], controllers: [SeccionController], providers: [SeccionService], + exports: [TypeOrmModule] }) export class SeccionModule {} diff --git a/src/seccion_pregunta/entities/seccion_pregunta.entity.ts b/src/seccion_pregunta/entities/seccion_pregunta.entity.ts index e476f4f..c0dcad6 100644 --- a/src/seccion_pregunta/entities/seccion_pregunta.entity.ts +++ b/src/seccion_pregunta/entities/seccion_pregunta.entity.ts @@ -11,11 +11,15 @@ export class SeccionPregunta { @Column({ type: 'tinyint' }) posicion: number; - @ManyToOne(() => Pregunta, (pregunta) => pregunta.id_pregunta) + @ManyToOne(() => Pregunta, (pregunta) => pregunta.seccionPreguntas) + pregunta: Pregunta; + @Column({ type: 'int' }) id_pregunta: number; - @ManyToOne(() => Seccion, (seccion) => seccion.id_seccion) + @ManyToOne(() => Seccion, (seccion) => seccion.seccionPreguntas) + seccion: Seccion; + @Column({ type: 'int' }) id_seccion: number; } diff --git a/src/seccion_pregunta/seccion_pregunta.module.ts b/src/seccion_pregunta/seccion_pregunta.module.ts index d911c60..5564e86 100644 --- a/src/seccion_pregunta/seccion_pregunta.module.ts +++ b/src/seccion_pregunta/seccion_pregunta.module.ts @@ -1,9 +1,19 @@ import { Module } from '@nestjs/common'; import { SeccionPreguntaService } from './seccion_pregunta.service'; import { SeccionPreguntaController } from './seccion_pregunta.controller'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { SeccionPregunta } from './entities/seccion_pregunta.entity'; +import { PreguntaModule } from '../pregunta/pregunta.module'; +import { SeccionModule } from '../seccion/seccion.module'; @Module({ + imports: [ + TypeOrmModule.forFeature([SeccionPregunta]), + PreguntaModule, + SeccionModule + ], controllers: [SeccionPreguntaController], providers: [SeccionPreguntaService], + exports: [TypeOrmModule] }) export class SeccionPreguntaModule {} diff --git a/src/tipo_pregunta/entities/tipo_pregunta.entity.ts b/src/tipo_pregunta/entities/tipo_pregunta.entity.ts index 178c993..e6bea46 100644 --- a/src/tipo_pregunta/entities/tipo_pregunta.entity.ts +++ b/src/tipo_pregunta/entities/tipo_pregunta.entity.ts @@ -9,6 +9,6 @@ export class TipoPregunta { @Column() tipo_pregunta: string; - @OneToMany(() => Pregunta, pregunta => pregunta.id_tipo_pregunta) + @OneToMany(() => Pregunta, pregunta => pregunta.tipoPregunta) preguntas: Pregunta[]; } \ No newline at end of file diff --git a/src/tipo_pregunta/tipo_pregunta.module.ts b/src/tipo_pregunta/tipo_pregunta.module.ts index 0494c98..0e23b66 100644 --- a/src/tipo_pregunta/tipo_pregunta.module.ts +++ b/src/tipo_pregunta/tipo_pregunta.module.ts @@ -1,9 +1,13 @@ import { Module } from '@nestjs/common'; import { TipoPreguntaService } from './tipo_pregunta.service'; import { TipoPreguntaController } from './tipo_pregunta.controller'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { TipoPregunta } from './entities/tipo_pregunta.entity'; @Module({ + imports: [TypeOrmModule.forFeature([TipoPregunta])], controllers: [TipoPreguntaController], providers: [TipoPreguntaService], + exports: [TypeOrmModule] }) export class TipoPreguntaModule {} diff --git a/utils/crear_formulario.json b/utils/crear_formulario.json new file mode 100644 index 0000000..e69de29 diff --git a/utils/crear_formulario_feria.json b/utils/crear_formulario_feria.json new file mode 100644 index 0000000..e69de29 diff --git a/utils/mandar_formulario.json b/utils/mandar_formulario.json new file mode 100644 index 0000000..e69de29 diff --git a/utils/respuesta_1.json b/utils/respuesta_1.json new file mode 100644 index 0000000..e69de29 diff --git a/utils/respuesta_2.json b/utils/respuesta_2.json new file mode 100644 index 0000000..e69de29 -- 2.43.0 From a1f52daa8afd243234da2590a9c8808b6d47149f Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 2 Apr 2025 11:50:08 -0600 Subject: [PATCH 2/3] swagger --- Dockerfile | 21 +- docker-compose.yml | 5 + package-lock.json | 53 ++++- package.json | 2 + src/cuestionario/cuestionario.controller.ts | 7 + .../cuestionario.documentation.ts | 181 +++++++++++++++++ src/cuestionario/cuestionario.module.ts | 22 +- src/cuestionario/cuestionario.service.ts | 149 +++++++++++++- .../dto/create-cuestionario.dto.ts | 32 ++- .../entities/cuestionario.entity.ts | 6 +- src/docs/swagger.config.ts | 26 ++- src/main.ts | 44 +++- src/pregunta/dto/create-pregunta.dto.ts | 37 +++- src/pregunta/entities/pregunta.entity.ts | 4 +- src/pregunta/pregunta.controller.ts | 7 + src/pregunta/pregunta.documentation.ts | 189 ++++++++++++++++++ src/seccion/dto/create-seccion.dto.ts | 24 ++- src/seccion/entities/seccion.entity.ts | 2 +- src/seccion/seccion.controller.ts | 7 + src/seccion/seccion.documentation.ts | 185 +++++++++++++++++ utils/crear_formulario.json | 0 utils/crear_formulario_feria.json | 0 utils/crear_formulario_feria.ts | 53 +++++ utils/load-form.js | 46 +++++ utils/respuesta_formulario_feria.ts | 21 ++ utils/test-crear-formulario.ts | 28 +++ 26 files changed, 1109 insertions(+), 42 deletions(-) create mode 100644 src/cuestionario/cuestionario.documentation.ts create mode 100644 src/pregunta/pregunta.documentation.ts create mode 100644 src/seccion/seccion.documentation.ts delete mode 100644 utils/crear_formulario.json delete mode 100644 utils/crear_formulario_feria.json create mode 100644 utils/crear_formulario_feria.ts create mode 100644 utils/load-form.js create mode 100644 utils/respuesta_formulario_feria.ts create mode 100644 utils/test-crear-formulario.ts diff --git a/Dockerfile b/Dockerfile index 4f67ac3..0b618ea 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ -# Etapa de construcción -FROM node:22-alpine AS builder +# Etapa de desarrollo para ejecutar en watch mode +FROM node:20-alpine WORKDIR /app @@ -7,17 +7,12 @@ COPY package*.json ./ RUN npm install COPY . . + +# Asegurarse de compilar el proyecto RUN npm run build -# Etapa de producción -FROM node:20-alpine AS production +# Puerto expuesto +EXPOSE 3000 -WORKDIR /app - -COPY --from=builder /app/node_modules ./node_modules -COPY --from=builder /app/dist ./dist -COPY --from=builder /app/package*.json ./ - -EXPOSE 4100 - -CMD ["node", "dist/main"] +# Comando para iniciar en modo desarrollo +CMD ["npm", "run", "start:dev"] diff --git a/docker-compose.yml b/docker-compose.yml index ab95035..8e9a574 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,6 +11,7 @@ services: MARIADB_DATABASE: formularios_test volumes: - mariadb_data:/var/lib/mysql + restart: unless-stopped api: build: @@ -19,10 +20,14 @@ services: container_name: formularios_api env_file: - .env + volumes: + - .:/app + - /app/node_modules ports: - "4200:3000" depends_on: - mariadb + restart: unless-stopped volumes: mariadb_data: diff --git a/package-lock.json b/package-lock.json index 8815789..7f12beb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,8 @@ "@nestjs/platform-express": "^11.0.1", "@nestjs/swagger": "^11.1.0", "@nestjs/typeorm": "^11.0.0", + "axios": "^1.8.4", + "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "dotenv": "^16.4.7", "mysql2": "^3.14.0", @@ -4191,7 +4193,6 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, "license": "MIT" }, "node_modules/aws-ssl-profiles": { @@ -4203,6 +4204,17 @@ "node": ">= 6.0.0" } }, + "node_modules/axios": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", + "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/b4a": { "version": "1.6.7", "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", @@ -4803,6 +4815,12 @@ "dev": true, "license": "MIT" }, + "node_modules/class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", + "license": "MIT" + }, "node_modules/class-validator": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.1.tgz", @@ -4968,7 +4986,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" @@ -5299,7 +5316,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.4.0" @@ -5543,7 +5559,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -6321,6 +6336,26 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -6434,7 +6469,6 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", - "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -6460,7 +6494,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -6470,7 +6503,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "license": "MIT", "dependencies": { "mime-db": "1.52.0" @@ -6824,7 +6856,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -9262,6 +9293,12 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", diff --git a/package.json b/package.json index f18668e..539948a 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,8 @@ "@nestjs/platform-express": "^11.0.1", "@nestjs/swagger": "^11.1.0", "@nestjs/typeorm": "^11.0.0", + "axios": "^1.8.4", + "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "dotenv": "^16.4.7", "mysql2": "^3.14.0", diff --git a/src/cuestionario/cuestionario.controller.ts b/src/cuestionario/cuestionario.controller.ts index 82adea6..ff9a3b2 100644 --- a/src/cuestionario/cuestionario.controller.ts +++ b/src/cuestionario/cuestionario.controller.ts @@ -2,32 +2,39 @@ import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/commo import { CuestionarioService } from './cuestionario.service'; import { CreateCuestionarioDto } from './dto/create-cuestionario.dto'; import { UpdateCuestionarioDto } from './dto/update-cuestionario.dto'; +import { CuestionarioApiDocumentation } from './cuestionario.documentation'; @Controller('cuestionario') +@CuestionarioApiDocumentation.ApiController export class CuestionarioController { constructor(private readonly cuestionarioService: CuestionarioService) {} @Post() + @CuestionarioApiDocumentation.ApiCreate create(@Body() createCuestionarioDto: CreateCuestionarioDto) { return this.cuestionarioService.create(createCuestionarioDto); } @Get() + @CuestionarioApiDocumentation.ApiGetAll findAll() { return this.cuestionarioService.findAll(); } @Get(':id') + @CuestionarioApiDocumentation.ApiGetOne findOne(@Param('id') id: string) { return this.cuestionarioService.findOne(+id); } @Patch(':id') + @CuestionarioApiDocumentation.ApiUpdate update(@Param('id') id: string, @Body() updateCuestionarioDto: UpdateCuestionarioDto) { return this.cuestionarioService.update(+id, updateCuestionarioDto); } @Delete(':id') + @CuestionarioApiDocumentation.ApiRemove remove(@Param('id') id: string) { return this.cuestionarioService.remove(+id); } diff --git a/src/cuestionario/cuestionario.documentation.ts b/src/cuestionario/cuestionario.documentation.ts new file mode 100644 index 0000000..3b8cc7d --- /dev/null +++ b/src/cuestionario/cuestionario.documentation.ts @@ -0,0 +1,181 @@ +import { ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { feriaSexualidad } from '../../utils/crear_formulario_feria'; +import { applyDecorators } from '@nestjs/common'; + +export class CuestionarioApiDocumentation { + // Decoradores para toda la clase del controlador + static ApiController = ApiTags('Cuestionarios'); + + // Documentación para crear un cuestionario + static ApiCreate = applyDecorators( + ApiOperation({ + summary: 'Crear un nuevo cuestionario', + description: 'Crea un nuevo cuestionario con secciones, preguntas y opciones' + }), + ApiBody({ + description: 'Datos del cuestionario a crear', + examples: { + feriaSexualidad: { + summary: 'Ejemplo de cuestionario para feria de sexualidad', + description: 'Cuestionario con secciones, preguntas y opciones para feria de sexualidad', + value: feriaSexualidad + } + } + }), + ApiResponse({ + status: 201, + description: 'Cuestionario creado correctamente', + schema: { + properties: { + success: { type: 'boolean', example: true }, + cuestionario: { + type: 'object', + properties: { + id_cuestionario: { type: 'number', example: 1 }, + nombre_form: { type: 'string', example: 'Registro para la Feria de la Sexualidad - FES Acatlán' }, + descripcion: { type: 'string' }, + contador_secciones: { type: 'number', example: 1 }, + editable: { type: 'boolean', example: true }, + fecha_fin: { type: 'string', format: 'date-time' }, + fecha_inicio: { type: 'string', format: 'date-time' }, + id_tipo_cuestionario: { type: 'number', example: 1 } + } + } + } + } + }), + ApiResponse({ status: 400, description: 'Datos del cuestionario inválidos' }), + ApiResponse({ status: 500, description: 'Error interno del servidor' }) + ); + + // Documentación para obtener todos los cuestionarios + static ApiGetAll = applyDecorators( + ApiOperation({ + summary: 'Obtener todos los cuestionarios', + description: 'Retorna una lista de todos los cuestionarios registrados' + }), + ApiResponse({ + status: 200, + description: 'Lista de cuestionarios obtenida correctamente', + schema: { + type: 'array', + items: { + type: 'object', + properties: { + id_cuestionario: { type: 'number', example: 1 }, + nombre_form: { type: 'string', example: 'Registro para la Feria de la Sexualidad - FES Acatlán' }, + descripcion: { type: 'string' }, + contador_secciones: { type: 'number', example: 1 }, + editable: { type: 'boolean', example: true }, + fecha_fin: { type: 'string', format: 'date-time' }, + fecha_inicio: { type: 'string', format: 'date-time' }, + id_tipo_cuestionario: { type: 'number', example: 1 } + } + } + } + }), + ApiResponse({ status: 500, description: 'Error interno del servidor' }) + ); + + // Documentación para obtener un cuestionario por ID + static ApiGetOne = applyDecorators( + ApiOperation({ + summary: 'Obtener un cuestionario por ID', + description: 'Retorna un cuestionario específico por su ID' + }), + ApiParam({ + name: 'id', + description: 'ID del cuestionario', + required: true, + type: 'number', + example: 1 + }), + ApiResponse({ + status: 200, + description: 'Cuestionario obtenido correctamente', + schema: { + type: 'object', + properties: { + id_cuestionario: { type: 'number', example: 1 }, + nombre_form: { type: 'string', example: 'Registro para la Feria de la Sexualidad - FES Acatlán' }, + descripcion: { type: 'string' }, + contador_secciones: { type: 'number', example: 1 }, + editable: { type: 'boolean', example: true }, + fecha_fin: { type: 'string', format: 'date-time' }, + fecha_inicio: { type: 'string', format: 'date-time' }, + id_tipo_cuestionario: { type: 'number', example: 1 } + } + } + }), + ApiResponse({ status: 404, description: 'Cuestionario no encontrado' }), + ApiResponse({ status: 500, description: 'Error interno del servidor' }) + ); + + // Documentación para actualizar un cuestionario + static ApiUpdate = applyDecorators( + ApiOperation({ + summary: 'Actualizar un cuestionario', + description: 'Actualiza los datos de un cuestionario existente' + }), + ApiParam({ + name: 'id', + description: 'ID del cuestionario a actualizar', + required: true, + type: 'number', + example: 1 + }), + ApiBody({ + description: 'Datos a actualizar del cuestionario', + schema: { + type: 'object', + properties: { + nombre_form: { type: 'string', example: 'Nombre actualizado del formulario' }, + descripcion: { type: 'string', example: 'Descripción actualizada' }, + fecha_inicio: { type: 'string', format: 'date-time' }, + fecha_fin: { type: 'string', format: 'date-time' }, + editable: { type: 'boolean', example: true } + } + } + }), + ApiResponse({ + status: 200, + description: 'Cuestionario actualizado correctamente', + schema: { + type: 'object', + properties: { + affected: { type: 'number', example: 1 } + } + } + }), + ApiResponse({ status: 400, description: 'Datos de actualización inválidos' }), + ApiResponse({ status: 404, description: 'Cuestionario no encontrado' }), + ApiResponse({ status: 500, description: 'Error interno del servidor' }) + ); + + // Documentación para eliminar un cuestionario + static ApiRemove = applyDecorators( + ApiOperation({ + summary: 'Eliminar un cuestionario', + description: 'Elimina permanentemente un cuestionario por su ID' + }), + ApiParam({ + name: 'id', + description: 'ID del cuestionario a eliminar', + required: true, + type: 'number', + example: 1 + }), + ApiResponse({ + status: 200, + description: 'Cuestionario eliminado correctamente', + schema: { + type: 'object', + properties: { + affected: { type: 'number', example: 1 } + } + } + }), + ApiResponse({ status: 404, description: 'Cuestionario no encontrado' }), + ApiResponse({ status: 500, description: 'Error interno del servidor' }) + ); +} \ No newline at end of file diff --git a/src/cuestionario/cuestionario.module.ts b/src/cuestionario/cuestionario.module.ts index c6ba022..642215f 100644 --- a/src/cuestionario/cuestionario.module.ts +++ b/src/cuestionario/cuestionario.module.ts @@ -3,11 +3,29 @@ import { CuestionarioService } from './cuestionario.service'; import { CuestionarioController } from './cuestionario.controller'; import { TypeOrmModule } from '@nestjs/typeorm'; import { Cuestionario } from './entities/cuestionario.entity'; +import { Seccion } from '../seccion/entities/seccion.entity'; +import { CuestionarioSeccion } from '../cuestionario_seccion/entities/cuestionario_seccion.entity'; +import { Pregunta } from '../pregunta/entities/pregunta.entity'; +import { SeccionPregunta } from '../seccion_pregunta/entities/seccion_pregunta.entity'; +import { Opcion } from '../opcion/entities/opcion.entity'; +import { PreguntaOpcion } from '../pregunta_opcion/entities/pregunta_opcion.entity'; +import { TipoPregunta } from '../tipo_pregunta/entities/tipo_pregunta.entity'; @Module({ - imports: [TypeOrmModule.forFeature([Cuestionario])], + imports: [ + TypeOrmModule.forFeature([ + Cuestionario, + Seccion, + CuestionarioSeccion, + Pregunta, + SeccionPregunta, + Opcion, + PreguntaOpcion, + TipoPregunta + ]) + ], controllers: [CuestionarioController], providers: [CuestionarioService], - exports: [TypeOrmModule] + exports: [TypeOrmModule, CuestionarioService] }) export class CuestionarioModule {} diff --git a/src/cuestionario/cuestionario.service.ts b/src/cuestionario/cuestionario.service.ts index 6a8f5ac..784cd9d 100644 --- a/src/cuestionario/cuestionario.service.ts +++ b/src/cuestionario/cuestionario.service.ts @@ -1,26 +1,163 @@ import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository, DataSource } from 'typeorm'; import { CreateCuestionarioDto } from './dto/create-cuestionario.dto'; import { UpdateCuestionarioDto } from './dto/update-cuestionario.dto'; +import { Cuestionario } from './entities/cuestionario.entity'; +import { Seccion } from '../seccion/entities/seccion.entity'; +import { CuestionarioSeccion } from '../cuestionario_seccion/entities/cuestionario_seccion.entity'; +import { Pregunta } from '../pregunta/entities/pregunta.entity'; +import { SeccionPregunta } from '../seccion_pregunta/entities/seccion_pregunta.entity'; +import { Opcion } from '../opcion/entities/opcion.entity'; +import { PreguntaOpcion } from '../pregunta_opcion/entities/pregunta_opcion.entity'; +import { TipoPregunta } from '../tipo_pregunta/entities/tipo_pregunta.entity'; @Injectable() export class CuestionarioService { - create(createCuestionarioDto: CreateCuestionarioDto) { - return 'This action adds a new cuestionario'; + constructor( + @InjectRepository(Cuestionario) + private cuestionarioRepository: Repository, + @InjectRepository(Seccion) + private seccionRepository: Repository, + @InjectRepository(CuestionarioSeccion) + private cuestionarioSeccionRepository: Repository, + @InjectRepository(Pregunta) + private preguntaRepository: Repository, + @InjectRepository(SeccionPregunta) + private seccionPreguntaRepository: Repository, + @InjectRepository(Opcion) + private opcionRepository: Repository, + @InjectRepository(PreguntaOpcion) + private preguntaOpcionRepository: Repository, + @InjectRepository(TipoPregunta) + private tipoPreguntaRepository: Repository, + private dataSource: DataSource, + ) {} + + async create(createCuestionarioDto: CreateCuestionarioDto) { + const queryRunner = this.dataSource.createQueryRunner(); + await queryRunner.connect(); + await queryRunner.startTransaction(); + + try { + // 1. Crear cuestionario + const cuestionario = new Cuestionario(); + cuestionario.nombre_form = createCuestionarioDto.nombre_form; + cuestionario.descripcion = createCuestionarioDto.descripcion; + cuestionario.fecha_inicio = createCuestionarioDto.fecha_inicio ? new Date(createCuestionarioDto.fecha_inicio) : undefined; + cuestionario.fecha_fin = createCuestionarioDto.fecha_fin ? new Date(createCuestionarioDto.fecha_fin) : undefined; + cuestionario.id_tipo_cuestionario = createCuestionarioDto.id_tipo_cuestionario; + cuestionario.contador_secciones = createCuestionarioDto.secciones?.length || 0; + cuestionario.editable = true; + + const savedCuestionario = await queryRunner.manager.save(cuestionario); + + // 2. Crear secciones y vincularlas al cuestionario + if (createCuestionarioDto.secciones && createCuestionarioDto.secciones.length > 0) { + for (let i = 0; i < createCuestionarioDto.secciones.length; i++) { + const seccionDto = createCuestionarioDto.secciones[i]; + + // Crear sección + const seccion = new Seccion(); + seccion.titulo = seccionDto.titulo; + seccion.descripcion = seccionDto.descripcion; + seccion.contador_pregunta = seccionDto.preguntas?.length || 0; + + const savedSeccion = await queryRunner.manager.save(seccion); + + // Vincular sección con cuestionario + const cuestionarioSeccion = new CuestionarioSeccion(); + cuestionarioSeccion.id_cuestionario = savedCuestionario.id_cuestionario; + cuestionarioSeccion.id_seccion = savedSeccion.id_seccion; + cuestionarioSeccion.posicion = i + 1; + + await queryRunner.manager.save(cuestionarioSeccion); + + // 3. Crear preguntas y vincularlas a la sección + if (seccionDto.preguntas && seccionDto.preguntas.length > 0) { + for (let j = 0; j < seccionDto.preguntas.length; j++) { + const preguntaDto = seccionDto.preguntas[j]; + + // Obtener el tipo de pregunta por su nombre + const tipoPregunta = await queryRunner.manager.findOne(TipoPregunta, { + where: { tipo_pregunta: preguntaDto.tipo }, + }); + + if (!tipoPregunta) { + throw new Error(`Tipo de pregunta '${preguntaDto.tipo}' no encontrado`); + } + + // Crear pregunta + const pregunta = new Pregunta(); + pregunta.pregunta = preguntaDto.titulo; + pregunta.obligatoria = preguntaDto.obligatoria || false; + pregunta.id_tipo_pregunta = tipoPregunta.id_tipo; + pregunta.id_opcion_dependiente = preguntaDto.id_opcion_dependiente || undefined; + pregunta.contador_opcion = preguntaDto.opciones?.length || 0; + + const savedPregunta = await queryRunner.manager.save(pregunta); + + // Vincular pregunta con sección + const seccionPregunta = new SeccionPregunta(); + seccionPregunta.id_seccion = savedSeccion.id_seccion; + seccionPregunta.id_pregunta = savedPregunta.id_pregunta; + seccionPregunta.posicion = j + 1; + + await queryRunner.manager.save(seccionPregunta); + + // 4. Crear opciones para preguntas de tipo multiple/radio + if (preguntaDto.opciones && preguntaDto.opciones.length > 0) { + for (let k = 0; k < preguntaDto.opciones.length; k++) { + const opcionDto = preguntaDto.opciones[k]; + + // Crear opción + const opcion = new Opcion(); + opcion.opcion = opcionDto.valor; + + const savedOpcion = await queryRunner.manager.save(opcion); + + // Vincular opción con pregunta + const preguntaOpcion = new PreguntaOpcion(); + preguntaOpcion.id_pregunta = savedPregunta.id_pregunta; + // Utilizamos la opción como objeto, no como ID + preguntaOpcion.opcion = savedOpcion; + preguntaOpcion.posicion = k + 1; + + await queryRunner.manager.save(preguntaOpcion); + } + } + } + } + } + } + + await queryRunner.commitTransaction(); + + return { + success: true, + cuestionario: savedCuestionario, + }; + } catch (error) { + await queryRunner.rollbackTransaction(); + throw error; + } finally { + await queryRunner.release(); + } } findAll() { - return `This action returns all cuestionario`; + return this.cuestionarioRepository.find(); } findOne(id: number) { - return `This action returns a #${id} cuestionario`; + return this.cuestionarioRepository.findOne({ where: { id_cuestionario: id } }); } update(id: number, updateCuestionarioDto: UpdateCuestionarioDto) { - return `This action updates a #${id} cuestionario`; + return this.cuestionarioRepository.update(id, updateCuestionarioDto); } remove(id: number) { - return `This action removes a #${id} cuestionario`; + return this.cuestionarioRepository.delete(id); } } diff --git a/src/cuestionario/dto/create-cuestionario.dto.ts b/src/cuestionario/dto/create-cuestionario.dto.ts index d752739..2930547 100644 --- a/src/cuestionario/dto/create-cuestionario.dto.ts +++ b/src/cuestionario/dto/create-cuestionario.dto.ts @@ -1 +1,31 @@ -export class CreateCuestionarioDto {} +import { CreateSeccionDto } from '../../seccion/dto/create-seccion.dto'; +import { IsArray, IsDateString, IsNotEmpty, IsNumber, IsOptional, IsString, ValidateNested } from 'class-validator'; +import { Type } from 'class-transformer'; + +export class CreateCuestionarioDto { + @IsNotEmpty() + @IsString() + nombre_form: string; + + @IsOptional() + @IsString() + descripcion?: string; + + @IsOptional() + @IsDateString() + fecha_inicio?: Date; + + @IsOptional() + @IsDateString() + fecha_fin?: Date; + + @IsNotEmpty() + @IsNumber() + id_tipo_cuestionario: number; + + @IsOptional() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => CreateSeccionDto) + secciones?: CreateSeccionDto[]; +} diff --git a/src/cuestionario/entities/cuestionario.entity.ts b/src/cuestionario/entities/cuestionario.entity.ts index f755b13..d7559d8 100644 --- a/src/cuestionario/entities/cuestionario.entity.ts +++ b/src/cuestionario/entities/cuestionario.entity.ts @@ -16,16 +16,16 @@ export class Cuestionario { contador_secciones: number; @Column({ type: 'text', nullable: true }) - descripcion: string; + descripcion?: string; @Column({ type: 'boolean', default: true }) editable: boolean; @Column({ type: 'datetime', nullable: true }) - fecha_fin: Date; + fecha_fin?: Date; @Column({ type: 'datetime', nullable: true }) - fecha_inicio: Date; + fecha_inicio?: Date; @OneToMany(() => CuestionarioSeccion,(cuestionario_seccion) => cuestionario_seccion.id_cuestionario) diff --git a/src/docs/swagger.config.ts b/src/docs/swagger.config.ts index e0e99a1..2f0a0b2 100644 --- a/src/docs/swagger.config.ts +++ b/src/docs/swagger.config.ts @@ -1,11 +1,31 @@ import { DocumentBuilder } from '@nestjs/swagger'; export const swaggerConfig = new DocumentBuilder() - .setTitle('Sistema de Registro de Usuarios y Eventos') - .setDescription('API para gestionar usuarios, eventos, asistencia y códigos QR') + .setTitle('Sistema de Formularios y Eventos CIDWA') + .setDescription(` + API para la gestión de formularios, cuestionarios, eventos y asistencias. + + ## Funcionalidades principales + + - **Cuestionarios**: Creación y gestión de formularios con secciones y preguntas + - **Eventos**: Registro y control de eventos + - **Participantes**: Gestión de usuarios y participantes + - **Asistencia**: Control de asistencia a eventos + - **QR**: Generación y validación de códigos QR + + ## Ejemplos de uso + + La API permite crear cuestionarios complejos con secciones y diferentes tipos de preguntas. + Consulte la documentación de cada endpoint para ver ejemplos específicos. + `) .setVersion('1.0') + .addTag('Cuestionarios') + .addTag('Secciones') + .addTag('Preguntas') + .addTag('Opciones') .addTag('Usuarios') .addTag('Eventos') .addTag('Asistencia') - .addTag('QR') + .addTag('QR') + .addBearerAuth() .build(); diff --git a/src/main.ts b/src/main.ts index 3eabdcd..3f43b41 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,13 +2,55 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import 'dotenv/config'; import { DocsModule } from './docs/docs.module'; +import { DataSource } from 'typeorm'; +import { TipoPregunta } from './tipo_pregunta/entities/tipo_pregunta.entity'; +import { ValidationPipe } from '@nestjs/common'; async function bootstrap() { const app = await NestFactory.create(AppModule); - //await app.listen(process.env.PORT ?? 3000); + + // Configuración de validación global + app.useGlobalPipes( + new ValidationPipe({ + whitelist: true, // Elimina propiedades no decoradas + forbidNonWhitelisted: true, // Arroja error si hay propiedades no decoradas + transform: true, // Transforma los datos recibidos al tipo definido en el DTO + }) + ); + + // CORS configuration + app.enableCors({ + origin: '*', + methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'], + allowedHeaders: ['Content-Type', 'Authorization'], + }); + // Swagger setup DocsModule.setupSwagger(app); + // Seed TipoPregunta data + const dataSource = app.get(DataSource); + const tiposPreguntaRepository = dataSource.getRepository(TipoPregunta); + + // Check if data exists + const count = await tiposPreguntaRepository.count(); + + if (count === 0) { + // Create default tipos de pregunta + const tiposPregunta = [ + { tipo_pregunta: 'Texto' }, + { tipo_pregunta: 'Numero' }, + { tipo_pregunta: 'Radio' }, + { tipo_pregunta: 'Multiple' }, + { tipo_pregunta: 'Abierto' }, + { tipo_pregunta: 'Fecha' }, + ]; + + await tiposPreguntaRepository.save(tiposPregunta); + console.log('Tipos de pregunta creados'); + } + + // Start the server await app.listen(process.env.PORT ?? 3000); console.log('API en ejecución en http://localhost:3000'); console.log('Swagger disponible en http://localhost:3000/api-docs'); diff --git a/src/pregunta/dto/create-pregunta.dto.ts b/src/pregunta/dto/create-pregunta.dto.ts index ee5d15c..fd6c832 100644 --- a/src/pregunta/dto/create-pregunta.dto.ts +++ b/src/pregunta/dto/create-pregunta.dto.ts @@ -1 +1,36 @@ -export class CreatePreguntaDto {} +import { Type } from 'class-transformer'; +import { IsArray, IsBoolean, IsNotEmpty, IsNumber, IsOptional, IsString, ValidateNested } from 'class-validator'; + +export class OpcionDto { + @IsNotEmpty() + @IsString() + valor: string; +} + +export class CreatePreguntaDto { + @IsNotEmpty() + @IsString() + titulo: string; + + @IsOptional() + @IsBoolean() + obligatoria?: boolean; + + @IsNotEmpty() + @IsString() + tipo: string; + + @IsOptional() + @IsNumber() + contador_opcion?: number; + + @IsOptional() + @IsNumber() + id_opcion_dependiente?: number; + + @IsOptional() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => OpcionDto) + opciones?: OpcionDto[]; +} diff --git a/src/pregunta/entities/pregunta.entity.ts b/src/pregunta/entities/pregunta.entity.ts index 1b67797..a912496 100644 --- a/src/pregunta/entities/pregunta.entity.ts +++ b/src/pregunta/entities/pregunta.entity.ts @@ -25,8 +25,8 @@ export class Pregunta { @Column() id_tipo_pregunta: number; - @Column({ nullable: true }) - id_opcion_dependiente: number; + @Column({ type: 'int', nullable: true }) + id_opcion_dependiente?: number; @OneToMany(() => SeccionPregunta, seccionPregunta => seccionPregunta.pregunta) seccionPreguntas: SeccionPregunta[]; diff --git a/src/pregunta/pregunta.controller.ts b/src/pregunta/pregunta.controller.ts index c5672b4..b3c6757 100644 --- a/src/pregunta/pregunta.controller.ts +++ b/src/pregunta/pregunta.controller.ts @@ -2,32 +2,39 @@ import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/commo import { PreguntaService } from './pregunta.service'; import { CreatePreguntaDto } from './dto/create-pregunta.dto'; import { UpdatePreguntaDto } from './dto/update-pregunta.dto'; +import { PreguntaApiDocumentation } from './pregunta.documentation'; @Controller('pregunta') +@PreguntaApiDocumentation.ApiController export class PreguntaController { constructor(private readonly preguntaService: PreguntaService) {} @Post() + @PreguntaApiDocumentation.ApiCreate create(@Body() createPreguntaDto: CreatePreguntaDto) { return this.preguntaService.create(createPreguntaDto); } @Get() + @PreguntaApiDocumentation.ApiGetAll findAll() { return this.preguntaService.findAll(); } @Get(':id') + @PreguntaApiDocumentation.ApiGetOne findOne(@Param('id') id: string) { return this.preguntaService.findOne(+id); } @Patch(':id') + @PreguntaApiDocumentation.ApiUpdate update(@Param('id') id: string, @Body() updatePreguntaDto: UpdatePreguntaDto) { return this.preguntaService.update(+id, updatePreguntaDto); } @Delete(':id') + @PreguntaApiDocumentation.ApiRemove remove(@Param('id') id: string) { return this.preguntaService.remove(+id); } diff --git a/src/pregunta/pregunta.documentation.ts b/src/pregunta/pregunta.documentation.ts new file mode 100644 index 0000000..208879a --- /dev/null +++ b/src/pregunta/pregunta.documentation.ts @@ -0,0 +1,189 @@ +import { ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { applyDecorators } from '@nestjs/common'; + +export class PreguntaApiDocumentation { + // Decoradores para toda la clase del controlador + static ApiController = ApiTags('Preguntas'); + + // Documentación para crear una pregunta + static ApiCreate = applyDecorators( + ApiOperation({ + summary: 'Crear una nueva pregunta', + description: 'Crea una nueva pregunta para una sección' + }), + ApiBody({ + description: 'Datos de la pregunta a crear', + schema: { + type: 'object', + required: ['titulo', 'tipo'], + properties: { + titulo: { type: 'string', example: '¿Eres parte de la comunidad?' }, + obligatoria: { type: 'boolean', example: true }, + tipo: { type: 'string', example: 'Multiple' }, + contador_opcion: { type: 'number', example: 0 }, + id_opcion_dependiente: { type: 'number', example: null }, + opciones: { + type: 'array', + items: { + type: 'object', + properties: { + valor: { type: 'string', example: 'Si' } + } + } + } + } + } + }), + ApiResponse({ + status: 201, + description: 'Pregunta creada correctamente', + schema: { + type: 'object', + properties: { + id_pregunta: { type: 'number', example: 1 }, + pregunta: { type: 'string', example: '¿Eres parte de la comunidad?' }, + 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 } + } + } + }), + ApiResponse({ status: 400, description: 'Datos de la pregunta inválidos' }), + ApiResponse({ status: 500, description: 'Error interno del servidor' }) + ); + + // Documentación para obtener todas las preguntas + static ApiGetAll = applyDecorators( + ApiOperation({ + summary: 'Obtener todas las preguntas', + description: 'Retorna una lista de todas las preguntas registradas' + }), + ApiResponse({ + status: 200, + description: 'Lista de preguntas obtenida correctamente', + schema: { + type: 'array', + items: { + type: 'object', + properties: { + id_pregunta: { type: 'number', example: 1 }, + pregunta: { type: 'string', example: '¿Eres parte de la comunidad?' }, + 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 } + } + } + } + }), + ApiResponse({ status: 500, description: 'Error interno del servidor' }) + ); + + // Documentación para obtener una pregunta por ID + static ApiGetOne = applyDecorators( + ApiOperation({ + summary: 'Obtener una pregunta por ID', + description: 'Retorna una pregunta específica por su ID' + }), + ApiParam({ + name: 'id', + description: 'ID de la pregunta', + required: true, + type: 'number', + example: 1 + }), + ApiResponse({ + status: 200, + description: 'Pregunta obtenida correctamente', + schema: { + type: 'object', + properties: { + id_pregunta: { type: 'number', example: 1 }, + pregunta: { type: 'string', example: '¿Eres parte de la comunidad?' }, + 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 }, + opciones: { + type: 'array', + items: { + type: 'object', + properties: { + id_opcion: { type: 'number', example: 1 }, + opcion: { type: 'string', example: 'Si' } + } + } + } + } + } + }), + ApiResponse({ status: 404, description: 'Pregunta no encontrada' }), + ApiResponse({ status: 500, description: 'Error interno del servidor' }) + ); + + // Documentación para actualizar una pregunta + static ApiUpdate = applyDecorators( + ApiOperation({ + summary: 'Actualizar una pregunta', + description: 'Actualiza los datos de una pregunta existente' + }), + ApiParam({ + name: 'id', + description: 'ID de la pregunta a actualizar', + required: true, + type: 'number', + example: 1 + }), + ApiBody({ + description: 'Datos a actualizar de la pregunta', + schema: { + type: 'object', + properties: { + pregunta: { type: 'string', example: 'Pregunta actualizada' }, + obligatoria: { type: 'boolean', example: false } + } + } + }), + ApiResponse({ + status: 200, + description: 'Pregunta actualizada correctamente', + schema: { + type: 'object', + properties: { + affected: { type: 'number', example: 1 } + } + } + }), + ApiResponse({ status: 400, description: 'Datos de actualización inválidos' }), + ApiResponse({ status: 404, description: 'Pregunta no encontrada' }), + ApiResponse({ status: 500, description: 'Error interno del servidor' }) + ); + + // Documentación para eliminar una pregunta + static ApiRemove = applyDecorators( + ApiOperation({ + summary: 'Eliminar una pregunta', + description: 'Elimina permanentemente una pregunta por su ID' + }), + ApiParam({ + name: 'id', + description: 'ID de la pregunta a eliminar', + required: true, + type: 'number', + example: 1 + }), + ApiResponse({ + status: 200, + description: 'Pregunta eliminada correctamente', + schema: { + type: 'object', + properties: { + affected: { type: 'number', example: 1 } + } + } + }), + ApiResponse({ status: 404, description: 'Pregunta no encontrada' }), + ApiResponse({ status: 500, description: 'Error interno del servidor' }) + ); +} \ No newline at end of file diff --git a/src/seccion/dto/create-seccion.dto.ts b/src/seccion/dto/create-seccion.dto.ts index 4753a9c..ccb44c1 100644 --- a/src/seccion/dto/create-seccion.dto.ts +++ b/src/seccion/dto/create-seccion.dto.ts @@ -1 +1,23 @@ -export class CreateSeccionDto {} +import { CreatePreguntaDto } from '../../pregunta/dto/create-pregunta.dto'; +import { IsArray, IsNotEmpty, IsNumber, IsOptional, IsString, ValidateNested } from 'class-validator'; +import { Type } from 'class-transformer'; + +export class CreateSeccionDto { + @IsNotEmpty() + @IsString() + titulo: string; + + @IsOptional() + @IsString() + descripcion?: string; + + @IsOptional() + @IsNumber() + contador_pregunta?: number; + + @IsOptional() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => CreatePreguntaDto) + preguntas?: CreatePreguntaDto[]; +} diff --git a/src/seccion/entities/seccion.entity.ts b/src/seccion/entities/seccion.entity.ts index 6e27cd6..a5c3b6d 100644 --- a/src/seccion/entities/seccion.entity.ts +++ b/src/seccion/entities/seccion.entity.ts @@ -11,7 +11,7 @@ export class Seccion { contador_pregunta: number; @Column({ type: 'text', nullable: true }) - descripcion: string; + descripcion?: string; @Column({ type: 'text', nullable: true }) titulo: string; diff --git a/src/seccion/seccion.controller.ts b/src/seccion/seccion.controller.ts index f3e3dcf..d3967c8 100644 --- a/src/seccion/seccion.controller.ts +++ b/src/seccion/seccion.controller.ts @@ -2,32 +2,39 @@ import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/commo import { SeccionService } from './seccion.service'; import { CreateSeccionDto } from './dto/create-seccion.dto'; import { UpdateSeccionDto } from './dto/update-seccion.dto'; +import { SeccionApiDocumentation } from './seccion.documentation'; @Controller('seccion') +@SeccionApiDocumentation.ApiController export class SeccionController { constructor(private readonly seccionService: SeccionService) {} @Post() + @SeccionApiDocumentation.ApiCreate create(@Body() createSeccionDto: CreateSeccionDto) { return this.seccionService.create(createSeccionDto); } @Get() + @SeccionApiDocumentation.ApiGetAll findAll() { return this.seccionService.findAll(); } @Get(':id') + @SeccionApiDocumentation.ApiGetOne findOne(@Param('id') id: string) { return this.seccionService.findOne(+id); } @Patch(':id') + @SeccionApiDocumentation.ApiUpdate update(@Param('id') id: string, @Body() updateSeccionDto: UpdateSeccionDto) { return this.seccionService.update(+id, updateSeccionDto); } @Delete(':id') + @SeccionApiDocumentation.ApiRemove remove(@Param('id') id: string) { return this.seccionService.remove(+id); } diff --git a/src/seccion/seccion.documentation.ts b/src/seccion/seccion.documentation.ts new file mode 100644 index 0000000..72d4976 --- /dev/null +++ b/src/seccion/seccion.documentation.ts @@ -0,0 +1,185 @@ +import { ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { applyDecorators } from '@nestjs/common'; + +export class SeccionApiDocumentation { + // Decoradores para toda la clase del controlador + static ApiController = ApiTags('Secciones'); + + // Documentación para crear una sección + static ApiCreate = applyDecorators( + ApiOperation({ + summary: 'Crear una nueva sección', + description: 'Crea una nueva sección para un cuestionario' + }), + ApiBody({ + description: 'Datos de la sección a crear', + schema: { + type: 'object', + required: ['titulo'], + properties: { + titulo: { type: 'string', example: 'Información Personal' }, + descripcion: { + type: 'string', + example: 'Proporciona tus datos personales para poder contactarte' + }, + contador_pregunta: { type: 'number', example: 0 }, + preguntas: { + type: 'array', + items: { + type: 'object', + properties: { + titulo: { type: 'string', example: '¿Eres parte de la comunidad?' }, + obligatoria: { type: 'boolean', example: true }, + tipo: { type: 'string', example: 'Multiple' }, + opciones: { + type: 'array', + items: { + type: 'object', + properties: { + valor: { type: 'string', example: 'Si' } + } + } + } + } + } + } + } + } + }), + ApiResponse({ + status: 201, + description: 'Sección creada correctamente', + schema: { + type: 'object', + properties: { + id_seccion: { type: 'number', example: 1 }, + titulo: { type: 'string', example: 'Información Personal' }, + descripcion: { type: 'string' }, + contador_pregunta: { type: 'number', example: 0 } + } + } + }), + ApiResponse({ status: 400, description: 'Datos de la sección inválidos' }), + ApiResponse({ status: 500, description: 'Error interno del servidor' }) + ); + + // Documentación para obtener todas las secciones + static ApiGetAll = applyDecorators( + ApiOperation({ + summary: 'Obtener todas las secciones', + description: 'Retorna una lista de todas las secciones registradas' + }), + ApiResponse({ + status: 200, + description: 'Lista de secciones obtenida correctamente', + schema: { + type: 'array', + items: { + type: 'object', + properties: { + id_seccion: { type: 'number', example: 1 }, + titulo: { type: 'string', example: 'Información Personal' }, + descripcion: { type: 'string' }, + contador_pregunta: { type: 'number', example: 0 } + } + } + } + }), + ApiResponse({ status: 500, description: 'Error interno del servidor' }) + ); + + // Documentación para obtener una sección por ID + static ApiGetOne = applyDecorators( + ApiOperation({ + summary: 'Obtener una sección por ID', + description: 'Retorna una sección específica por su ID' + }), + ApiParam({ + name: 'id', + description: 'ID de la sección', + required: true, + type: 'number', + example: 1 + }), + ApiResponse({ + status: 200, + description: 'Sección obtenida correctamente', + schema: { + type: 'object', + properties: { + id_seccion: { type: 'number', example: 1 }, + titulo: { type: 'string', example: 'Información Personal' }, + descripcion: { type: 'string' }, + contador_pregunta: { type: 'number', example: 0 } + } + } + }), + ApiResponse({ status: 404, description: 'Sección no encontrada' }), + ApiResponse({ status: 500, description: 'Error interno del servidor' }) + ); + + // Documentación para actualizar una sección + static ApiUpdate = applyDecorators( + ApiOperation({ + summary: 'Actualizar una sección', + description: 'Actualiza los datos de una sección existente' + }), + ApiParam({ + name: 'id', + description: 'ID de la sección a actualizar', + required: true, + type: 'number', + example: 1 + }), + ApiBody({ + description: 'Datos a actualizar de la sección', + schema: { + type: 'object', + properties: { + titulo: { type: 'string', example: 'Título actualizado' }, + descripcion: { type: 'string', example: 'Descripción actualizada' } + } + } + }), + ApiResponse({ + status: 200, + description: 'Sección actualizada correctamente', + schema: { + type: 'object', + properties: { + affected: { type: 'number', example: 1 } + } + } + }), + ApiResponse({ status: 400, description: 'Datos de actualización inválidos' }), + ApiResponse({ status: 404, description: 'Sección no encontrada' }), + ApiResponse({ status: 500, description: 'Error interno del servidor' }) + ); + + // Documentación para eliminar una sección + static ApiRemove = applyDecorators( + ApiOperation({ + summary: 'Eliminar una sección', + description: 'Elimina permanentemente una sección por su ID' + }), + ApiParam({ + name: 'id', + description: 'ID de la sección a eliminar', + required: true, + type: 'number', + example: 1 + }), + ApiResponse({ + status: 200, + description: 'Sección eliminada correctamente', + schema: { + type: 'object', + properties: { + affected: { type: 'number', example: 1 } + } + } + }), + ApiResponse({ status: 404, description: 'Sección no encontrada' }), + ApiResponse({ status: 500, description: 'Error interno del servidor' }) + ); +} \ No newline at end of file diff --git a/utils/crear_formulario.json b/utils/crear_formulario.json deleted file mode 100644 index e69de29..0000000 diff --git a/utils/crear_formulario_feria.json b/utils/crear_formulario_feria.json deleted file mode 100644 index e69de29..0000000 diff --git a/utils/crear_formulario_feria.ts b/utils/crear_formulario_feria.ts new file mode 100644 index 0000000..23fda26 --- /dev/null +++ b/utils/crear_formulario_feria.ts @@ -0,0 +1,53 @@ +export const feriaSexualidad = { + nombre_form: 'Registro para la Feria de la Sexualidad - FES Acatlán', + descripcion: + 'La Feria de la Sexualidad es un espacio seguro e informativo dirigido a toda la comunidad universitaria. Este registro nos ayudará a organizar mejor las actividades, talleres y charlas, así como conocer tus intereses. Por favor, completa el siguiente formulario para confirmar tu participación.', + fecha_inicio: '2025-03-07T00:00:01', + fecha_fin: '2025-03-18T23:59:59', + id_tipo_cuestionario: 1, + secciones: [ + { + titulo: 'Información Personal', + descripcion: + 'Proporciona tus datos personales para poder contactarte y enviarte información relevante sobre la Feria de la Sexualidad.', + preguntas: [ + { + titulo: '¿Eres parte de la comunidad de la FES Acatlán?', + tipo: 'Multiple', + opciones: [{ valor: 'Si' }, { valor: 'No' }], + obligatoria: true, + }, + { + titulo: 'Numero de cuenta', + obligatoria: true, + tipo: 'Numero', + }, + { + titulo: 'Nombre(s)', + obligatoria: true, + tipo: 'Texto', + }, + { + titulo: 'Apellidos', + obligatoria: true, + tipo: 'Texto', + }, + { + titulo: 'genero', + obligatoria: true, + tipo: 'Radio', + opciones: [ + { valor: 'Masculino' }, + { valor: 'Femenino' }, + { valor: 'Prefiero no decirlo' }, + ], + }, + { + titulo: 'Institución de procedencia', + tipo: 'Abierto', + obligatoria: true, + }, + ], + }, + ], + }; \ No newline at end of file diff --git a/utils/load-form.js b/utils/load-form.js new file mode 100644 index 0000000..800e54b --- /dev/null +++ b/utils/load-form.js @@ -0,0 +1,46 @@ +// Script para cargar un formulario desde un archivo JSON + +const axios = require('axios'); +const fs = require('fs'); +const path = require('path'); + +const API_URL = 'http://localhost:3000'; + +async function loadFormFromFile(filePath) { + try { + // Leer el archivo JSON + const formData = require(filePath); + + console.log('Enviando datos del formulario al servidor...'); + console.log('Datos:', JSON.stringify(formData, null, 2)); + + // Enviar los datos al endpoint + const response = await axios.post(`${API_URL}/cuestionario`, formData); + + console.log('Formulario creado exitosamente:'); + console.log(JSON.stringify(response.data, null, 2)); + + return response.data; + } catch (error) { + console.error('Error al crear el formulario:'); + if (error.response) { + console.error('Status:', error.response.status); + console.error('Data:', JSON.stringify(error.response.data, null, 2)); + } else { + console.error(error.message); + } + throw error; + } +} + +// Verificar si se proporcionó un argumento de archivo +const args = process.argv.slice(2); +const filePath = args[0] || './crear_formulario_feria'; + +// Ejecutar la función +loadFormFromFile(filePath) + .then(() => console.log('Proceso completado')) + .catch(() => { + console.log('Proceso fallido'); + process.exit(1); + }); \ No newline at end of file diff --git a/utils/respuesta_formulario_feria.ts b/utils/respuesta_formulario_feria.ts new file mode 100644 index 0000000..0a2b5fe --- /dev/null +++ b/utils/respuesta_formulario_feria.ts @@ -0,0 +1,21 @@ +const comunidad = { + id_formulario: 1, + respuestas: [ + { id_pregunta: 101, valor: 'Si' }, // ¿Eres parte de la comunidad de la FES Acatlán? + { id_pregunta: 102, valor: '123456789' }, // Numero de cuenta + { id_pregunta: 106, valor: 'Prefiero no decirlo' }, + ], + fecha_envio: '2025-04-02T13:45:00', + }; + + const externos = { + id_formulario: 1, + respuestas: [ + { id_pregunta: 101, valor: 'No' }, // ¿Eres parte de la comunidad de la FES Acatlán? + { id_pregunta: 103, valor: 'Juan Carlos' }, // Nombre(s) + { id_pregunta: 104, valor: 'Ramírez López' }, // Apellidos + { id_pregunta: 106, valor: 'Prefiero no decirlo' }, // genero + { id_pregunta: 107, valor: 'UAM Azcapotzalco' }, // Institución de procedencia + ], + fecha_envio: '2025-04-02T13:45:00', + }; \ No newline at end of file diff --git a/utils/test-crear-formulario.ts b/utils/test-crear-formulario.ts new file mode 100644 index 0000000..7e88c02 --- /dev/null +++ b/utils/test-crear-formulario.ts @@ -0,0 +1,28 @@ +import { feriaSexualidad } from './crear_formulario_feria'; +import axios from 'axios'; + +// Asumiendo que el servidor está corriendo en localhost:3000 +const API_URL = 'http://localhost:3000'; + +async function crearFormulario() { + try { + const response = await axios.post(`${API_URL}/cuestionario`, feriaSexualidad); + console.log('Formulario creado exitosamente:'); + console.log(JSON.stringify(response.data, null, 2)); + return response.data; + } catch (error) { + console.error('Error al crear el formulario:'); + if (error.response) { + console.error('Status:', error.response.status); + console.error('Data:', error.response.data); + } else { + console.error(error.message); + } + throw error; + } +} + +// Ejecutar la función +crearFormulario() + .then(() => console.log('Proceso completado')) + .catch(() => console.log('Proceso fallido')); \ No newline at end of file -- 2.43.0 From a16e82005ee8f5f546ef8b4572f1f3e30e328a42 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 2 Apr 2025 12:43:37 -0600 Subject: [PATCH 3/3] formato correcto de cuestionario por verificar --- Dockerfile | 2 +- docker-compose.yml | 2 +- src/cuestionario/cuestionario.controller.ts | 7 + .../cuestionario.documentation.ts | 31 ++- src/cuestionario/cuestionario.module.ts | 4 +- src/cuestionario/cuestionario.service.ts | 156 ++++++++++- src/cuestionario/dto/formulario.schema.ts | 94 +++++++ src/docs/docs.module.ts | 4 +- src/main.ts | 6 +- src/opcion/opcion.controller.ts | 7 + src/opcion/opcion.documentation.ts | 153 +++++++++++ src/pregunta/pregunta.module.ts | 12 +- src/pregunta/pregunta.service.ts | 252 +++++++++++++++++- src/seccion/seccion.module.ts | 18 +- src/seccion/seccion.service.ts | 201 +++++++++++++- utils/get_formulario_feria.ts | 197 ++++++++++++++ utils/load-form.js | 2 +- utils/test-crear-formulario.ts | 4 +- 18 files changed, 1109 insertions(+), 43 deletions(-) create mode 100644 src/cuestionario/dto/formulario.schema.ts create mode 100644 src/opcion/opcion.documentation.ts create mode 100644 utils/get_formulario_feria.ts diff --git a/Dockerfile b/Dockerfile index 0b618ea..d122543 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,7 @@ COPY . . RUN npm run build # Puerto expuesto -EXPOSE 3000 +EXPOSE 4200 # Comando para iniciar en modo desarrollo CMD ["npm", "run", "start:dev"] diff --git a/docker-compose.yml b/docker-compose.yml index 8e9a574..9b4cf9a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -24,7 +24,7 @@ services: - .:/app - /app/node_modules ports: - - "4200:3000" + - "4200:4200" depends_on: - mariadb restart: unless-stopped diff --git a/src/cuestionario/cuestionario.controller.ts b/src/cuestionario/cuestionario.controller.ts index ff9a3b2..f62f04f 100644 --- a/src/cuestionario/cuestionario.controller.ts +++ b/src/cuestionario/cuestionario.controller.ts @@ -3,6 +3,7 @@ import { CuestionarioService } from './cuestionario.service'; import { CreateCuestionarioDto } from './dto/create-cuestionario.dto'; import { UpdateCuestionarioDto } from './dto/update-cuestionario.dto'; import { CuestionarioApiDocumentation } from './cuestionario.documentation'; +import { ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; @Controller('cuestionario') @CuestionarioApiDocumentation.ApiController @@ -26,6 +27,12 @@ export class CuestionarioController { findOne(@Param('id') id: string) { return this.cuestionarioService.findOne(+id); } + + @Get(':id/formulario') + @CuestionarioApiDocumentation.ApiGetFormulario + findFormulario(@Param('id') id: string) { + return this.cuestionarioService.findCompleto(+id); + } @Patch(':id') @CuestionarioApiDocumentation.ApiUpdate diff --git a/src/cuestionario/cuestionario.documentation.ts b/src/cuestionario/cuestionario.documentation.ts index 3b8cc7d..c170471 100644 --- a/src/cuestionario/cuestionario.documentation.ts +++ b/src/cuestionario/cuestionario.documentation.ts @@ -1,10 +1,38 @@ -import { ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags, getSchemaPath } from '@nestjs/swagger'; import { feriaSexualidad } from '../../utils/crear_formulario_feria'; import { applyDecorators } from '@nestjs/common'; +import { FormularioDto } from './dto/formulario.schema'; export class CuestionarioApiDocumentation { // Decoradores para toda la clase del controlador static ApiController = ApiTags('Cuestionarios'); + + // Documentación para obtener el formulario completo + static ApiGetFormulario = applyDecorators( + ApiOperation({ + summary: 'Obtener formulario completo', + description: 'Devuelve el cuestionario completo con la misma estructura que se usa para crearlo' + }), + ApiParam({ + name: 'id', + description: 'ID del cuestionario', + required: true, + type: 'number', + example: 1 + }), + ApiResponse({ + status: 200, + description: 'Formulario completo con todas sus secciones, preguntas y opciones', + type: FormularioDto, + content: { + 'application/json': { + schema: { $ref: getSchemaPath(FormularioDto) }, + example: feriaSexualidad + } + } + }), + ApiResponse({ status: 404, description: 'Cuestionario no encontrado' }) + ); // Documentación para crear un cuestionario static ApiCreate = applyDecorators( @@ -14,6 +42,7 @@ export class CuestionarioApiDocumentation { }), ApiBody({ description: 'Datos del cuestionario a crear', + type: FormularioDto, examples: { feriaSexualidad: { summary: 'Ejemplo de cuestionario para feria de sexualidad', diff --git a/src/cuestionario/cuestionario.module.ts b/src/cuestionario/cuestionario.module.ts index 642215f..400dd2a 100644 --- a/src/cuestionario/cuestionario.module.ts +++ b/src/cuestionario/cuestionario.module.ts @@ -10,6 +10,7 @@ import { SeccionPregunta } from '../seccion_pregunta/entities/seccion_pregunta.e import { Opcion } from '../opcion/entities/opcion.entity'; import { PreguntaOpcion } from '../pregunta_opcion/entities/pregunta_opcion.entity'; import { TipoPregunta } from '../tipo_pregunta/entities/tipo_pregunta.entity'; +import { TipoCuestionario } from '../tipo_cuestionario/entities/tipo_cuestionario.entity'; @Module({ imports: [ @@ -21,7 +22,8 @@ import { TipoPregunta } from '../tipo_pregunta/entities/tipo_pregunta.entity'; SeccionPregunta, Opcion, PreguntaOpcion, - TipoPregunta + TipoPregunta, + TipoCuestionario ]) ], controllers: [CuestionarioController], diff --git a/src/cuestionario/cuestionario.service.ts b/src/cuestionario/cuestionario.service.ts index 784cd9d..f73e8dd 100644 --- a/src/cuestionario/cuestionario.service.ts +++ b/src/cuestionario/cuestionario.service.ts @@ -1,6 +1,6 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository, DataSource } from 'typeorm'; +import { Repository, DataSource, In } from 'typeorm'; import { CreateCuestionarioDto } from './dto/create-cuestionario.dto'; import { UpdateCuestionarioDto } from './dto/update-cuestionario.dto'; import { Cuestionario } from './entities/cuestionario.entity'; @@ -11,6 +11,7 @@ import { SeccionPregunta } from '../seccion_pregunta/entities/seccion_pregunta.e import { Opcion } from '../opcion/entities/opcion.entity'; import { PreguntaOpcion } from '../pregunta_opcion/entities/pregunta_opcion.entity'; import { TipoPregunta } from '../tipo_pregunta/entities/tipo_pregunta.entity'; +import { TipoCuestionario } from '../tipo_cuestionario/entities/tipo_cuestionario.entity'; @Injectable() export class CuestionarioService { @@ -149,8 +150,155 @@ export class CuestionarioService { return this.cuestionarioRepository.find(); } - findOne(id: number) { - return this.cuestionarioRepository.findOne({ where: { id_cuestionario: id } }); + async findOne(id: number) { + const cuestionario = await this.cuestionarioRepository.findOne({ + where: { id_cuestionario: id } + }); + + if (!cuestionario) { + throw new NotFoundException(`Cuestionario con ID ${id} no encontrado`); + } + + return cuestionario; + } + + async findCompleto(id: number) { + // Obtener el cuestionario básico + const cuestionario = await this.cuestionarioRepository.findOne({ + where: { id_cuestionario: id } + }); + + if (!cuestionario) { + throw new NotFoundException(`Cuestionario con ID ${id} no encontrado`); + } + + // Para fines de demostración, usar valores hardcodeados + // para el tipo de cuestionario según get_formulario_feria.ts + const tipoCuestionario = { + id_tipo_cuestionario: 1, + tipo_cuestionario: 'Encuesta' + }; + + // Obtener las relaciones cuestionario-seccion + const cuestionarioSecciones = await this.cuestionarioSeccionRepository.find({ + where: { id_cuestionario: id }, + order: { posicion: 'ASC' } + }); + + // Obtener todas las secciones + const seccionIds = cuestionarioSecciones.map(cs => cs.id_seccion); + const secciones = await this.seccionRepository.find({ + where: { id_seccion: In(seccionIds) } + }); + + const seccionesFormateadas = await Promise.all( + cuestionarioSecciones.map(async (cs) => { + const seccion = secciones.find(s => s.id_seccion === cs.id_seccion); + + if (!seccion) return null; + + // Obtener relaciones sección-pregunta + const seccionPreguntas = await this.seccionPreguntaRepository.find({ + where: { id_seccion: seccion.id_seccion }, + order: { posicion: 'ASC' } + }); + + // Obtener preguntas + const preguntaIds = seccionPreguntas.map(sp => sp.id_pregunta); + const preguntas = await this.preguntaRepository.find({ + where: { id_pregunta: In(preguntaIds) } + }); + + // Formatear cada pregunta según el formato requerido + const preguntasFormateadas = await Promise.all( + seccionPreguntas.map(async (sp) => { + const pregunta = preguntas.find(p => p.id_pregunta === sp.id_pregunta); + + if (!pregunta) return null; + + // Usar un mapeo hardcodeado para tipos de pregunta + const tiposPregunta = { + 1: { id_tipo: 1, tipo_pregunta: 'Cerrada' }, + 2: { id_tipo: 2, tipo_pregunta: 'Abierta' }, + 3: { id_tipo: 3, tipo_pregunta: 'Multiple' } + }; + + const tipoPregunta = tiposPregunta[pregunta.id_tipo_pregunta] || + { id_tipo: pregunta.id_tipo_pregunta, tipo_pregunta: 'Desconocido' }; + + // Obtener opciones para preguntas de tipo multiple/radio + const preguntaOpciones = await this.preguntaOpcionRepository.find({ + where: { id_pregunta: pregunta.id_pregunta }, + relations: ['opcion'], + order: { posicion: 'ASC' } + }); + + // Formatear las opciones según el formato requerido + const opcionesFormateadas = preguntaOpciones.map(po => ({ + id_pregunta_opcion: po.id_pregunta_opcion, + posicion: po.posicion, + id_opcion: po.opcion.id_opcion, + opcion: { + id_opcion: po.opcion.id_opcion, + opcion: po.opcion.opcion + } + })); + + // Construir objeto de pregunta según el formato requerido + return { + id_seccion_pregunta: sp.id_seccion_pregunta, + posicion: sp.posicion, + pregunta: { + id_pregunta: pregunta.id_pregunta, + pregunta: pregunta.pregunta, + contador_opcion: pregunta.contador_opcion, + obligatoria: pregunta.obligatoria, + id_tipo_pregunta: pregunta.id_tipo_pregunta, + id_opcion_dependiente: pregunta.id_opcion_dependiente, + tipo_pregunta: { + id_tipo: tipoPregunta.id_tipo, + tipo_pregunta: tipoPregunta.tipo_pregunta + }, + opciones: opcionesFormateadas + } + }; + }) + ).then(results => results.filter(p => p !== null)); + + // Construir objeto de sección según el formato requerido + return { + id_cuestionario_seccion: cs.id_cuestionario_seccion, + posicion: cs.posicion, + seccion: { + id_seccion: seccion.id_seccion, + contador_pregunta: seccion.contador_pregunta, + descripcion: seccion.descripcion, + titulo: seccion.titulo + }, + preguntas: preguntasFormateadas + }; + }) + ).then(results => results.filter(s => s !== null)); + + // Construir objeto final respetando el formato de get_formulario_feria.ts + return { + tipo_cuestionario: { + id_tipo_cuestionario: tipoCuestionario.id_tipo_cuestionario, + tipo_cuestionario: tipoCuestionario.tipo_cuestionario + }, + cuestionario: { + id_cuestionario: cuestionario.id_cuestionario, + nombre_form: cuestionario.nombre_form, + contador_secciones: cuestionario.contador_secciones, + descripcion: cuestionario.descripcion, + editable: cuestionario.editable, + fecha_inicio: cuestionario.fecha_inicio ? cuestionario.fecha_inicio.toISOString() : null, + fecha_fin: cuestionario.fecha_fin ? cuestionario.fecha_fin.toISOString() : null, + id_cuestionario_original: cuestionario.id_cuestionario_original, + id_tipo_cuestionario: cuestionario.id_tipo_cuestionario, + secciones: seccionesFormateadas + } + }; } update(id: number, updateCuestionarioDto: UpdateCuestionarioDto) { diff --git a/src/cuestionario/dto/formulario.schema.ts b/src/cuestionario/dto/formulario.schema.ts new file mode 100644 index 0000000..d30e259 --- /dev/null +++ b/src/cuestionario/dto/formulario.schema.ts @@ -0,0 +1,94 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class OpcionDto { + @ApiProperty({ + description: 'Valor de la opción', + example: 'Si' + }) + valor: string; +} + +export class PreguntaDto { + @ApiProperty({ + description: 'Título de la pregunta', + example: '¿Eres parte de la comunidad de la FES Acatlán?' + }) + titulo: string; + + @ApiProperty({ + description: 'Indica si la pregunta es obligatoria', + example: true + }) + obligatoria: boolean; + + @ApiProperty({ + description: 'Tipo de pregunta (Texto, Numero, Multiple, Radio, Abierto, Fecha)', + example: 'Multiple' + }) + tipo: string; + + @ApiProperty({ + description: 'Lista de opciones para preguntas de tipo Multiple o Radio', + type: [OpcionDto], + required: false + }) + opciones?: OpcionDto[]; +} + +export class SeccionDto { + @ApiProperty({ + description: 'Título de la sección', + example: 'Información Personal' + }) + titulo: string; + + @ApiProperty({ + description: 'Descripción de la sección', + example: 'Proporciona tus datos personales para poder contactarte.' + }) + descripcion?: string; + + @ApiProperty({ + description: 'Lista de preguntas que conforman la sección', + type: [PreguntaDto] + }) + preguntas: PreguntaDto[]; +} + +export class FormularioDto { + @ApiProperty({ + description: 'Nombre del formulario', + example: 'Registro para la Feria de la Sexualidad - FES Acatlán' + }) + nombre_form: string; + + @ApiProperty({ + description: 'Descripción del formulario', + example: 'La Feria de la Sexualidad es un espacio seguro e informativo...' + }) + descripcion?: string; + + @ApiProperty({ + description: 'Fecha de inicio del formulario', + example: '2025-03-07T00:00:01' + }) + fecha_inicio?: string; + + @ApiProperty({ + description: 'Fecha de fin del formulario', + example: '2025-03-18T23:59:59' + }) + fecha_fin?: string; + + @ApiProperty({ + description: 'ID del tipo de cuestionario', + example: 1 + }) + id_tipo_cuestionario: number; + + @ApiProperty({ + description: 'Secciones que conforman el formulario', + type: [SeccionDto] + }) + secciones: SeccionDto[]; +} \ No newline at end of file diff --git a/src/docs/docs.module.ts b/src/docs/docs.module.ts index 90d1820..21a9935 100644 --- a/src/docs/docs.module.ts +++ b/src/docs/docs.module.ts @@ -6,7 +6,7 @@ import { INestApplication } from '@nestjs/common'; @Module({}) export class DocsModule { static setupSwagger(app: INestApplication) { - const document = SwaggerModule.createDocument(app, swaggerConfig); - SwaggerModule.setup('api-docs', app, document); + const document = SwaggerModule.createDocument(app, swaggerConfig); + SwaggerModule.setup('api-docs', app, document); } } diff --git a/src/main.ts b/src/main.ts index 3f43b41..bdbcf72 100644 --- a/src/main.ts +++ b/src/main.ts @@ -51,8 +51,8 @@ async function bootstrap() { } // Start the server - await app.listen(process.env.PORT ?? 3000); - console.log('API en ejecución en http://localhost:3000'); - console.log('Swagger disponible en http://localhost:3000/api-docs'); + await app.listen(process.env.PORT ?? 4200); + console.log('API en ejecución en http://localhost:4200'); + console.log('Swagger disponible en http://localhost:4200/api-docs'); } bootstrap(); diff --git a/src/opcion/opcion.controller.ts b/src/opcion/opcion.controller.ts index 635597a..c64e3dc 100644 --- a/src/opcion/opcion.controller.ts +++ b/src/opcion/opcion.controller.ts @@ -2,32 +2,39 @@ import { Controller, Get, Post, Body, Patch, Param, Delete } from '@nestjs/commo import { OpcionService } from './opcion.service'; import { CreateOpcionDto } from './dto/create-opcion.dto'; import { UpdateOpcionDto } from './dto/update-opcion.dto'; +import { OpcionApiDocumentation } from './opcion.documentation'; @Controller('opcion') +@OpcionApiDocumentation.ApiController export class OpcionController { constructor(private readonly opcionService: OpcionService) {} @Post() + @OpcionApiDocumentation.ApiCreate create(@Body() createOpcionDto: CreateOpcionDto) { return this.opcionService.create(createOpcionDto); } @Get() + @OpcionApiDocumentation.ApiGetAll findAll() { return this.opcionService.findAll(); } @Get(':id') + @OpcionApiDocumentation.ApiGetOne findOne(@Param('id') id: string) { return this.opcionService.findOne(+id); } @Patch(':id') + @OpcionApiDocumentation.ApiUpdate update(@Param('id') id: string, @Body() updateOpcionDto: UpdateOpcionDto) { return this.opcionService.update(+id, updateOpcionDto); } @Delete(':id') + @OpcionApiDocumentation.ApiRemove remove(@Param('id') id: string) { return this.opcionService.remove(+id); } diff --git a/src/opcion/opcion.documentation.ts b/src/opcion/opcion.documentation.ts new file mode 100644 index 0000000..701d375 --- /dev/null +++ b/src/opcion/opcion.documentation.ts @@ -0,0 +1,153 @@ +import { ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { applyDecorators } from '@nestjs/common'; + +export class OpcionApiDocumentation { + // Decoradores para toda la clase del controlador + static ApiController = ApiTags('Opciones'); + + // Documentación para crear una opción + static ApiCreate = applyDecorators( + ApiOperation({ + summary: 'Crear una nueva opción', + description: 'Crea una nueva opción para una pregunta' + }), + ApiBody({ + description: 'Datos de la opción a crear', + schema: { + type: 'object', + required: ['opcion'], + properties: { + opcion: { type: 'string', example: 'Si' } + } + } + }), + ApiResponse({ + status: 201, + description: 'Opción creada correctamente', + schema: { + type: 'object', + properties: { + id_opcion: { type: 'number', example: 1 }, + opcion: { type: 'string', example: 'Si' } + } + } + }), + ApiResponse({ status: 400, description: 'Datos de la opción inválidos' }), + ApiResponse({ status: 500, description: 'Error interno del servidor' }) + ); + + // Documentación para obtener todas las opciones + static ApiGetAll = applyDecorators( + ApiOperation({ + summary: 'Obtener todas las opciones', + description: 'Retorna una lista de todas las opciones registradas' + }), + ApiResponse({ + status: 200, + description: 'Lista de opciones obtenida correctamente', + schema: { + type: 'array', + items: { + type: 'object', + properties: { + id_opcion: { type: 'number', example: 1 }, + opcion: { type: 'string', example: 'Si' } + } + } + } + }), + ApiResponse({ status: 500, description: 'Error interno del servidor' }) + ); + + // Documentación para obtener una opción por ID + static ApiGetOne = applyDecorators( + ApiOperation({ + summary: 'Obtener una opción por ID', + description: 'Retorna una opción específica por su ID' + }), + ApiParam({ + name: 'id', + description: 'ID de la opción', + required: true, + type: 'number', + example: 1 + }), + ApiResponse({ + status: 200, + description: 'Opción obtenida correctamente', + schema: { + type: 'object', + properties: { + id_opcion: { type: 'number', example: 1 }, + opcion: { type: 'string', example: 'Si' } + } + } + }), + ApiResponse({ status: 404, description: 'Opción no encontrada' }), + ApiResponse({ status: 500, description: 'Error interno del servidor' }) + ); + + // Documentación para actualizar una opción + static ApiUpdate = applyDecorators( + ApiOperation({ + summary: 'Actualizar una opción', + description: 'Actualiza los datos de una opción existente' + }), + ApiParam({ + name: 'id', + description: 'ID de la opción a actualizar', + required: true, + type: 'number', + example: 1 + }), + ApiBody({ + description: 'Datos a actualizar de la opción', + schema: { + type: 'object', + properties: { + opcion: { type: 'string', example: 'Opción actualizada' } + } + } + }), + ApiResponse({ + status: 200, + description: 'Opción actualizada correctamente', + schema: { + type: 'object', + properties: { + affected: { type: 'number', example: 1 } + } + } + }), + ApiResponse({ status: 400, description: 'Datos de actualización inválidos' }), + ApiResponse({ status: 404, description: 'Opción no encontrada' }), + ApiResponse({ status: 500, description: 'Error interno del servidor' }) + ); + + // Documentación para eliminar una opción + static ApiRemove = applyDecorators( + ApiOperation({ + summary: 'Eliminar una opción', + description: 'Elimina permanentemente una opción por su ID' + }), + ApiParam({ + name: 'id', + description: 'ID de la opción a eliminar', + required: true, + type: 'number', + example: 1 + }), + ApiResponse({ + status: 200, + description: 'Opción eliminada correctamente', + schema: { + type: 'object', + properties: { + affected: { type: 'number', example: 1 } + } + } + }), + ApiResponse({ status: 404, description: 'Opción no encontrada' }), + ApiResponse({ status: 500, description: 'Error interno del servidor' }) + ); +} \ No newline at end of file diff --git a/src/pregunta/pregunta.module.ts b/src/pregunta/pregunta.module.ts index d829b94..158e0a3 100644 --- a/src/pregunta/pregunta.module.ts +++ b/src/pregunta/pregunta.module.ts @@ -4,14 +4,22 @@ import { PreguntaController } from './pregunta.controller'; import { TypeOrmModule } from '@nestjs/typeorm'; import { Pregunta } from './entities/pregunta.entity'; import { TipoPreguntaModule } from '../tipo_pregunta/tipo_pregunta.module'; +import { TipoPregunta } from '../tipo_pregunta/entities/tipo_pregunta.entity'; +import { Opcion } from '../opcion/entities/opcion.entity'; +import { PreguntaOpcion } from '../pregunta_opcion/entities/pregunta_opcion.entity'; @Module({ imports: [ - TypeOrmModule.forFeature([Pregunta]), + TypeOrmModule.forFeature([ + Pregunta, + TipoPregunta, + Opcion, + PreguntaOpcion + ]), TipoPreguntaModule ], controllers: [PreguntaController], providers: [PreguntaService], - exports: [TypeOrmModule] + exports: [TypeOrmModule, PreguntaService] }) export class PreguntaModule {} diff --git a/src/pregunta/pregunta.service.ts b/src/pregunta/pregunta.service.ts index b196cc8..b190384 100644 --- a/src/pregunta/pregunta.service.ts +++ b/src/pregunta/pregunta.service.ts @@ -1,26 +1,254 @@ -import { Injectable } from '@nestjs/common'; -import { CreatePreguntaDto } from './dto/create-pregunta.dto'; +import { Injectable, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository, DataSource } from 'typeorm'; +import { CreatePreguntaDto, OpcionDto } from './dto/create-pregunta.dto'; import { UpdatePreguntaDto } from './dto/update-pregunta.dto'; +import { Pregunta } from './entities/pregunta.entity'; +import { TipoPregunta } from '../tipo_pregunta/entities/tipo_pregunta.entity'; +import { Opcion } from '../opcion/entities/opcion.entity'; +import { PreguntaOpcion } from '../pregunta_opcion/entities/pregunta_opcion.entity'; @Injectable() export class PreguntaService { - create(createPreguntaDto: CreatePreguntaDto) { - return 'This action adds a new pregunta'; + constructor( + @InjectRepository(Pregunta) + private preguntaRepository: Repository, + @InjectRepository(TipoPregunta) + private tipoPreguntaRepository: Repository, + @InjectRepository(Opcion) + private opcionRepository: Repository, + @InjectRepository(PreguntaOpcion) + private preguntaOpcionRepository: Repository, + private dataSource: DataSource + ) {} + + async create(createPreguntaDto: CreatePreguntaDto) { + const queryRunner = this.dataSource.createQueryRunner(); + await queryRunner.connect(); + await queryRunner.startTransaction(); + + try { + // Obtener el tipo de pregunta por su nombre + const tipoPregunta = await this.tipoPreguntaRepository.findOne({ + where: { tipo_pregunta: createPreguntaDto.tipo } + }); + + if (!tipoPregunta) { + throw new Error(`Tipo de pregunta '${createPreguntaDto.tipo}' no encontrado`); + } + + // Crear pregunta + const pregunta = new Pregunta(); + pregunta.pregunta = createPreguntaDto.titulo; + pregunta.obligatoria = createPreguntaDto.obligatoria || false; + pregunta.id_tipo_pregunta = tipoPregunta.id_tipo; + pregunta.id_opcion_dependiente = createPreguntaDto.id_opcion_dependiente || undefined; + pregunta.contador_opcion = createPreguntaDto.opciones?.length || 0; + + const savedPregunta = await queryRunner.manager.save(pregunta); + + // Crear opciones para preguntas de tipo multiple/radio + if (createPreguntaDto.opciones && createPreguntaDto.opciones.length > 0) { + await this.createOpciones( + queryRunner, + savedPregunta.id_pregunta, + createPreguntaDto.opciones + ); + } + + await queryRunner.commitTransaction(); + + return { + success: true, + pregunta: savedPregunta + }; + } catch (error) { + await queryRunner.rollbackTransaction(); + throw error; + } finally { + await queryRunner.release(); + } } - findAll() { - return `This action returns all pregunta`; + private async createOpciones( + queryRunner: any, + preguntaId: number, + opciones: OpcionDto[] + ) { + for (let i = 0; i < opciones.length; i++) { + const opcionDto = opciones[i]; + + // Crear opción + const opcion = new Opcion(); + opcion.opcion = opcionDto.valor; + + const savedOpcion = await queryRunner.manager.save(opcion); + + // Vincular opción con pregunta + const preguntaOpcion = new PreguntaOpcion(); + preguntaOpcion.id_pregunta = preguntaId; + preguntaOpcion.opcion = savedOpcion; + preguntaOpcion.posicion = i + 1; + + await queryRunner.manager.save(preguntaOpcion); + } } - findOne(id: number) { - return `This action returns a #${id} pregunta`; + async findAll() { + return this.preguntaRepository.find(); } - update(id: number, updatePreguntaDto: UpdatePreguntaDto) { - return `This action updates a #${id} pregunta`; + async findOne(id: number) { + const pregunta = await this.preguntaRepository.findOne({ + where: { id_pregunta: id } + }); + + if (!pregunta) { + throw new NotFoundException(`Pregunta con ID ${id} no encontrada`); + } + + return pregunta; } - remove(id: number) { - return `This action removes a #${id} pregunta`; + async findWithOptions(id: number) { + const pregunta = await this.preguntaRepository.findOne({ + where: { id_pregunta: id }, + relations: ['tipoPregunta'] + }); + + if (!pregunta) { + throw new NotFoundException(`Pregunta con ID ${id} no encontrada`); + } + + // Obtener las opciones para esta pregunta + const preguntaOpciones = await this.preguntaOpcionRepository.find({ + where: { id_pregunta: id }, + relations: ['opcion'], + order: { posicion: 'ASC' } + }); + + const opciones = preguntaOpciones.map(po => ({ + id: po.opcion.id_opcion, + valor: po.opcion.opcion, + posicion: po.posicion + })); + + return { + ...pregunta, + tipo: pregunta.tipoPregunta?.tipo_pregunta, + opciones + }; + } + + async update(id: number, updatePreguntaDto: UpdatePreguntaDto) { + const queryRunner = this.dataSource.createQueryRunner(); + await queryRunner.connect(); + await queryRunner.startTransaction(); + + try { + const pregunta = await this.preguntaRepository.findOne({ + where: { id_pregunta: id } + }); + + if (!pregunta) { + throw new NotFoundException(`Pregunta con ID ${id} no encontrada`); + } + + // Actualizar los campos básicos de la pregunta + 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; + + // Si se proporciona un nuevo tipo de pregunta, actualizarlo + if (updatePreguntaDto.tipo) { + const tipoPregunta = await this.tipoPreguntaRepository.findOne({ + where: { tipo_pregunta: updatePreguntaDto.tipo } + }); + + if (!tipoPregunta) { + throw new Error(`Tipo de pregunta '${updatePreguntaDto.tipo}' no encontrado`); + } + + pregunta.id_tipo_pregunta = tipoPregunta.id_tipo; + } + + // Actualizar la pregunta + const updatedPregunta = await queryRunner.manager.save(pregunta); + + // Si se proporcionan nuevas opciones, actualizarlas + if (updatePreguntaDto.opciones && updatePreguntaDto.opciones.length > 0) { + // Eliminar las opciones existentes + const existingOptions = await this.preguntaOpcionRepository.find({ + where: { id_pregunta: id } + }); + + for (const option of existingOptions) { + await queryRunner.manager.remove(option); + } + + // Crear las nuevas opciones + await this.createOpciones( + queryRunner, + id, + updatePreguntaDto.opciones + ); + + // Actualizar el contador de opciones + updatedPregunta.contador_opcion = updatePreguntaDto.opciones.length; + await queryRunner.manager.save(updatedPregunta); + } + + await queryRunner.commitTransaction(); + + return { + success: true, + pregunta: updatedPregunta + }; + } catch (error) { + await queryRunner.rollbackTransaction(); + throw error; + } finally { + await queryRunner.release(); + } + } + + async remove(id: number) { + const queryRunner = this.dataSource.createQueryRunner(); + await queryRunner.connect(); + await queryRunner.startTransaction(); + + try { + const pregunta = await this.preguntaRepository.findOne({ + where: { id_pregunta: id } + }); + + if (!pregunta) { + throw new NotFoundException(`Pregunta con ID ${id} no encontrada`); + } + + // Eliminar las relaciones con opciones + const preguntaOpciones = await this.preguntaOpcionRepository.find({ + where: { id_pregunta: id } + }); + + for (const po of preguntaOpciones) { + await queryRunner.manager.remove(po); + } + + // Eliminar la pregunta + await queryRunner.manager.remove(pregunta); + + await queryRunner.commitTransaction(); + + return { + success: true, + message: `Pregunta con ID ${id} eliminada correctamente` + }; + } catch (error) { + await queryRunner.rollbackTransaction(); + throw error; + } finally { + await queryRunner.release(); + } } } diff --git a/src/seccion/seccion.module.ts b/src/seccion/seccion.module.ts index d0d484c..8baf400 100644 --- a/src/seccion/seccion.module.ts +++ b/src/seccion/seccion.module.ts @@ -3,11 +3,25 @@ import { SeccionService } from './seccion.service'; import { SeccionController } from './seccion.controller'; import { TypeOrmModule } from '@nestjs/typeorm'; import { Seccion } from './entities/seccion.entity'; +import { Pregunta } from '../pregunta/entities/pregunta.entity'; +import { SeccionPregunta } from '../seccion_pregunta/entities/seccion_pregunta.entity'; +import { TipoPregunta } from '../tipo_pregunta/entities/tipo_pregunta.entity'; +import { Opcion } from '../opcion/entities/opcion.entity'; +import { PreguntaOpcion } from '../pregunta_opcion/entities/pregunta_opcion.entity'; @Module({ - imports: [TypeOrmModule.forFeature([Seccion])], + imports: [ + TypeOrmModule.forFeature([ + Seccion, + Pregunta, + SeccionPregunta, + TipoPregunta, + Opcion, + PreguntaOpcion + ]) + ], controllers: [SeccionController], providers: [SeccionService], - exports: [TypeOrmModule] + exports: [TypeOrmModule, SeccionService] }) export class SeccionModule {} diff --git a/src/seccion/seccion.service.ts b/src/seccion/seccion.service.ts index 789091b..7915dd1 100644 --- a/src/seccion/seccion.service.ts +++ b/src/seccion/seccion.service.ts @@ -1,26 +1,205 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, NotFoundException } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository, DataSource } from 'typeorm'; import { CreateSeccionDto } from './dto/create-seccion.dto'; import { UpdateSeccionDto } from './dto/update-seccion.dto'; +import { Seccion } from './entities/seccion.entity'; +import { Pregunta } from '../pregunta/entities/pregunta.entity'; +import { SeccionPregunta } from '../seccion_pregunta/entities/seccion_pregunta.entity'; +import { TipoPregunta } from '../tipo_pregunta/entities/tipo_pregunta.entity'; +import { Opcion } from '../opcion/entities/opcion.entity'; +import { PreguntaOpcion } from '../pregunta_opcion/entities/pregunta_opcion.entity'; @Injectable() export class SeccionService { - create(createSeccionDto: CreateSeccionDto) { - return 'This action adds a new seccion'; + constructor( + @InjectRepository(Seccion) + private seccionRepository: Repository, + @InjectRepository(Pregunta) + private preguntaRepository: Repository, + @InjectRepository(SeccionPregunta) + private seccionPreguntaRepository: Repository, + @InjectRepository(TipoPregunta) + private tipoPreguntaRepository: Repository, + @InjectRepository(Opcion) + private opcionRepository: Repository, + @InjectRepository(PreguntaOpcion) + private preguntaOpcionRepository: Repository, + private dataSource: DataSource + ) {} + + async create(createSeccionDto: CreateSeccionDto) { + const queryRunner = this.dataSource.createQueryRunner(); + await queryRunner.connect(); + await queryRunner.startTransaction(); + + try { + // 1. Crear sección + const seccion = new Seccion(); + seccion.titulo = createSeccionDto.titulo; + seccion.descripcion = createSeccionDto.descripcion; + seccion.contador_pregunta = createSeccionDto.preguntas?.length || 0; + + const savedSeccion = await queryRunner.manager.save(seccion); + + // 2. Crear preguntas y vincularlas a la sección + if (createSeccionDto.preguntas && createSeccionDto.preguntas.length > 0) { + for (let i = 0; i < createSeccionDto.preguntas.length; i++) { + const preguntaDto = createSeccionDto.preguntas[i]; + + // Obtener el tipo de pregunta por su nombre + const tipoPregunta = await queryRunner.manager.findOne(TipoPregunta, { + where: { tipo_pregunta: preguntaDto.tipo }, + }); + + if (!tipoPregunta) { + throw new Error(`Tipo de pregunta '${preguntaDto.tipo}' no encontrado`); + } + + // Crear pregunta + const pregunta = new Pregunta(); + pregunta.pregunta = preguntaDto.titulo; + pregunta.obligatoria = preguntaDto.obligatoria || false; + pregunta.id_tipo_pregunta = tipoPregunta.id_tipo; + pregunta.id_opcion_dependiente = preguntaDto.id_opcion_dependiente || undefined; + pregunta.contador_opcion = preguntaDto.opciones?.length || 0; + + const savedPregunta = await queryRunner.manager.save(pregunta); + + // Vincular pregunta con sección + const seccionPregunta = new SeccionPregunta(); + seccionPregunta.id_seccion = savedSeccion.id_seccion; + seccionPregunta.id_pregunta = savedPregunta.id_pregunta; + seccionPregunta.posicion = i + 1; + + await queryRunner.manager.save(seccionPregunta); + + // Crear opciones para preguntas de tipo multiple/radio + if (preguntaDto.opciones && preguntaDto.opciones.length > 0) { + for (let j = 0; j < preguntaDto.opciones.length; j++) { + const opcionDto = preguntaDto.opciones[j]; + + // Crear opción + const opcion = new Opcion(); + opcion.opcion = opcionDto.valor; + + const savedOpcion = await queryRunner.manager.save(opcion); + + // Vincular opción con pregunta + const preguntaOpcion = new PreguntaOpcion(); + preguntaOpcion.id_pregunta = savedPregunta.id_pregunta; + preguntaOpcion.opcion = savedOpcion; + preguntaOpcion.posicion = j + 1; + + await queryRunner.manager.save(preguntaOpcion); + } + } + } + } + + await queryRunner.commitTransaction(); + + return { + success: true, + seccion: savedSeccion, + }; + } catch (error) { + await queryRunner.rollbackTransaction(); + throw error; + } finally { + await queryRunner.release(); + } } - findAll() { - return `This action returns all seccion`; + async findAll() { + return this.seccionRepository.find(); } - findOne(id: number) { - return `This action returns a #${id} seccion`; + async findOne(id: number) { + const seccion = await this.seccionRepository.findOne({ + where: { id_seccion: id } + }); + + if (!seccion) { + throw new NotFoundException(`Sección con ID ${id} no encontrada`); + } + + return seccion; } - update(id: number, updateSeccionDto: UpdateSeccionDto) { - return `This action updates a #${id} seccion`; + async findWithPreguntas(id: number) { + const seccion = await this.seccionRepository.findOne({ + where: { id_seccion: id } + }); + + if (!seccion) { + throw new NotFoundException(`Sección con ID ${id} no encontrada`); + } + + // Obtener las relaciones seccion-pregunta para esta sección + const seccionPreguntas = await this.seccionPreguntaRepository.find({ + where: { id_seccion: id }, + relations: ['pregunta'] + }); + + // Extraer y organizar las preguntas + const preguntas = await Promise.all( + seccionPreguntas.map(async (sp) => { + const pregunta = await this.preguntaRepository.findOne({ + where: { id_pregunta: sp.id_pregunta } + }); + + // Obtener las opciones para preguntas de tipo multiple/radio + const preguntaOpciones = await this.preguntaOpcionRepository.find({ + where: { id_pregunta: sp.id_pregunta }, + relations: ['opcion'], + order: { posicion: 'ASC' } + }); + + const opciones = preguntaOpciones.map(po => po.opcion); + + return { + ...pregunta, + posicion: sp.posicion, + opciones + }; + }) + ); + + // Ordenar las preguntas por posición + preguntas.sort((a, b) => a.posicion - b.posicion); + + return { + ...seccion, + preguntas + }; } - remove(id: number) { - return `This action removes a #${id} seccion`; + async update(id: number, updateSeccionDto: UpdateSeccionDto) { + const seccion = await this.seccionRepository.findOne({ + where: { id_seccion: id } + }); + + if (!seccion) { + throw new NotFoundException(`Sección con ID ${id} no encontrada`); + } + + // Actualizar solo los campos proporcionados + if (updateSeccionDto.titulo) seccion.titulo = updateSeccionDto.titulo; + if (updateSeccionDto.descripcion !== undefined) seccion.descripcion = updateSeccionDto.descripcion; + + return this.seccionRepository.save(seccion); + } + + async remove(id: number) { + const seccion = await this.seccionRepository.findOne({ + where: { id_seccion: id } + }); + + if (!seccion) { + throw new NotFoundException(`Sección con ID ${id} no encontrada`); + } + + return this.seccionRepository.remove(seccion); } } diff --git a/utils/get_formulario_feria.ts b/utils/get_formulario_feria.ts new file mode 100644 index 0000000..dfffb3c --- /dev/null +++ b/utils/get_formulario_feria.ts @@ -0,0 +1,197 @@ +export const cuestionario_feria = { + tipo_cuestionario: { + id_tipo_cuestionario: 1, + tipo_cuestionario: 'Encuesta', + }, + cuestionario: { + id_cuestionario: 10, + nombre_form: + 'Registro para la Feria de la Sexualidad - Comunidad FES Acatlán', + contador_secciones: 2, + descripcion: + 'La Feria de la Sexualidad es un espacio seguro e informativo dirigido a toda la comunidad universitaria. Este registro nos ayudará a organizar mejor las actividades, talleres y charlas, así como conocer tus intereses. Por favor, completa el siguiente formulario para confirmar tu participación.', + editable: true, + fecha_inicio: '2025-03-26T00:00:00', + fecha_fin: '2025-03-31T23:59:59', + id_cuestionario_original: null, + id_tipo_cuestionario: 1, + secciones: [ + { + id_cuestionario_seccion: 1, + posicion: 1, + seccion: { + id_seccion: 100, + contador_pregunta: 2, + descripcion: 'Preguntas generales', + titulo: 'General', + }, + preguntas: [ + { + id_seccion_pregunta: 1000, + posicion: 1, + pregunta: { + id_pregunta: 10000, + pregunta: '¿Cómo calificaría nuestro servicio?', + contador_opcion: 4, + obligatoria: true, + id_tipo_pregunta: 1, + id_opcion_dependiente: null, + tipo_pregunta: { + id_tipo: 1, + tipo_pregunta: 'Cerrada', + }, + opciones: [ + { + id_pregunta_opcion: 50000, + posicion: 1, + id_opcion: 90000, + opcion: { + id_opcion: 90000, + opcion: 'Excelente', + }, + }, + { + id_pregunta_opcion: 50001, + posicion: 2, + id_opcion: 90001, + opcion: { + id_opcion: 90001, + opcion: 'Bueno', + }, + }, + { + id_pregunta_opcion: 50002, + posicion: 3, + id_opcion: 90002, + opcion: { + id_opcion: 90002, + opcion: 'Regular', + }, + }, + { + id_pregunta_opcion: 50003, + posicion: 4, + id_opcion: 90003, + opcion: { + id_opcion: 90003, + opcion: 'Malo', + }, + }, + ], + }, + }, + { + id_seccion_pregunta: 1001, + posicion: 2, + pregunta: { + id_pregunta: 10001, + pregunta: '¿Recomendaría nuestro servicio?', + contador_opcion: 2, + obligatoria: true, + id_tipo_pregunta: 1, + id_opcion_dependiente: null, + tipo_pregunta: { + id_tipo: 1, + tipo_pregunta: 'Cerrada', + }, + opciones: [ + { + id_pregunta_opcion: 50004, + posicion: 1, + id_opcion: 90004, + opcion: { + id_opcion: 90004, + opcion: 'Sí', + }, + }, + { + id_pregunta_opcion: 50005, + posicion: 2, + id_opcion: 90005, + opcion: { + id_opcion: 90005, + opcion: 'No', + }, + }, + ], + }, + }, + ], + }, + { + id_cuestionario_seccion: 2, + posicion: 2, + seccion: { + id_seccion: 101, + contador_pregunta: 1, + descripcion: 'Preguntas adicionales para conocer más detalles', + titulo: 'Adicionales', + }, + preguntas: [ + { + id_seccion_pregunta: 1002, + posicion: 1, + pregunta: { + id_pregunta: 10002, + pregunta: '¿Qué mejorarías en nuestro servicio?', + contador_opcion: 0, + obligatoria: false, + id_tipo_pregunta: 2, + id_opcion_dependiente: null, + tipo_pregunta: { + id_tipo: 2, + tipo_pregunta: 'Abierta', + }, + opciones: [], + }, + }, + { + id_seccion_pregunta: 1003, + posicion: 3, + pregunta: { + id_pregunta: 10003, + pregunta: '¿Qué aspectos del servicio fueron de tu agrado?', + contador_opcion: 3, + obligatoria: false, + id_tipo_pregunta: 3, // Puedes usar este ID para distinguir múltiples + id_opcion_dependiente: null, + tipo_pregunta: { + id_tipo: 3, + tipo_pregunta: 'Multiple', + }, + opciones: [ + { + id_pregunta_opcion: 50006, + posicion: 1, + id_opcion: 90006, + opcion: { + id_opcion: 90006, + opcion: 'Rapidez', + }, + }, + { + id_pregunta_opcion: 50007, + posicion: 2, + id_opcion: 90007, + opcion: { + id_opcion: 90007, + opcion: 'Atención al cliente', + }, + }, + { + id_pregunta_opcion: 50008, + posicion: 3, + id_opcion: 90008, + opcion: { + id_opcion: 90008, + opcion: 'Facilidad de uso', + }, + }, + ], + }, + }, + ], + }, + ], + }, + }; \ No newline at end of file diff --git a/utils/load-form.js b/utils/load-form.js index 800e54b..9489b53 100644 --- a/utils/load-form.js +++ b/utils/load-form.js @@ -4,7 +4,7 @@ const axios = require('axios'); const fs = require('fs'); const path = require('path'); -const API_URL = 'http://localhost:3000'; +const API_URL = 'http://localhost:4200'; async function loadFormFromFile(filePath) { try { diff --git a/utils/test-crear-formulario.ts b/utils/test-crear-formulario.ts index 7e88c02..62b9ada 100644 --- a/utils/test-crear-formulario.ts +++ b/utils/test-crear-formulario.ts @@ -1,8 +1,8 @@ import { feriaSexualidad } from './crear_formulario_feria'; import axios from 'axios'; -// Asumiendo que el servidor está corriendo en localhost:3000 -const API_URL = 'http://localhost:3000'; +// Asumiendo que el servidor está corriendo en localhost:4200 +const API_URL = 'http://localhost:4200'; async function crearFormulario() { try { -- 2.43.0