3.4.06. Cuestionarios - diezMalena/api_FCTFiller GitHub Wiki

Planteamiento

El objetivo general del apartado de cuestionarios de la aplicación será la gestión por parte de los diferentes roles que conforman la aplicación de los cuestionarios para el ciclo de FCT.

  • Creación, edición y borrado.
  • Resolución o cumplimentación.
  • Visualización de respuestas, estadísticas y descarga de los resultados.

Descripción general

Los formularios se generan de forma totalmente dinámica en función del tipo de pregunta (configurable) actualmente radio o texto. También incluyen validación para los campos necesarios. Las principales puntos a destacar son:

  • Fácil configuración de nuevos tipos de campos.
  • Generación dinámica de campos.
  • Validación de campos obligatorios

Actores

Jefatura

Desde jefatura se podrá gestionar el CRUD de cuestionarios, creación, edición, borrado y activación o desactivación de los mismos.

jefatura_listar

Creación

jefatura_crear

Cliente

El modelo cuestionario cuenta con las variables que definirán el objeto, incluyendo array de pregunta, que son las que definen el texto y tipo de cada pregunta identificada por su id y relacionadas con su cuestionario por el id_cuestionario.

export class CuestionarioModel {
  id!: number;
  titulo!: string;
  destinatario!: string;
  codigo_centro!: string;
  preguntas!: Array<PreguntaModel>;
  activo!: boolean;
}
export class PreguntaModel {
  id!: number;
  id_cuestionario!: number;
  tipo!: string;
  pregunta!: string;
}

Ejemplo JSON: Al hacerse la llamada post para guardar el cuestionario desde el cliente se mandarían los datos en formato json al servidor del siguiente modo.

{
	"activo": false,
	"codigo_centro": "1111VDG",
	"destinatario": "alumno",
	"id": 0,
	"preguntas": [{
		"tipo": "rango",
		"pregunta": "Pregunta 1"
	}, {
		"tipo": "texto",
		"pregunta": "Pregunta 2"
	}],
	"titulo": "Nuevo Cuestionario"
}

El servicio encargado de realizar la llamada post para almacenar los datos es CuestionarioService: add

add(cuestionario: CuestionarioModel): Observable<any> {
    const headers = this.headers;
    return this.http.post(`${crearCuestionarioURL}`, cuestionario,{ headers }).pipe(
      map((res) => {
        return res || {};
      }),
      catchError(this.handleError)
    )
  }

Servidor

El json recibido se procesa por el controlador: controladorJefatura.crearCuestionario que crea el cuestionario en la tabla cuestionarios y las preguntas en la tabla preguntas-cuestionario

Edición

jefatura_editar

Cliente

Para la edición se carga el cuestionario en función de su id y se rellenan los campos del formulario con los datos obtenidos

  getCuestionarioEdicion(id: string | null): Observable<any> {
    const headers = this.headers;
    return this.http.get<CuestionarioModel>(`${obtenerCuestionarioEdicionURL}/${id}`,{headers}).pipe(
      map((cuestionario: CuestionarioModel) => {
        return cuestionario || {};
      })
    )
  }

Una vez editado el cuestionario y al pulsar en guardar submit, se envía el cuestionario actualizado al servidor.

update(storage: CuestionarioModel): Observable<any> {
    const headers = this.headers;
    return this.http.post(`${editarCuestionarioURL}`, storage,{headers}).pipe(
      map((res) => {
        return res || {};
      }),
      catchError(this.handleError)
    )
  }

Servidor

Una vez recibido el json con los nuevos datos del formulario se actualiza las tablas cuestionarios y preguntas-cuestionario.

public function editarCuestionario(Request $r)
    {
        try {
            Cuestionario::where('id', '=', $r->id)->update(['destinatario' => $r->destinatario, 'titulo' => $r->titulo]);
            PreguntasCuestionario::where('id_cuestionario', '=', $r->id)->delete();
            foreach ($r->preguntas as $preg) {
                PreguntasCuestionario::create(['id_cuestionario' => $r->id ,'tipo' => $preg['tipo'], 'pregunta' => $preg['pregunta']]);
            }
            return response()->json(['mensaje' => 'Cuestionario editado correctamente'], 200);
        } catch (Exception $ex) {
            return response()->json(['mensaje' => 'Se ha producido un error en el servidor. Detalle del error: ' . $ex->getMessage()], 500);
        }
    }

Borrado

jefatura_borrado2

Cliente

Al clicar en el botón eliminar el cliente nos avisará con un mensaje de confirmación para hacer el borrado más seguro evitando borrados no deseados.

 public async eliminarCuestionario(id: number){
    let hacerlo = await this.dialogService.confirmacion(
      'Eliminar',
      `¿Está seguro de que desea eliminar el cuestionario?`
    );
    if (hacerlo) {
      this.cuestionarioService.eliminarCuestionario(id).subscribe({
      next: (res) => {
        this.toastr.success('Cuestionario Eliminado', 'Eliminado');
        this.listarCuestionarios();
      },
      error: e => {
        this.toastr.error('El cuestionario no ha podido eliminarse', 'Fallo');
      }
    })
    }else{
      this.toastr.info('El cuestionario está a salvo.', 'No eliminado');
    }
  }

Una vez confirmada la acción de borrar, mediante un dialogService se envía un json con el id del cuestionario a eliminar.

eliminarCuestionario(id: number): Observable<void> {
    const headers = this.headers;
    return this.http.delete<void>(`${eliminarCuestionarioURL}/${id}`,{headers}).pipe()
  }

Servidor

Recibido el json con el id del cuestionario a eliminar se procede a su borrado de la tabla cuestionarios y preguntas-cuestionario

public function eliminarCuestionario($id)
    {
        try {
            Cuestionario::where('id', '=', $id)->delete();
            return response()->json(['mensaje' => 'Cuestionario borrado correctamente'], 200);
        } catch (Exception $ex) {
            return response()->json(['mensaje' => 'Se ha producido un error en el servidor. Detalle del error: ' . $ex->getMessage()], 500);
        }
    }

Activación-Desactivación

jefatura-activacion

Teniendo en cuenta que solo un cuestionario puede estar activo para un codigo_centro y un destinatario, es decir, no puede haber varios cuestionarios para los alumnos o tutores de empresa de un mismo centro se plantea la siguiente solución:

Cliente

Para la desactivación no debe de contemplarse esta problemática, con lo cual solo se desactiva el cuestionario por su id.

desactivarCuestionario(id_cuestionario: number): Observable<any> {
    const headers = this.headers;
    return this.http.post(`${desactivarCuestionarioURL}/${id_cuestionario}`, null, {headers}).pipe(
      map((res) => {
        return res || {};
      }),
      catchError(this.handleError)
    )
  }

En el caso de la activación se buscan otros posibles cuestionarios que compartan destinatario y codigo_centro, procediendo a su desactivación, haciendo posible que solo uno esté activo en cada caso.

activarCuestionario(id_cuestionario: number , destinatario: string, cod_centro: string): Observable<any> {
    const headers = this.headers;
    return this.http.post(`${activarCuestionarioURL}/${id_cuestionario}/${destinatario}/${cod_centro}`, null, {headers}).pipe(
      map((res) => {
        return res || {};
      }),
      catchError(this.handleError)
    )
  }

Servidor

Para la activación del cuestionario se iteran todos los cuestionarios existentes para un mismo codigo_centro y destinatario y se desactivan antes de activarse el cuestionario seleccionado por su id.

public function activarCuestionario($id_formulario, $destinatario, $codigo_centro){
        Cuestionario::where([
            ['destinatario', '=',$destinatario],
            ['codigo_centro', '=', $codigo_centro]
            ])->update([
            'activo' => false
        ]);
        Cuestionario::where([
            ['id', '=',$id_formulario],
            ])->update([
            'activo' => true
        ]);
    }

Alumno

Contestar Cuestionario

Un alumno tendrá acceso a un formulario una vez sea activado el cuestionario para dicho codigo_centro, curso_academico y destinatario.

alumno_contestar_cuestionario

Cliente

Se obtiene el cuestionario según el id y se construye el formulario de respuesta por cada pregunta del cuestionario.

getCuestionario(destinatario: string | null, codigo_centro: string|undefined|null ): Observable<any> {
    const headers = this.headers;
    return this.http.get<CuestionarioModel>(`${obtenerCuestionarioURL}/${destinatario}/${codigo_centro}`,{headers}).pipe(
      map((cuestionario: CuestionarioModel) => {
        return cuestionario || {};
      })
    )
  }

Se realiza una validación para saber si hay un cuestionario disponible o si este ya ha sido respondido. En caso contrario se muestra un aviso por pantalla.

async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
    let respondido = await this.getData();
    if (this.usuario!.isAlumno() || this.usuario!.isTutorEmpresa()) {
      if(respondido){
        this.toastr.info('No hay cuestionarios pendientes de contestar', '¡Atención!');
        return false;
      }return true;
    }
    return false;
  }
  async getData() {
    let data = await this.cuestionarioRespondido.getDataSynchronous(this.usuario?.dni, this.usuario?.tipo)
    if(data.length>0){
      return true;
    } return false;
 }

Servidor

Recibido el json con las respuestas del cuestionario crearCuestionarioRespondido envía consulta a la base de datos para agregar el cuestionario_respondido y las preguntas respondidas en sus correspondientes tablas.

public function crearCuestionarioRespondido(Request $r)
    {
        $cuestionarioRespondido = CuestionarioRespondido::create([
            'titulo' => $r->titulo,
            'destinatario' => $r->destinatario,
            'id_usuario' => $r->id_usuario,
            'codigo_centro' => $r->codigo_centro,
            'ciclo' => $r->ciclo,
            'curso_academico' => $r->curso_academico,
            'dni_tutor_empresa' =>$r->dni_tutor_empresa,
        ]);
        foreach ($r->respuestas as $resp) {
            PreguntasRespondidas::create(['id_cuestionario_respondido' => $cuestionarioRespondido->id ,'tipo' => $resp['tipo'], 'pregunta' => $resp['pregunta'], 'respuesta' => $resp['respuesta']]);
        }
        return response()->json(['message' => 'Formulario guardado con éxito'], 200);
    }

En cuanto a la verificación en la parte de servidor contamos con la función verificarCuestionariRespondido cuyo parametro de entrada es el id_usuario y nos devuelve un json con el cuestionario si aun no ha sido respondido.

Tutor Empresa

Un tutor en una empresa puede tener asociados a varios alumnos de uno o varios centros. Es por ello que al acceder a cuestionario no se accede a un formulario de respuesta directamente. Se accede a un listado con todos los alumnos asociados, en dicho listado podrá acceder a cada uno de los cuestionarios para su resolución. Dichas respuestas estarán asociadas a cada centro y alumno, y podrán ser visualizadas por los diferentes actores de cada uno de los centros o tutores de alumnos con dichos alumnos asociados.

tutorEmpresa_listar_cuestionarios

Cliente

Desde tutor.empresa.service se llama a getCuestionarios según el dni del tutor. Esta función devuelve una lista de los cuestionarios de sus alumnos asociados.

getCuestionarios(dni:string | undefined): Observable<any> {
    const headers = this.headers;
    return this.http.get<Array<CuestionarioTutorEmpresaModel>>(`${obtenerCuestionariosURL}/${dni}`,{headers}).pipe(
      map((cuestionarios: Array<CuestionarioTutorEmpresaModel>) => {
        cuestionarios = <Array<CuestionarioTutorEmpresaModel>>cuestionarios.map((cuestionario: CuestionarioTutorEmpresaModel) => {
          return cuestionario
        });
        return cuestionarios || [];
      })
    )
  }

Servidor

public function obtenerCuestionariosTutorEmpresaAlumnos($dni){
        $fct = Fct::where('dni_tutor_empresa', '=', $dni)->get();
        $datos=[];
        foreach ($fct as $registroFct) {
            $cuestionarioRespondido = CuestionarioRespondido::where([
                ['dni_tutor_empresa', $dni],
                ['id_usuario', $registroFct->dni_alumno],
                ['curso_academico', $registroFct->curso_academico],
                ['destinatario', 'empresa']
            ])->get();
            $respondido=false;
            if(($cuestionarioRespondido) && (count($cuestionarioRespondido)>0)){
                $respondido=true;
            }
            $cod_centro='';
            $cod_grupo='';
            $matricula = Matricula::where('dni_alumno', '=', $registroFct->dni_alumno)
                ->select(['*'])
                ->first();
                if ($matricula){
                    $cod_centro=$matricula->cod_centro;
                    $cod_grupo=$matricula->cod_grupo;
                }
            $registroAlumno = array(
                'dni_alumno'=> $registroFct->dni_alumno,
                'curso_academico'=> $registroFct->curso_academico,
                'cod_centro'=> $cod_centro,
                'cod_grupo'=> $cod_grupo,
                'dni_alumno'=> $registroFct->dni_alumno,
                'respondido'=> $respondido);
            array_push($datos,$registroAlumno);
        }
        return response()->json($datos, 200);
    }

obtenerCuestionariosTutorEmpresaAlumnos busca por cada alumno asociado a tutor empresa se comprueba si el cuestionario ha sido contestado, y se recogen los datos necesarios para componer la respuesta.

Tutor Alumno

El tutor del alumno podrá visualizar la media de las respuestas de sus alumnos asociados, así como descargar en .pdf las respuestas de cada alumno o empresa con alumnos asociados de forma individual. Esta vista es compartida con el rol de jefatura. Por lo tanto se muestra ejemplo en el siguiente punto.

Tutor Alumno y Jefatura

jefatura_tutor_verRespuestas

Tanto el tutor de un alumno como jefatura tendrán acceso a los resultados de sus alumnos o empresas asociadas. El usuario puede seleccionar los datos mostrados filtrando por el curso_academico y por el destinatario del cuestionario (tutor de empresa o alumno). La gráfica muestra el resultado de la media de cada una de las preguntas de tipo rango existentes en el cuestionario. Por otro lado cuenta con una tabla que lista las respuestas al cuestionario obtenidas de alumnos o tutores. Esta tabla cuenta con un botón para la descarga de un documento en formato .pdf para la visualización de las respuestas de forma individual.

Generación de gráficos

Cliente

la llamada a cuestionarioRespondidoService.obtenerMediasCuestionariosRespondidos solicita al servidor los datos de las medias de las respuestas a las preguntas de tipo rango de los formularios. Se distingue según el tipo de usuario (tutor o alumno).

obtenerMediasCuestionariosRespondidos(curso_academico:string | undefined , destinatario: string | undefined, codigo_centro: string | undefined, tipo_usuario: string | undefined ): Observable<any> {
    const headers = this.headers;
    if (tipo_usuario == "tutor"){
      return this.http.get<Array<CuestionariosRespondidosMediasModel>>(`${obtenerMediasCuestionariosRespondidosFCTURL}?curso_academico=${curso_academico}&destinatario=${destinatario}&codigo_centro=${codigo_centro}`,{headers}).pipe(
        map((cuestionarios: Array<CuestionariosRespondidosMediasModel>) => {
          cuestionarios = <Array<CuestionariosRespondidosMediasModel>>cuestionarios.map((cuestionario: CuestionariosRespondidosMediasModel) => {
            return cuestionario
          });
          return cuestionarios || [];
        })
      )
    }else{
      return this.http.get<Array<CuestionariosRespondidosMediasModel>>(`${obtenerMediasCuestionariosRespondidosURL}?curso_academico=${curso_academico}&destinatario=${destinatario}&codigo_centro=${codigo_centro}`,{headers}).pipe(
        map((cuestionarios: Array<CuestionariosRespondidosMediasModel>) => {
          cuestionarios = <Array<CuestionariosRespondidosMediasModel>>cuestionarios.map((cuestionario: CuestionariosRespondidosMediasModel) => {
            return cuestionario
          });
          return cuestionarios || [];
        })
      )
    }
  }
Servidor

Los datos enviados serían las medias de las respuestas de tipo rango asociadas a un tipo de destinatario en un curso_academico concreto.

public function obtenerMediasCuestionariosRespondidos(Request $r){
       $respuestasFiltradas = CuestionarioRespondido::select(DB::raw('avg(preguntas_respondidas.respuesta) as value, preguntas_respondidas.pregunta as name'))->join('preguntas_respondidas', 'cuestionario_respondidos.id', '=', 'preguntas_respondidas.id_cuestionario_respondido')
            ->where([
                ['cuestionario_respondidos.curso_academico', '=', $r->curso_academico],
                ['cuestionario_respondidos.destinatario', '=', $r->destinatario],
                ['cuestionario_respondidos.codigo_centro', '=', $r->codigo_centro],
                ['preguntas_respondidas.tipo', '=', 'rango'],
            ])
            ->groupBy('preguntas_respondidas.pregunta')
            ->get();
            return response()->json($respuestasFiltradas, 200);
    }

Listar Cuestionarios Respondidos

Se genera un listado con todos los cuestionarios respondidos filtrando por curso_academico, destinatario y codigo_centro del usuario activo, pues solo podrán verse los resultados de los cuestionarios del centro desde el que se consultan.

Cliente

cuestionarioRespondidoService.obtenerCuestionariosRespondidos hace una llamada para la obtención de los datos de cada cuestionario respondido a partir del curso_academico, el destinatario y el codigo_centro

obtenerCuestionariosRespondidos(curso_academico:string | undefined , destinatario: string | undefined, codigo_centro: string | undefined, tipo_usuario: string | undefined ): Observable<any> {
    const headers = this.headers;
    if (tipo_usuario == "tutor"){
      return this.http.get<Array<CuestionarioRespondidoModel>>(`${listarCuestionariosRespondidosFCTURL}?curso_academico=${curso_academico}&destinatario=${destinatario}&codigo_centro=${codigo_centro}`,{headers}).pipe(
        map((cuestionarios: Array<CuestionarioRespondidoModel>) => {
          cuestionarios = <Array<CuestionarioRespondidoModel>>cuestionarios.map((cuestionario: CuestionarioRespondidoModel) => {
            return cuestionario
          });
          return cuestionarios || [];
        })
      )
    }else{
      return this.http.get<Array<CuestionarioRespondidoModel>>(`${listarCuestionariosRespondidosURL}?curso_academico=${curso_academico}&destinatario=${destinatario}&codigo_centro=${codigo_centro}`,{headers}).pipe(
        map((cuestionarios: Array<CuestionarioRespondidoModel>) => {
          cuestionarios = <Array<CuestionarioRespondidoModel>>cuestionarios.map((cuestionario: CuestionarioRespondidoModel) => {
            return cuestionario
          });
          return cuestionarios || [];
        })
      )
    }

  }
Servidor
 public function listarCuestionariosRespondidos(Request $r){
        try {
            $cuestionarios = CuestionarioRespondido::where([
            ['destinatario', '=',$r->destinatario],
            ['codigo_centro', '=', $r->codigo_centro],
            ['curso_academico', '=', $r->curso_academico]
            ])->get();
            return response()->json($cuestionarios, 200);
        } catch (Exception $ex) {
            return response()->json(['mensaje' => 'Se ha producido un error en el servidor. Detalle del error: ' . $ex->getMessage()], 500);
        }
    }

Descarga de pdf

pdf_descarga

Cliente

Desde el cliente se envía el id_cuestionario a descargar.

descargarCuestionario(id_cuestionario: number, tipo_usuario: string | undefined ): any {
    if (tipo_usuario == "tutor"){
      const headers = this.headers;
    return this.http.get(`${descargarCuestionarioFCTURL}/${id_cuestionario}`,{ headers, responseType: 'blob'});
    }else{
      const headers = this.headers;
      return this.http.get(`${descargarCuestionarioURL}/${id_cuestionario}`,{ headers, responseType: 'blob'});
    }
  }
```

##### Servidor

Se recibe el id_cuestionario y se busca el cuestionario y sus preguntas para posteriormente con la librería DOMPDF renderizar el fichero blade (vista cuestionario.blade.php) pasándole los datos para realizar la descarga.

```php
public function descargarCuestionario($id_cuestionario){
        $datos = array();
        $cuestionario = CuestionarioRespondido::select('*')->where('id', '=', $id_cuestionario)->get();
        foreach (PreguntasRespondidas::where('id_cuestionario_respondido', '=', $cuestionario[0]->id)->get() as $p) {
            $datos[] = $p;
        }
        $pdf = PDF::loadView('cuestionario', [
            'datos' =>$datos,
            'titulo' =>$cuestionario[0]->titulo,
            'id_usuario' =>$cuestionario[0]->id_usuario,
            'destinatario' =>$cuestionario[0]->destinatario,
            'codigo_centro' =>$cuestionario[0]->codigo_centro,
            'ciclo' =>$cuestionario[0]->ciclo,
            'curso_academico' =>$cuestionario[0]->curso_academico,
        ]);
        return $pdf->download('pdf_file.pdf');
    }
```
⚠️ **GitHub.com Fallback** ⚠️