3.4.07. CRUD de empresas - diezMalena/api_FCTFiller GitHub Wiki

Planteamiento

En el CRUD de empresas, cualquier docente podrá visualizar y añadir (ver registro de empresas) en el sistema. También podrán hacer lo propio con los convenios o acuerdos que tenga su centro educativo con cada empresa, así como descargar y editar los anexos correspondientes (0 y 0A).

Actualmente, cualquier docente también puede eliminar y editar los datos de las empresas; y también anular convenios. Esto último estará reservado sólo a los directores de los centros educativos, mientras que la eliminación y edición de datos de empresas se reservará al usuario superadministrador (ver estudio del usuario o roadmap). También se plantea la renovación de convenios (Anexo XVI).

Esta funcionalidad supone la base de casi todas las funcionalidades de la aplicación. Los convenios o acuerdos entre centros educativos y empresas son el primer paso del proceso de las FCT, que permiten a los tutores asignar a sus alumnos a ciertas empresas, iniciándose así las prácticas para ellos. En términos de diseño de la base de datos, el convenio es el principal nexo de unión entre las empresas y los centros educativos.

Los componentes de cliente implicados son GestionEmpresasComponent, ModalEmpresaComponent y ModalConvenioComponent. El CrudEmpresasService hace peticiones a funciones de la API ubicadas, por lo general, en el ControladorTutorFCT.

Podemos dividir esta funcionalidad en dos: la gestión de empresas y la gestión de sus convenios. Pero, primero, echemos un vistazo a la funcionalidad.

Vista general

Esta funcionalidad contiene la gestión de empresas y convenios, pero se plantean en la misma unidad por motivos prácticos: el usuario objetivo es el docente (independientemente de su rol), que está asociado a un solo centro de estudios. Cada empresa va a poder hacer un solo convenio con cada centro de estudios, por lo que se ha aprovechado para mostrar en el mismo lugar la información de la empresa y la del convenio con el centro de estudios, si lo hubiese.

image

Como se puede observar en la captura de pantalla, el principal objeto de la vista es la tabla, que contiene la información justa sobre cada empresa y dos grupos de botones: uno para las acciones sobre la empresa (edición y eliminación) y otro para las acciones relativas al convenio (crearlo, visualizarlo, editarlo, anularlo o renovarlo). En cuanto a la visualización de la empresa, es accesible clickeando sobre su fila en la tabla.

Gestión de empresas

Obtención de empresas

El componente se inicializa con una petición al servidor de todas las empresas que hay en la base de datos. Como cada interacción con el servidor, se hace en tres pasos:

  1. Componente: getEmpresas(). Esta función se llama en la construcción del componente e inicializa la variable de empresas (Empresa[]) mediante subscripción a la petición que hace el servicio al servidor.
  2. Servicio: getEmpresas(dniProfesor: string). Recibe el DNI del profesor, en este caso el que ha iniciado sesión, y hace una petición POST a la ruta /solicitar_empresas/profesor={dniProfesor} de la API, devolviendo un Observable con los datos de la empresa al que se subscribe el método del componente.
  3. API: getEmpresasFromProfesor(string $dniProfesor). Extrae todas las empresas de la base de datos y las recorre una a una, introduciendo de forma encapsulada los datos del convenio (si hubiese) y de su representante legal.

De esta forma, en el cliente se dispondrá de toda la información de la empresa, con los datos del convenio y del representante legal, de forma que se reduzcan al mínimo las peticiones al servidor.

En la template, mediante un *ngFor, se recorren las empresas. Esto permite mostrar su información en la tabla y enviar el objeto completo a los modales correspondientes.

Modal de empresa

El componente asociado a este modal es ModalEmpresaComponent.

Visualización y edición

image image El modal de empresa contiene los datos de la empresa seleccionada y los muestra como texto plano o como formulario en función de si está o no en modo de edición.

Esto se consigue enviando mediante trigger el objeto de tipo Empresa y una variable boolean:

public mostrarEmpresa(empresa: Empresa, editar: boolean) {
  this.modal.open(ModalEmpresaComponent, {
    size: 'lg',
    backdrop: 'static',
    keyboard: false,
  });
  this.crudEmpresasService.empresaTrigger.emit([empresa, editar]);
}

Cada una de las <td> llama a esta función con editar en false:

<td scope="col" class="align-middle cursor-pointer" attr.aria-label="Ver en detalle {{empresa.nombre}}" (click)="mostrarEmpresa(empresa, false)">{{empresa.cif}}</td>

mientras que el botón de edición llama a la función con editar a true:

<button class="btn btn-outline-warning p-2 py-1 m-1" (click)="mostrarEmpresa(empresa, true)" placement="left" ngbTooltip="Editar empresa" attr.aria-label="Editar {{empresa.nombre}}"></button>

La variable editar nos permite aplicar clases y atributos dinámicamente, así como llamar a diferentes funciones en el submit, de forma que podemos utilizar el mismo modal con distintos objetivos.

<input type="text" class="{{editar ? 'form-control' : 'form-control-plaintext'}}" id="cif" placeholder="00000000X" formControlName="cif" [attr.readonly]="editar ? null : true" />

Dado que la visualización es una vista con la que el usuario no puede interactuar, pasamos a explicar en detalle el modo de edición. Este modo contiene un formulario con los datos de la empresa, su ubicación y su representante. Dicho formulario se valida de la misma manera que el resto de formularios en la aplicación.

Para prevenir que el usuario cierre el formulario tras haber hecho cambios y pierda todo el progreso, se cuenta con dos herramientas: la función onChanges() y el diálogo de confirmación

Submit

  1. La función onSubmit() en el componente ModalEmpresaComponent prepara los datos encapsulándolos en un objeto JSON y llama a la función updateEmpresa(empresa: Empresa). Este método se subscribe a la función del servicio y, dentro, llama con un await al método asíncrono updateRepresentante(representante: Trabajador), que hace lo propio con el representante.
  2. En CrudEmpresasService, el JSON se envía mediante PUT a la ruta /update_empresa de la API, devolviendo un Observable con la respuesta del servidor. El caso del representante es igual, cambiando la ruta por /update_trabajador
  3. En la API se hace una llamada al método updateEmpresa(Request $req), que hace un UPDATE de la tabla empresa mediante Eloquent y devuelve una response con el título y el mensaje que aparecerán en el toastr de cliente, así como un código de estado 200 o 400, según si se ha realizado o no la actualización. Se hace lo mismo para la actualización del representante, mediante el método updateTrabajador(Request $req)

Eliminación

La eliminación de una empresa ocurre previa invocación del diálogo de confirmación, ya que es una acción delicada para el sistema.

  1. En GestionEmpresasComponent, el método asíncrono deleteEmpresa(empresa: Empresa) se subscribe a la función homónima del servicio correspondiente, eliminando a la empresa del vector de empresas del componente y mostrando un toastr con el resultado.
  2. En CrudEmpresasService, el método deleteEmpresa recibe nada más que el ID de la empresa, que toma el lugar en la variable de la ruta /delete_empresa/id={id}. El servicio hace una petición DELETE al servidor y devuelve un Observable con la respuesta.
  3. En la API se llama a la función deleteEmpresa(int $idEmpresa), en la cual se eliminan primero a todos los trabajadores asociados a la empresa y, después, se hace un Empresa::destroy($idEmpresa). Se devuelve una response con el título y el mensaje que aparecerán en el toastr del cliente, así como el código de estado 200 o 400, según si ha ido todo bien o ha habido algún error, respectivamente.

Gestión de convenios

La obtención de datos del convenio está encapsulada dentro de la obtención de datos de la empresa. Dentro de la ya mencionada función getEmpresasFromProfesor del ControladorTutorFCT en la API, se recorren todas las empresas obtenidas de la base de datos y, a cada una, se le asigna su convenio si lo hay, null si no lo hay:

$codCentro = Profesor::find($dniProfesor)->cod_centro_estudios;
$empresas = Empresa::all();
foreach ($empresas as $empresa) {
    $empresa->convenio = Convenio::where('cod_centro', $codCentro)
        ->where('id_empresa', $empresa->id)->first();
    $empresa->representante = $this->getRepresentanteLegal($empresa->id);
}

Esto permite, en el cliente, generar unos u otros botones que nos permitirán distintas acciones respecto del convenio:

  • Si el centro no tiene convenio con la empresa, sólo se podrar crear un convenio.
  • Si el centro tiene convenio con la empresa, se podrá ver (y descargar), visualizar o anular.
  • Si el centro tiene convenio con la empresa y falta menos de un año para que caduque, se podrá renovar (Anexo XVI).

Salvo la anulación, el resto de acciones están controladas mediante el método de GestionEmpresasComponent: mostrarConvenio(empresa: Empresa, modo: number), que recibe el objeto de la empresa y el modo del modal: 0 para creación, 1 para visualización, 2 para edición y 3 para renovación. Con esto, invoca a un modal ModalConvenioComponent al que emite el objeto de la empresa, el del centro del usuario registrado (siempre un docente) y el modo.

Plantillas de los Anexos 0 / 0A

Los documentos que se han elaborado como plantilla de los Anexos 0 / 0A son los siguientes:

Anexo0.docx

Anexo0A.docx

Modal de convenio

Siguiendo la filosofía de los modales mencionada en la sección de la Wiki correspondiente, el modal de convenio tiene una estructura común para todas sus modalidades que varía según el modo, en este caso más que el modal de empresas. El componente asociado es ModalConvenioComponent.

image

El modal de convenio muestra los datos nucleares del convenio (los que se registran en la tabla de la BBDD). Sólo son editables el número de convenio y la fecha de inicio, debido a varias razones:

  • Las siglas del centro están definidas en la BBDD y son invariables. Es una constante para el usuario.
  • El convenio dura 4 años exactos, por lo que la fecha de fin siempre será la correspondiente a sumar 4 años a la fecha de inicio.
  • El código de convenio es un campo generado a partir de las siglas del centro, el número de convenio y su fecha de inicio.

El resto son datos del centro de estudios y de la empresa. Mediante el checkbox "Dispongo del documento", el usuario tendrá dos opciones: subir el documento (en PDF) o generar uno nuevo. En el segundo caso, se desplegará un formulario con los datos del centro de estudios y la empresa, ya completados con los datos disponibles en el componente (obtenidos del servidor), pero editables. De esta forma, se da al usuario la opción de modificar los campos en caso de que haya errores o de que el convenio se realizara anteriormente y los datos no corespondan a los que existen en la BBDD actualmente.

Planteamiento del formulario

Este formulario tiene una gran cantidad de datos, por lo que se decidió subdividirlo. El formulario general es un FormGroup que contiene distintos FormGroup, uno por sección de datos:

  • convenio: datos del convenio (parte superior de la vista).
  • anexo: base64 del archivo del anexo.
  • director: datos del director del centro de estudios.
  • centro: datos del centro de estudios.
  • representante: datos del representante legal de la empresa.
  • empresa: datos de la empresa.

Para simplificar la validación del formulario y el envío de datos al servidor, se trata cada FormGroup como un formulario independiente, con su get asociado. El onSubmit() y onChanges(), sin embargo, irán asociados al FormGroup que engloba al resto.

El onSubmit(), por su parte, hace el control de errores y establece todas las variables que se enviarán al servidor. Después evalua el modo del modal y llama a la función correspondiente.

Gestión de cambios

Para gestionar la visualización de la segunda parte del modal de convenio, se utiliza la función changeSubir(event: any), que, asociada al checkbox, cambia el valor de la variable bandera subir del componente. Con esto también se controla si el <input type="file"> está activado o desactivado. Esta variable se evalúa en el template mediante un *ngIf situado en el formulario:

<ng-container *ngIf="!subir && modo != 1">
    <!-- Formulario -->
</ng-container>

Cada vez que se modifica alguno de los campos del convenio, el código de convenio debe cambiar. Para ello, se asocia el evento change de los inputs del número de convenio y de la fecha de inicio a sendas funciones que cambian sus valores asociados en el formulario y llaman a la función auxiliar:

public construirCodConvenio(codCentro: string, num: number, fecha: string | Date): string

Esta función devuelve el código de convenio, que se establece como valor del elemento del formulario que le corresponde.

Adicionalmente, el método changeFechaConvenio(event: any) hace una llamada al servicio auxiliar para tratar fechas con la que suma 4 años a la fecha de inicio y establece dicho valor en el input de la fecha de fin.

Creación (CREATE)

La fecha de inicio que se muestra por defecto en esta vista es la del día actual. El proceso que se sigue cuando se envían los datos al servidor es el habitual:

  1. El botón de submit de la parte inferior del formulario acciona el método del componente onSubmitAdd(), que subscribe al método addConvenio del servicio. Actualiza los datos del convenio para que se actualice la vista padre, muestra un toastr con la información correspondiente e invoca un diálogo de confirmación que da la opción al usuario de descargar el Anexo 0 / 0A generado. Si se recibe un código de estado 409 del servidor, se avisa en el toastr de que se debe cambiar el código de convenio.

  2. En el servicio, el método addConvenio(datos: object) envía los datos mediante POST al servidor mediante la ruta /add_convenio, devolviendo un Observable con la respuesta, que incluye la ruta del anexo generado (si no se ha subido).

  3. En la API, la función addConvenio(Request $req) crea un registro en la tabla convenio de la BBDD con los datos del convenio encapsulados en el JSON. Según el valor de la propiedad $req->subir_anexo (true o false), se hace uso de la función generarAnexo0 o Auxiliar::guardarFichero. En cualquier caso, se actualiza la ruta del anexo en la tabla convenio, se crea un registro en la tabla de anexos y se devuelve una response con la ruta del anexo y un código de estado 201 o, si ha habido algún error, el mensaje de la excepción y el código de error correspondiente (409 si hay registro repetido -excepción SQL 1062-, 400 si es otro error de la BBDD y 500 si es del servidor).

    El método generarAnexo0(Request $req) recibe una request, a partir de la cual crea modelos de los elementos para pasarlos por la función Auxiliar::modelsToArray, de forma que las variables puedan ocupar su lugar en el documento de texto.

Visualización (READ)

image

Esta versión del modal sólo muestra los datos del convenio y permite al usuario descargar el Anexo 0 / 0A. Esta funcionalidad se desencadena mediante el método downloadAnexo(ruta: string):

  1. En el componente, downloadAnexo(ruta: string) se subscribe al método descargarAnexo0 del RegistroEmpresaService, ya que la creación del convenio estaba planteada en esa funcionalidad al inicio. Si tal archivo no existe, el servidor devuelve un 404, lo cual se trata en esta función para mostrar un toastr pidiendo al usuario que vuelva a generar o subir el anexo. Si existe, las siguiente líneas de código permiten que se lleve a cabo la descarga en el navegador:
    let arr = ruta.split('\\', 3);
    let nombre = arr.pop();
    const blob = new Blob([response], {
        type: 'application/octet-stream',
    });
    FileSaver.saveAs(blob, nombre);
  2. En el servicio, descargarAnexo0(ruta: string) hace una petición POST al servidor mediante la ruta /descargarAnexo0 con la ruta incrustada como objeto JSON. Devuelve un Observable con la respuesta del servidor, que incluye el archivo.
  3. En la API, se llama al método descargarAnexo0(Request $req), que envía una Response de tipo download con la ruta del anexo, lo cual envía el archivo. Si en dicha ruta no hay localizado ningún archivo, se envía una respuesta JSON con un código de estado 404, que se trata en el componente.

Edición (UPDATE)

La actualización de los datos del anexo se gestiona mediante el siguiente proceso:

  1. En el componente, el método onSubmitEdit(), previamente a la petición al servidor, comprueba si ha cambiado el código de convenio ha cambiado, con el objetivo de almacenar el código de convenio anterior para poder llevar a cabo la actualización en la BBDD. Tras esto, se subscribe al método que hace la llamada al servidor. Dentro, se llama a un toastr de forma similar a como se hacía en la creación de convenios. Si la respuesta es exitosa, además, se cambian los datos del convenio en el objeto de la empresa, de forma que se actualice la vista padre.

  2. En el servicio, la función editConvenio(datos: object) hace una petición PUT al servidor mediante la ruta /editar_convenio, con los datos del formulario enviados como objeto JSON. Devuelve Observable con la respuesta del servidor, que se gestiona en el método del componente.

  3. En la API, se llama a la función updateConvenio(Request $req). El primer paso es comprobar si el código de convenio ha cambiado, en cuyo caso la variable se almacena para comprobar el campo en el WHERE asociado al UPDATE. Tras esto, primero se elimina el archivo que había antes, extrayendo la ruta anterior del anexo de la tabla convenio. Igual que en la creación del anexo, se comprueba si el usuario ha subido un anexo propio o lo quiere generar, y se almacena la ruta resultante. Esta ruta sirve para actualizar el registro de la tabla convenio (junto con el resto de cambios) y el de la tabla anexos.

    Si el proceso ha sido exitoso, la response JSON devuelve un código 201. Si el código de convenio está repetido, se devuelve un código 409; y si ha habido otra excepción SQL se devuelve un 400. El error 500 se reserva a otras excepciones genéricas.

Anulación del convenio

La anulación del convenio está planteada de forma que el registro del convenio de la BBDD desaparezca, pero el anexo sólo pase a estar deshabilitado. El proceso que sigue esta funcionalidad es el habitual:

  1. En el componente (GestionEmpresasComponent), el botón llama a la función asíncrona anularConvenio(empresa: Empresa). Tras una confirmación por parte del usuario, se subscribe al método eliminarConvenio de CrudEmpresasService. Si todo ha ido bien, se establece la variable del convenio dentro del objeto de la empresa como undefined, de forma que se actualice la vista. En cualquier caso, se muestra un toastr con la información de la respuesta.
  2. En el servicio, el método eliminarConvenio(cod: string) se encarga de hacer la petición DELETE al servidor mediante la ruta /eliminar_convenio/cod={cod}, lo cual devuelve un Observable con la respuesta del servidor, que se trata en el componente. El código de convenio concatena sus elementos mediante '/', lo cual entra en conflicto con el sistema de rutas. Por tanto, se deben sustituir todas las instancias de '/' por '-'.
  3. En la API, se llama a la función deleteConvenio(string $cod). En primer lugar, vuelve a reemplazar las instancias '-' por '/' para retornar el código de convenio a su estado original. Tras esto, en la tabla anexos se establece el registro correspondiente al anexo del convenio como deshabilitado, y se elimina el registro de la tabla de convenios. Finalmente, se devuelve la respuesta JSON típica: 200 si ha ido todo bien, 500 si ha habido un error del servidor.

Renovación del convenio

La renovación del convenio permite tanto al centro de estudios como a la empresa renovar su convenio por cuatro años más. Esta acción tiene como resultado la cumplimentación del Anexo XVI. La opción de renovar será visible en el CRUD cuando quede menos de un año para que termine el convenio entre dichas entidades.

Esta funcionalidad sigue el siguiente proceso:

  1. En el componente, el método onSubmitRenovar(), previamente a la petición al servidor, al igual que sucede en la edición del convenio, comprueba si el código de convenio ha cambiado, con el objetivo de almacenar el código de convenio anterior para poder llevar a cabo la actualización en la BBDD. Tras esto, se subscribe al método que hace la llamada al servidor. Si la respuesta es exitosa, además, se cambian los datos del convenio en el objeto de la empresa, de forma que se actualice la vista padre.

  2. En el servicio, la función renovarConvenio(datos: object) hace una petición POST al servidor mediante la ruta /renovar_convenio, con los datos del formulario enviados como objeto JSON. Devuelve Observable con la respuesta del servidor, que se gestiona en el método del componente.

  3. En la API, se llama a la función renovarConvenio(Request $req). El primer paso es comprobar si el código de convenio ha cambiado, en cuyo caso la variable se almacena para comprobar el campo en el WHERE asociado al UPDATE. Tras esto, primero se elimina el archivo que había antes, extrayendo la ruta anterior del anexo de la tabla convenio. El siguiente paso consiste en añadirle cuatro años más al convenio. Para ello, utilizamos el siguiente código:

      date("Y-m-d", strtotime($req->convenio['fecha_fin'] . "+ 4 year"));

    Después de ello, se hace una llamada a la función generarAnexoXVI(Request $req), que recibe una request, a partir de la cual crea modelos de los elementos para pasarlos por la función Auxiliar::modelsToArray, de forma que las variables puedan ocupar su lugar en el documento de texto referente al Anexo XVI.

    Si el proceso ha sido exitoso, la response JSON devuelve un código 201. Si el código de convenio está repetido, se devuelve un código 409; y si ha habido otra excepción SQL se devuelve un 400. El error 500 se reserva a otras excepciones genéricas.

⚠️ **GitHub.com Fallback** ⚠️