3.4.06. Cuestionarios - diezMalena/api_FCTFiller GitHub Wiki
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.
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
Desde jefatura se podrá gestionar el CRUD de cuestionarios, creación, edición, borrado y activación o desactivación de los mismos.
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)
)
}
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
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)
)
}
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);
}
}
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()
}
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);
}
}
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:
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)
)
}
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
]);
}
Un alumno tendrá acceso a un formulario una vez sea activado el cuestionario para dicho codigo_centro
, curso_academico
y destinatario
.
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;
}
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.
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.
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 || [];
})
)
}
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.
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.
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.
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 || [];
})
)
}
}
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);
}
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.
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 || [];
})
)
}
}
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);
}
}
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');
}
```